같이 공부하기로 한 친구들이 있는데 이번에 디스코드 채널을 하나 파서 음성채널에서 같이 공부하기로 했다
그냥 공부만 하기에는 뭔가 아쉬운 부분이 있어서 음성채널에 들어와 있는 시간을 측정해서 기록을 하는 디스코드봇을 만들어보기로 했다
나는 SpringBoot
를 사용하기 때문에 관련된 정보를 찾아보니 JDA
라는게 있어서 이걸로 디스코드 봇을 한 번 만들어 봤다!
우선 디스코드봇을 사용하기 위해 등록을 해줘야 한다
https://discord.com/developers/applications
여기에 접속을 해서 New Application
을 통해 study-bot
을 만들어준다
실습은 두번째 봇을 만드는거라서 study-bot2
라고 해줬다
MESSAGE CONTENT INTENT
부분도 활성화 해주어야 한다
그리고 TOKEN
부분에서 Reset Token
을 눌러 비밀번호를 입력하고 토큰 값을 받아야 한다
이 토큰은 나중에 쓰여서 알아둬야 한다
만들고 OAuth2
부분에서 bot
부분과 Administrator
부분에 체크를 해준다
그리고 GENERATED URL
부분에 있는 곳으로 들어가면
이런 화면이 뜨게 되고 계속하기를 누르면
선택한 디스코드 채널에 봇이 나타나게 된다!
지금은 아무런 기능도 하지 않는 이름만 있는 봇이다
이제 원하는 기능을 만들어서 봇에 적용시켜보자!
일단 원하는 기능은 이렇다
! 일간기록
! 주간기록
! 월간기록
! 전체일간기록
! 전체주간기록
! 전체월간기록
위의 명령어를 통해서 동작하는 기능을 원했고 만들기 시작했다
우선 SpringBoot
프로젝트를 하나 만들어주고 시작해보자
딱 위의 기능만 원했기 때문에 코드의 퀄리티를 챙기기보다는 기능의 구현에 집중했다
→ 막 만들었다는 뜻
만드는게 엄청 어렵지는 않았다
자세히 뜯어보지는 않았지만, 이벤트 리스너를 기본으로 두고 동작하는 것 같았다
public static void main(String[] args){
ApplicationContext context = SpringApplication.run(StudybotApplication.class, args);
DiscordBotToken discordBotTokenEntity = context.getBean(DiscordBotToken.class);
String discordBotToken = discordBotTokenEntity.getDiscordBotToken();
JDA jda = JDABuilder.createDefault(discordBotToken)
.setActivity(Activity.playing("메시지 기다리는 중!"))
.enableIntents(GatewayIntent.MESSAGE_CONTENT)
.addEventListeners(context.getBean(StudyBotDiscordListener.class))
.build();
JDA jdaVoice = JDABuilder.createDefault(discordBotToken)
.setActivity(Activity.playing("메시지 기다리는 중!"))
.enableIntents(GatewayIntent.MESSAGE_CONTENT, GatewayIntent.GUILD_VOICE_STATES)
.addEventListeners(context.getBean(VoiceChannelTracker.class))
.build();
}
main 코드를 잠깐 보자면 별거 없다
시간을 좀 썼던 부분은 .addEventListners()
부분에서 빈을 넣어줄때 new
로 넣으면 계속 오류가 나서 context.getBean()
을 통해서 넣어줬다
이벤트 리스너에 빈을 등록한 후 해당 채널에서 설정한 이벤트들이 발성하면 로직에 따라 기능이 동작한다
위의 jda
는 메세지를 받기 위한거고, 아래의 jdaVoice
는 음성채널 이벤트를 받기 위한 코드이다
그럼 이제 내부를 살펴보자
메세지 JDA
는 StudyBotDiscordListener
라는 클래스에서 이벤트를 캐치해서 동작을 하도록 되어 있다
@Override
public void onMessageReceived(MessageReceivedEvent event) {
User user = event.getAuthor();
Member member = event.getMember();
TextChannel textChannel = event.getChannel().asTextChannel();
Message message = event.getMessage();
log.info("get message : " + message.getContentDisplay());
if (user.isBot()) {
return;
} else if (message.getContentDisplay().equals("")) {
log.info("문자열 비어있음");
}
String[] messageArray = message.getContentDisplay().split(" ");
if (messageArray[0].equalsIgnoreCase("!")) {
String[] messageArgs = Arrays.copyOfRange(messageArray, 1, messageArray.length);
String nickname = member.getNickname(); // 길드에서의 닉네임 (null일 수도 있음)
String displayName = nickname != null ? nickname : user.getName(); // 닉네임 없으면 기본 이름
for (String msg : messageArgs) {
String returnMessage = sendMessage(msg, displayName, user.getName()); // 길드 이름 전달
textChannel.sendMessage(returnMessage).queue();
}
}
}
코드만 보면 어렵지 않은데 디스코드 채널에 메세지 이벤트가 들어왔을때 !
로 시작하면 if(messageArray[0].equalsIgnoreCase("!"))
아래 부분의 코드를 실행 시킨다
sendMessage()
함수를 잠깐 살펴보자
private String sendMessage(String message, String displayName, String userName) {
String returnMessage = "";
switch (message) {
case "안녕":
returnMessage = displayName + " 얼른 공부좀해!";
break;
case "하기싫어":
returnMessage = displayName + " 그냥 좀 해";
break;
case "오늘만쉴까":
returnMessage = displayName + " 그럼 평생 쉬겠지";
break;
case "김민선바보":
returnMessage = "김민선 바보멍청이";
break;
case "오주영바보":
returnMessage = "오주영 바보멍청이";
break;
case "한승희바보":
returnMessage = "한승희 바보멍청이";
break;
case "허준기바보":
returnMessage = "허준기 바보멍청이";
break;
case "전체월간기록":
returnMessage = getAllMonthlyLogs();
break;
case "전체주간기록":
returnMessage = getAllWeeklyLogs();
break;
case "전체일간기록":
returnMessage = getAllDailyLogs();
break;
case "월간기록":
returnMessage = getMonthlyLogs(userName);
break;
case "주간기록":
returnMessage = getWeeklyLogs(userName);
break;
case "일간기록":
returnMessage = getDailyLogs(userName);
break;
case "명령어":
returnMessage = getHelpMessage();
break;
default:
returnMessage = "잘못된 명령어입니다.";
}
return returnMessage;
}
그냥 재미로 만든 명령어들은 하드코딩으로 해줬고, 기록을 가져오는 부분만 함수로 가져오게 해줬다
재미로 만든 명령어가 동작하는걸 보자!
이런식으로 switch
문을 통해서 등록된 명령어가 들어오면 출력이 되게 한다
등록이 되지 않은 명령어가 들어오게 되면?
이런식으로 잘못된 명령어라고 출력이 된다!
이 부분은 만들고 싶은 명령어로 바꿔서 사용하면 될 것 같다
글 마지막에 깃 레포 넣어놓을게용
! 명령어
라는 키워드를 통해서 전체 명령어 모음집도 출력이 되게 해줬다!
좀 더 예쁘게 꾸밀까 했지만 디스코드는 뭔가 형식이 다른것 같아서 이 정도로 만족했다
나름 예쁜듯하다
이제 이 디스코드봇을 만든 이유인 음성채널 시간 기록 코드를 살펴보자!
원래 H2
DB를 쓰려고 하다가 잘 안돼서 MySQL
로 DB를 구성해줬다
사실 원래 DB도 안쓰려고 했는데 그래도 열품타처럼 기록이 되면 좋을 것 같아서 하나 넣었다
거창한 DB는 아니고 그냥 음성채널 입장기록 저장할 간단한 로그 테이블 하나밖에 없긴 하다..ㅎㅎ
@Entity
@Table(name = "voice_channel_logs")
@Getter
@Setter
public class VoiceChannelLog {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private Long userId;
private String nickName;
private Long channelId;
private String channelName;
private Long duration; // 머문 시간(초)
private LocalDateTime recordedAt; // 기록 시간
private String userName;
}
진짜 간단한 사용자, 채널, 머무른 시간 정도 저장하는 테이블!!
@Override
public void onGuildVoiceUpdate(GuildVoiceUpdateEvent event) {
var member = event.getEntity();
var userId = member.getIdLong();
var nickName = member.getNickname();
var joinedChannel = event.getChannelJoined();
var leftChannel = event.getChannelLeft();
User user = member.getUser();
var textChannels = event.getGuild().getTextChannelsByName("공부-기록", true);
TextChannel textChannel = textChannels != null && !textChannels.isEmpty() ? textChannels.get(0) : null;
// 사용자가 새로운 채널에 입장했는지 확인
if (joinedChannel != null && leftChannel == null) {
if (!userJoinTimes.containsKey(userId)) { // 이미 기록된 사용자가 아닌 경우만 처리
userJoinTimes.put(userId, LocalDateTime.now());
System.out.println(nickName + "님이 `" + joinedChannel.getName() + "` 채널에 입장했습니다.");
if (textChannel != null) {
textChannel.sendMessage(
nickName + "님이 `" + joinedChannel.getName() + "` 채널에 입장했습니다."
).queue();
}
}
}
// 사용자가 채널에서 완전히 나갔는지 확인
if (leftChannel != null && joinedChannel == null) {
LocalDateTime joinTime = userJoinTimes.remove(userId);
if (joinTime != null) {
long duration = ChronoUnit.SECONDS.between(joinTime, LocalDateTime.now());
long hours = duration / 3600;
long minutes = (duration % 3600) / 60;
long seconds = duration % 60;
VoiceChannelLog log = new VoiceChannelLog();
log.setUserId(userId);
log.setNickName(nickName);
log.setChannelId(leftChannel.getIdLong());
log.setChannelName(leftChannel.getName());
log.setDuration(duration);
log.setRecordedAt(LocalDateTime.now());
log.setUserName(user.getName());
repository.save(log);
if (textChannel != null) {
textChannel.sendMessage(
nickName + "님이 `" + leftChannel.getName() + "` 채널에서 퇴장했습니다.\n" +
"머문 시간: " +
(hours > 0 ? hours + "시간 " : "") +
(minutes > 0 ? minutes + "분 " : "") +
seconds + "초"
).queue();
}
}
}
}
음성JDA
는 사실 이 코드가 전부다
채널에 입장하면 함수가 실행되고, 채널에서 나가면 머무른 시간만큼 db에 저장하는 코드.
그래도 입장과 퇴장 시에 채널에 메세지를 띄워주긴 한다
event.getGuild().getTextChannelsByName("공부-기록", true);
라는 코드를 통해 공부-기록
이라는 채널에 메세지를 띄워주게 된다
공부-기록
이라는 채널이 없으면 안돼서 꼭 만들어줘야한다!!!
아니면 저 코드를 바꾸는 방법도 있다
근데 이 블로그를 쓰면서 너무 별로인것 같아서 application.yml 파일에 넣는 방식으로 변경했다
voice-channel:
target-channel-name: // 디스코드 음성 채널 이름
text-channel:
target-channel-name: // 디스코드 공부 기록 채널 이름
이런식으로 yml
파일에 넣어두면 저 값을 가져와서 기록해준다!
이 과정에서 원래는 모든 음성채널의 기록을 가져왔는데 설정한 음성채널만 기록하도록 변경도 했다
진짜 멍청한 짓을 하나 해서 시간을 좀 많이 쓴 부분이 있다
jpa:
hibernate:
ddl-auto:
바로 이 부분이다..
처음에 ddl-auto
부분에 create
라고 넣고 로컬에서 테스트를 할 때 실행시킬때마다 DB
가 초기화 되는 문제가 있었다
나는 다른 문제인줄 알고 열심히 찾아보다가 1시간 뒤에 저 부분을 봤는데 update
가 아닌 create
로 되어있는걸 발견할 수 있었다...!
이런 기초적인 실수를 하다니
create
로 해 두면 코드를 실행시킬때마다 db를 새로 만들어서 이전에 실행했을때의 데이터가 다 사라지고 새로 만들기 때문에 발생하는 문제였다
update
로 고치고 해보니 다행히도 데이터가 사라지지 않고 잘 동작하는걸 볼 수 있었당
기록을 했으니 이제 기록한걸 불러오는 기능이 필요하다!
원래 개인기록만 불러오는 기능을 만들려고 했는데 그래도 남들이 한 기록이 보여야 경쟁이 되고 자극을 받을것 같아서 전체 기록을 불러오는 기록도 만들기로 했다!!
공부한 시간으로 경쟁하다니.. 좋은 경쟁이다
기록을 불러오는건 그냥 간단한 SQL
문으로 구현했다
일간
, 주간
, 월간
기록이 있는데 주간
기록의 코드를 살펴보자
특별한건 아니고 그냥 중간에 있어서..
VoiceChannleLogRepository
클래스@Query("SELECT v FROM VoiceChannelLog v WHERE v.recordedAt >= :start AND v.recordedAt < :end AND v.userName = :userName")
List<VoiceChannelLog> findLogsBetween(@Param("start") LocalDateTime start, @Param("end") LocalDateTime end, String userName);
이런식으로 시작 날짜와 끝나는 날짜를 받아서 DB에서 정보를 가져온다
private String getWeeklyLogs(String userName) {
LocalDate startOfWeek = LocalDate.now().with(DayOfWeek.MONDAY);
LocalDate endOfWeek = startOfWeek.plusDays(6);
LocalDateTime start = startOfWeek.atStartOfDay();
LocalDateTime end = endOfWeek.atTime(23, 59, 59);
List<VoiceChannelLog> logs = repository.findLogsBetween(start, end, userName);
return formatLogsSummed(logs, "주간");
}
위의 repository
에서 긁어오는 코드는 이렇다
formatLogsSummed()
메서드에서는 가져온 기록들을 더해서 반환해주는 역할을 한다
private String formatLogsSummed(List<VoiceChannelLog> logs, String periodName) {
if (logs.isEmpty()) {
return periodName + " 기간 동안 기록이 없습니다.";
}
// 사용자별로 총 머문 시간을 계산
Map<String, Long> userDurationMap = new HashMap<>();
logs.forEach(log -> userDurationMap.merge(
log.getNickName(), log.getDuration(), Long::sum
));
// 결과 메시지 작성
StringBuilder response = new StringBuilder(periodName + " 기간 내 기록:\n");
userDurationMap.forEach((username, totalDuration) -> {
long hours = totalDuration / 3600;
long minutes = (totalDuration % 3600) / 60;
long seconds = totalDuration % 60;
response.append(String.format(
"%s님이 총 %d시간 %d분 %d초 동안 머물렀습니다.\n",
username, hours, minutes, seconds
));
});
return response.toString();
}
굉장히 더러운 코드..
돌아가는 쓰레기를 만들기 위해서 대충 만들었다
시간이 생기면 코드 개선을 한번 시도해봐야겠다
기능/코드 설명은 이 정도로 끝!!
다음 글은 만든 디스코드봇을 서버에 배포하는 내용을 작성해봐야겠다!!
배포하는 방법을 아시는 분들은 이거 가져다 쓰시면 됩니다!
만들고 배포했는데 애들이 잘 써서 뿌듯하다 후후
덕분에 공부 열정이 불타올랐어요