Skip to content

Commit

Permalink
Merge pull request #95 from Alchive/feat/#91
Browse files Browse the repository at this point in the history
feat: 디스코드, 슬랙 봇 추가
  • Loading branch information
nahowo authored Dec 3, 2024
2 parents 6e52a4a + ebae369 commit fc2512f
Show file tree
Hide file tree
Showing 22 changed files with 503 additions and 141 deletions.
10 changes: 7 additions & 3 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -52,9 +52,13 @@ dependencies {
implementation 'io.micrometer:micrometer-registry-prometheus'

// Slack API
implementation("com.slack.api:bolt:1.18.0")
implementation("com.slack.api:bolt-servlet:1.18.0")
implementation("com.slack.api:bolt-jetty:1.18.0")
implementation 'com.slack.api:bolt:1.18.0'
implementation 'com.slack.api:bolt-servlet:1.18.0'
implementation 'com.slack.api:bolt-jetty:1.18.0'
implementation 'com.slack.api:slack-api-client:1.44.1'

// Discord API
implementation 'net.dv8tion:JDA:5.0.0-beta.5'
}

tasks.named('test') {
Expand Down
21 changes: 21 additions & 0 deletions src/main/java/com/Alchive/backend/config/JDAConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.Alchive.backend.config;

import net.dv8tion.jda.api.JDA;
import net.dv8tion.jda.api.JDABuilder;
import net.dv8tion.jda.api.exceptions.RateLimitedException;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import javax.security.auth.login.LoginException;

@Configuration
public class JDAConfig {
@Value("${DISCORD_BOT_TOKEN}")
private String discordBotToken;
@Bean
public JDA jda() throws LoginException, RateLimitedException {
return JDABuilder.createDefault(discordBotToken)
.build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ public OpenAPI openAPI() {
private Info apiInfo() {
return new Info()
.title("Alchive API")
.description("[ Base URL: http://localhost:8080/api/v1]\n\nAlchive의 API 문서")
.description("[ Base URL: http://localhost:8080/api/v2]\n\nAlchive의 API 문서")
.version("1.0.0");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,11 @@ public enum ErrorCode {
PROBLEM_NOT_FOUND(HttpStatus.NOT_FOUND.value(), "P001", "문제가 존재하지 않음"),

// SNS
SNS_NOT_FOUND(HttpStatus.NOT_FOUND.value(), "N001", "소셜이 존재하지 않음");
SNS_NOT_FOUND(HttpStatus.NOT_FOUND.value(), "N001", "소셜이 존재하지 않음"),

// DISCORD
INVALID_GRANT(HttpStatus.FORBIDDEN.value(), "D001", "승인되지 않은 코드"),
DISCORD_USER_NOT_FOUND(HttpStatus.NOT_FOUND.value(), "D002", "존재하지 않는 디스코드 유저");

private final int httpStatus;
private final String code;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.Alchive.backend.config.error.exception.sns;

import com.Alchive.backend.config.error.ErrorCode;
import com.Alchive.backend.config.error.exception.BusinessException;

public class InvalidGrantException extends BusinessException {
public InvalidGrantException() { super(ErrorCode.INVALID_GRANT); }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.Alchive.backend.config.error.exception.sns;

import com.Alchive.backend.config.error.ErrorCode;
import com.Alchive.backend.config.error.exception.BusinessException;

public class NoSuchDiscordUserException extends BusinessException {
public NoSuchDiscordUserException() { super(ErrorCode.DISCORD_USER_NOT_FOUND); }
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,13 @@ public enum ResultCode {

// SNS
SNS_INFO_SUCCESS("N001", "소셜 조회 성공"),
SNS_CREATE_SUCCESS("N002", "소셜 생성 성공");
SNS_CREATE_SUCCESS("N002", "소셜 생성 성공"),

// DISCORD
DISCORD_DM_SEND_SUCCESS("D001", "디스코드 DM 전송 성공"),

// SLACK
SLACK_DM_SEND_SUCCESS("L001", "슬랙 DM 전송 성공");
private final String code;
private final String message;
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
import com.Alchive.backend.dto.response.BoardDetailResponseDTO;
import com.Alchive.backend.dto.response.BoardResponseDTO;
import com.Alchive.backend.service.BoardService;
import com.Alchive.backend.slack.SlackService;
import com.Alchive.backend.service.SlackService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package com.Alchive.backend.controller;

import com.Alchive.backend.config.result.ResultResponse;
import com.Alchive.backend.domain.sns.Sns;
import com.Alchive.backend.domain.sns.SnsCategory;
import com.Alchive.backend.domain.user.User;
import com.Alchive.backend.dto.request.SnsCreateRequest;
import com.Alchive.backend.service.SnsService;
import com.Alchive.backend.service.DiscordService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

import org.springframework.http.*;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.*;

import static com.Alchive.backend.config.result.ResultCode.DISCORD_DM_SEND_SUCCESS;

@Tag(name = "디스코드", description = "디스코드 관련 api입니다. ")
@RequiredArgsConstructor
@RestController
@Slf4j
@RequestMapping("/api/v2/discord")
public class DiscordController {
private final SnsService snsService;
private final DiscordService discordService;

@Operation(summary = "디스코드 봇 연결", description = "디스코드 액세스 토큰을 요청하고 DM 채널을 연결하는 api입니다. ")
@GetMapping("/dm/open")
public ResponseEntity<ResultResponse> openDiscordDm(@AuthenticationPrincipal User user, @RequestParam String code) {
// Access Token 요청
String accessToken = discordService.getAccessToken(code);
log.info("Access Token 반환 완료: " + accessToken);

// 사용자 DISCORD USER ID 요청
String discordUserId = discordService.getDiscordUserIdFromAccessToken(accessToken);
log.info("Discord User Id 반환 완료: " + discordUserId);

// DM 채널 생성 요청
String channelId = discordService.getDmChannel(discordUserId);
log.info("DM 채널 생성 완료");

// Discord SNS 정보 저장
SnsCreateRequest snsCreateRequest = SnsCreateRequest.builder()
.category(SnsCategory.DISCORD)
.sns_id(discordUserId) // Discord User Id
.channel_id(channelId) // Discord Channel Id
.time("0 0 18 ? * MON")
.build();
snsService.createSns(user, snsCreateRequest);
log.info("SNS 정보 저장 완료");

// DM 전송 요청
discordService.sendDm(channelId, "안녕하세요! 이제부터 풀지 못한 문제들을 정해진 시간에 알려드릴게요. ");

return ResponseEntity.ok(ResultResponse.of(DISCORD_DM_SEND_SUCCESS));
}

@Operation(summary = "디스코드 DM 전송", description = "디스코드 DM으로 메시지를 전송하는 api입니다. ")
@PostMapping("dm/send")
public ResponseEntity<ResultResponse> sendDiscordDm(@AuthenticationPrincipal User user, @RequestParam String message) {
Sns discordInfo = discordService.getDiscordInfo(user);
discordService.sendDmJda(discordInfo.getSns_id(), message);
return ResponseEntity.ok(ResultResponse.of(DISCORD_DM_SEND_SUCCESS));
}
}
55 changes: 55 additions & 0 deletions src/main/java/com/Alchive/backend/controller/SlackController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package com.Alchive.backend.controller;

import com.Alchive.backend.config.result.ResultResponse;
import com.Alchive.backend.domain.sns.Sns;
import com.Alchive.backend.domain.user.User;
import com.Alchive.backend.dto.request.SnsCreateRequest;
import com.Alchive.backend.service.SnsService;
import com.Alchive.backend.service.SlackService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.*;

import static com.Alchive.backend.config.result.ResultCode.SLACK_DM_SEND_SUCCESS;

@Slf4j
@Tag(name = "슬랙", description = "슬랙 관련 API입니다. ")
@RestController
@RequestMapping("/api/v2/slack")
@RequiredArgsConstructor
public class SlackController {
private final SlackService slackService;
private final SnsService snsService;

@Operation(summary = "슬랙 봇 연결", description = "슬랙 액세스 토큰을 요청하고 DM 채널을 연결하는 api입니다. ")
@GetMapping("/dm/open")
public ResponseEntity<ResultResponse> openSlackDm(@AuthenticationPrincipal User user, @RequestParam String code) {
// Bot Access Token, User Access Token, Slack User Id 요청
SnsCreateRequest snsCreateRequest = slackService.getSlackInfo(code);
log.info("사용자 slack 정보를 불러왔습니다. ");

// Slack SNS 정보 저장
snsService.createSns(user, snsCreateRequest);
log.info("사용자 slack 정보를 저장했습니다. ");

// DM 전송 요청
String slackUserId = snsCreateRequest.getSns_id();
String botAccessToken = snsCreateRequest.getBot_token();

slackService.sendDm(slackUserId, botAccessToken, "안녕하세요! 이제부터 풀지 못한 문제들을 정해진 시간에 알려드릴게요. ");
log.info("봇을 사용자 slack 채널에 추가했습니다. ");
return ResponseEntity.ok(ResultResponse.of(SLACK_DM_SEND_SUCCESS));
}

@Operation(summary = "슬랙 DM 전송", description = "슬랙 DM으로 메시지를 전송하는 api입니다. ")
@PostMapping("dm/send")
public ResponseEntity<ResultResponse> sendSlackDm(@AuthenticationPrincipal User user, @RequestParam String message) {
Sns sns = slackService.getSlackInfo(user);
slackService.sendDm(sns.getSns_id(), sns.getBot_token(), message);
return ResponseEntity.ok(ResultResponse.of(SLACK_DM_SEND_SUCCESS));
}
}
20 changes: 14 additions & 6 deletions src/main/java/com/Alchive/backend/domain/sns/Sns.java
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,17 @@ public class Sns extends BaseEntity {
@Column(name = "category", length = 20, nullable = false)
private SnsCategory category;

@Column(name = "token", columnDefinition = "TEXT", nullable = false)
private String token;
@Column(name = "bot_token", columnDefinition = "TEXT")
private String bot_token;

@Column(name = "channel", columnDefinition = "TEXT")
private String channel;
@Column(name = "user_token", columnDefinition = "TEXT")
private String user_token;

@Column(name = "channel_id", columnDefinition = "TEXT")
private String channel_id;

@Column(name = "sns_id", columnDefinition = "TEXT")
private String sns_id;

@Column(name = "time", length = 30)
private String time;
Expand All @@ -43,8 +49,10 @@ public static Sns of(User user, SnsCreateRequest snsCreateRequest) {
return Sns.builder()
.user(user)
.category(snsCreateRequest.getCategory())
.token(snsCreateRequest.getToken())
.channel(snsCreateRequest.getChannel())
.bot_token(snsCreateRequest.getBot_token())
.user_token(snsCreateRequest.getUser_token())
.channel_id(snsCreateRequest.getChannel_id())
.sns_id(snsCreateRequest.getSns_id())
.time(snsCreateRequest.getTime())
.build();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,9 @@
public class SnsCreateRequest {
@NotNull(message = "소셜 카테고리는 필수입니다. ")
private SnsCategory category;
@NotNull(message = "토큰은 필수입니다. ")
private String token;
private String channel;
private String bot_token;
private String user_token;
private String channel_id;
private String sns_id;
private String time;
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,17 +15,21 @@ public class SnsResponseDTO {
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
private SnsCategory category;
private String token;
private String channel;
private String bot_token;
private String user_token;
private String channel_id;
private String sns_id;
private String time;

public SnsResponseDTO(Sns sns) {
this.id = sns.getId();
this.createdAt = sns.getCreatedAt();
this.updatedAt = sns.getUpdatedAt();
this.category = sns.getCategory();
this.token = sns.getToken();
this.channel = sns.getChannel();
this.bot_token = sns.getBot_token();
this.user_token = sns.getUser_token();
this.channel_id = sns.getChannel_id();
this.sns_id = sns.getSns_id();
this.time = sns.getTime();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,6 @@ public interface BoardRepository extends JpaRepository<Board, Long> {
Optional<Board> findByProblem_PlatformAndProblem_NumberAndUser_Id(ProblemPlatform platform, int problemNumber, Long userId);


@Query(value = "SELECT * FROM Board WHERE created_at <= :threeDaysAgo AND status != 'CORRECT' ORDER BY RAND() LIMIT 1", nativeQuery = true)
Board findUnsolvedBoardAddedBefore(@Param("threeDaysAgo") LocalDateTime threeDaysAgo);
@Query(value = "SELECT * FROM Board WHERE createdAt <= :threeDaysAgo AND status != 'CORRECT' AND userId = :user_id ORDER BY RAND() LIMIT 1", nativeQuery = true)
Board findUnsolvedBoardAddedBefore(@Param("threeDaysAgo") LocalDateTime threeDaysAgo, @Param("user_id") Long userId);
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.Alchive.backend.repository;

import com.Alchive.backend.domain.sns.Sns;
import com.Alchive.backend.domain.sns.SnsCategory;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

Expand All @@ -9,4 +10,7 @@
@Repository
public interface SnsReporitory extends JpaRepository<Sns, Long> {
Optional<Sns> findById(Long id);
Optional<Sns> findByUser_IdAndCategory(Long userId, SnsCategory category);

Boolean existsByUser_IdAndCategory(Long userId, SnsCategory category);
}
Loading

0 comments on commit fc2512f

Please sign in to comment.