From 98b8849082031eaa755eb4ce4e6b374bb2798e2c Mon Sep 17 00:00:00 2001 From: nahowo Date: Sat, 9 Nov 2024 13:37:58 +0900 Subject: [PATCH 1/6] =?UTF-8?q?refactor:=20Sns=20=EB=B0=98=ED=99=98=20?= =?UTF-8?q?=EB=8D=B0=EC=9D=B4=ED=84=B0=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/Alchive/backend/controller/SnsController.java | 5 +++-- src/main/java/com/Alchive/backend/service/SnsService.java | 3 ++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/Alchive/backend/controller/SnsController.java b/src/main/java/com/Alchive/backend/controller/SnsController.java index bb90a20..06f2ea7 100644 --- a/src/main/java/com/Alchive/backend/controller/SnsController.java +++ b/src/main/java/com/Alchive/backend/controller/SnsController.java @@ -1,6 +1,7 @@ package com.Alchive.backend.controller; import com.Alchive.backend.config.result.ResultResponse; +import com.Alchive.backend.domain.sns.Sns; import com.Alchive.backend.dto.request.SnsCreateRequest; import com.Alchive.backend.dto.response.SnsResponseDTO; import com.Alchive.backend.service.SnsService; @@ -30,7 +31,7 @@ public ResponseEntity getSns(@PathVariable Long snsId) { @Operation(summary = "소셜 정보 생성", description = "소셜 정보를 생성하는 메서드입니다. ") @PostMapping("") public ResponseEntity createSns(HttpServletRequest tokenRequest, SnsCreateRequest request) { - snsService.createSns(tokenRequest, request); - return ResponseEntity.ok(ResultResponse.of(SNS_CREATE_SUCCESS)); + SnsResponseDTO sns = snsService.createSns(tokenRequest, request); + return ResponseEntity.ok(ResultResponse.of(SNS_CREATE_SUCCESS, sns)); } } diff --git a/src/main/java/com/Alchive/backend/service/SnsService.java b/src/main/java/com/Alchive/backend/service/SnsService.java index a5ed3fc..0940f81 100644 --- a/src/main/java/com/Alchive/backend/service/SnsService.java +++ b/src/main/java/com/Alchive/backend/service/SnsService.java @@ -27,11 +27,12 @@ public SnsResponseDTO getSns(Long snsId) { } @Transactional - public void createSns(HttpServletRequest tokenRequest, SnsCreateRequest request) { + public SnsResponseDTO createSns(HttpServletRequest tokenRequest, SnsCreateRequest request) { Long userId = tokenService.validateAccessToken(tokenRequest); User user = userRepository.findById(userId) .orElseThrow(NoSuchUserIdException::new); Sns sns = Sns.of(user, request); snsReporitory.save(sns); + return new SnsResponseDTO(sns); } } From 7364f39b524321a8ce87fb7c8a23ed8b5b9066b7 Mon Sep 17 00:00:00 2001 From: nahowo Date: Sun, 10 Nov 2024 20:38:25 +0900 Subject: [PATCH 2/6] =?UTF-8?q?feat:=20=EB=94=94=EC=8A=A4=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=EB=B4=87=20=EC=97=B0=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 3 + .../backend/config/error/ErrorCode.java | 5 +- .../exception/sns/InvalidGrantException.java | 8 + .../backend/config/result/ResultCode.java | 5 +- .../backend/controller/BoardController.java | 2 +- .../backend/controller/SnsController.java | 4 +- .../backend/repository/SnsReporitory.java | 4 + .../Alchive/backend/service/SnsService.java | 11 +- .../backend/sns/DiscordController.java | 85 ++++++++++ .../Alchive/backend/sns/DiscordService.java | 153 ++++++++++++++++++ .../{slack => sns}/SlackController.java | 2 +- .../backend/{slack => sns}/SlackService.java | 4 +- src/main/resources/application.yml | 6 +- 13 files changed, 281 insertions(+), 11 deletions(-) create mode 100644 src/main/java/com/Alchive/backend/config/error/exception/sns/InvalidGrantException.java create mode 100644 src/main/java/com/Alchive/backend/sns/DiscordController.java create mode 100644 src/main/java/com/Alchive/backend/sns/DiscordService.java rename src/main/java/com/Alchive/backend/{slack => sns}/SlackController.java (97%) rename src/main/java/com/Alchive/backend/{slack => sns}/SlackService.java (97%) diff --git a/build.gradle b/build.gradle index 0ead835..6ada8b4 100644 --- a/build.gradle +++ b/build.gradle @@ -55,6 +55,9 @@ dependencies { 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") + + // Discord API + implementation("net.dv8tion:JDA:5.0.0-beta.5") } tasks.named('test') { diff --git a/src/main/java/com/Alchive/backend/config/error/ErrorCode.java b/src/main/java/com/Alchive/backend/config/error/ErrorCode.java index 246a29b..3a7e41d 100644 --- a/src/main/java/com/Alchive/backend/config/error/ErrorCode.java +++ b/src/main/java/com/Alchive/backend/config/error/ErrorCode.java @@ -33,7 +33,10 @@ 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", "승인되지 않은 코드"); private final int httpStatus; private final String code; diff --git a/src/main/java/com/Alchive/backend/config/error/exception/sns/InvalidGrantException.java b/src/main/java/com/Alchive/backend/config/error/exception/sns/InvalidGrantException.java new file mode 100644 index 0000000..65c4676 --- /dev/null +++ b/src/main/java/com/Alchive/backend/config/error/exception/sns/InvalidGrantException.java @@ -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); } +} diff --git a/src/main/java/com/Alchive/backend/config/result/ResultCode.java b/src/main/java/com/Alchive/backend/config/result/ResultCode.java index f83581b..eafc133 100644 --- a/src/main/java/com/Alchive/backend/config/result/ResultCode.java +++ b/src/main/java/com/Alchive/backend/config/result/ResultCode.java @@ -33,7 +33,10 @@ public enum ResultCode { // SNS SNS_INFO_SUCCESS("N001", "소셜 조회 성공"), - SNS_CREATE_SUCCESS("N002", "소셜 생성 성공"); + SNS_CREATE_SUCCESS("N002", "소셜 생성 성공"), + + // DISCORD + DISCORD_DM_SEND_SUCCESS("D001", "디스코드 DM 전송 성공"); private final String code; private final String message; } diff --git a/src/main/java/com/Alchive/backend/controller/BoardController.java b/src/main/java/com/Alchive/backend/controller/BoardController.java index 2ef8568..040da40 100644 --- a/src/main/java/com/Alchive/backend/controller/BoardController.java +++ b/src/main/java/com/Alchive/backend/controller/BoardController.java @@ -7,7 +7,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.sns.SlackService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.servlet.http.HttpServletRequest; diff --git a/src/main/java/com/Alchive/backend/controller/SnsController.java b/src/main/java/com/Alchive/backend/controller/SnsController.java index 06f2ea7..99965ce 100644 --- a/src/main/java/com/Alchive/backend/controller/SnsController.java +++ b/src/main/java/com/Alchive/backend/controller/SnsController.java @@ -31,7 +31,7 @@ public ResponseEntity getSns(@PathVariable Long snsId) { @Operation(summary = "소셜 정보 생성", description = "소셜 정보를 생성하는 메서드입니다. ") @PostMapping("") public ResponseEntity createSns(HttpServletRequest tokenRequest, SnsCreateRequest request) { - SnsResponseDTO sns = snsService.createSns(tokenRequest, request); - return ResponseEntity.ok(ResultResponse.of(SNS_CREATE_SUCCESS, sns)); + snsService.createSns(tokenRequest, request); + return ResponseEntity.ok(ResultResponse.of(SNS_CREATE_SUCCESS)); } } diff --git a/src/main/java/com/Alchive/backend/repository/SnsReporitory.java b/src/main/java/com/Alchive/backend/repository/SnsReporitory.java index 31b8207..e29bdd8 100644 --- a/src/main/java/com/Alchive/backend/repository/SnsReporitory.java +++ b/src/main/java/com/Alchive/backend/repository/SnsReporitory.java @@ -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; @@ -9,4 +10,7 @@ @Repository public interface SnsReporitory extends JpaRepository { Optional findById(Long id); + Optional findByUser_IdAndCategory(Long userId, SnsCategory category); + + Boolean existsByUser_IdAndCategory(Long userId, SnsCategory category); } diff --git a/src/main/java/com/Alchive/backend/service/SnsService.java b/src/main/java/com/Alchive/backend/service/SnsService.java index 0940f81..24f80ac 100644 --- a/src/main/java/com/Alchive/backend/service/SnsService.java +++ b/src/main/java/com/Alchive/backend/service/SnsService.java @@ -11,11 +11,13 @@ import jakarta.servlet.http.HttpServletRequest; import jakarta.transaction.Transactional; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import com.Alchive.backend.config.error.exception.sns.NoSuchSnsIdException; @RequiredArgsConstructor @Service +@Slf4j public class SnsService { private final TokenService tokenService; private final SnsReporitory snsReporitory; @@ -27,12 +29,17 @@ public SnsResponseDTO getSns(Long snsId) { } @Transactional - public SnsResponseDTO createSns(HttpServletRequest tokenRequest, SnsCreateRequest request) { + public void createSns(HttpServletRequest tokenRequest, SnsCreateRequest request) { Long userId = tokenService.validateAccessToken(tokenRequest); User user = userRepository.findById(userId) .orElseThrow(NoSuchUserIdException::new); + + if (snsReporitory.existsByUser_IdAndCategory(userId, request.getCategory())) { + log.info("해당 카테고리의 소셜 정보가 이미 존재합니다. "); + return; + } + Sns sns = Sns.of(user, request); snsReporitory.save(sns); - return new SnsResponseDTO(sns); } } diff --git a/src/main/java/com/Alchive/backend/sns/DiscordController.java b/src/main/java/com/Alchive/backend/sns/DiscordController.java new file mode 100644 index 0000000..85b2ac7 --- /dev/null +++ b/src/main/java/com/Alchive/backend/sns/DiscordController.java @@ -0,0 +1,85 @@ +package com.Alchive.backend.sns; + +import com.Alchive.backend.config.error.exception.sns.InvalidGrantException; +import com.Alchive.backend.config.result.ResultResponse; +import com.Alchive.backend.domain.sns.SnsCategory; +import com.Alchive.backend.dto.request.SnsCreateRequest; +import com.Alchive.backend.service.SnsService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.servlet.http.HttpServletRequest; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +import java.util.HashMap; +import java.util.Map; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.*; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.client.RestTemplate; + +import static com.Alchive.backend.config.result.ResultCode.DISCORD_DM_SEND_SUCCESS; + +@Tag(name = "디스코드", description = "디스코드 관련 api입니다. ") +@RequiredArgsConstructor +@RestController +@Slf4j +@RequestMapping("/api/v1/discord") +public class DiscordController { + @Value("${DISCORD_CLIENT_ID}") + private String clientId; + + @Value("${DISCORD_CLIENT_SECRET}") + private String clientSecret; + + @Value("${DISCORD_REDIRECT_URI}") + private String redirectUri; + + @Value("${DISCORD_BOT_TOKEN}") + private String discordBotToken; + + private final SnsService snsService; + private final DiscordService discordService; + + @Operation(summary = "디스코드 봇 연결", description = "디스코드 액세스 토큰을 요청하고 DM 채널을 연결하는 api입니다. ") + @GetMapping("/dm/open") + public ResponseEntity installDiscordBot(HttpServletRequest tokenRequest, @RequestParam String code) { + RestTemplate restTemplate = new RestTemplate(); + + // 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) + .token(discordUserId) // Discord User Id + .channel(channelId) // Discord Channel Id + .time("0 0 18 ? * MON") + .build(); + snsService.createSns(tokenRequest, 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 sendDiscordDm(HttpServletRequest tokenRequest, @RequestParam String message) { + String discordUserId = discordService.getDiscordUserId(tokenRequest); + String channelId = discordService.getDmChannel(discordUserId); + discordService.sendDm(channelId, message); + return ResponseEntity.ok(ResultResponse.of(DISCORD_DM_SEND_SUCCESS)); + } +} diff --git a/src/main/java/com/Alchive/backend/sns/DiscordService.java b/src/main/java/com/Alchive/backend/sns/DiscordService.java new file mode 100644 index 0000000..8353559 --- /dev/null +++ b/src/main/java/com/Alchive/backend/sns/DiscordService.java @@ -0,0 +1,153 @@ +package com.Alchive.backend.sns; + +import com.Alchive.backend.config.error.exception.sns.InvalidGrantException; +import com.Alchive.backend.config.error.exception.sns.NoSuchSnsIdException; +import com.Alchive.backend.config.jwt.TokenService; +import com.Alchive.backend.domain.board.Board; +import com.Alchive.backend.domain.sns.Sns; +import com.Alchive.backend.domain.sns.SnsCategory; +import com.Alchive.backend.repository.BoardRepository; +import com.Alchive.backend.repository.SnsReporitory; +import com.slack.api.Slack; +import com.slack.api.methods.MethodsClient; +import com.slack.api.methods.request.chat.ChatPostMessageRequest; +import jakarta.annotation.PostConstruct; +import jakarta.servlet.http.HttpServletRequest; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import net.dv8tion.jda.api.JDA; +import net.dv8tion.jda.api.JDABuilder; +import net.dv8tion.jda.api.entities.User; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.*; +import org.springframework.scheduling.annotation.EnableScheduling; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Service; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.client.RestTemplate; + +import java.time.LocalDateTime; +import java.time.temporal.ChronoUnit; +import java.util.HashMap; +import java.util.Map; + +@Service +@RequiredArgsConstructor +@EnableScheduling +@Slf4j +public class DiscordService { + private JDA jda; + private final BoardRepository boardRepository; + private final SnsReporitory snsReporitory; + private final TokenService tokenService; + + @Value("${DISCORD_CLIENT_ID}") + private String clientId; + + @Value("${DISCORD_CLIENT_SECRET}") + private String clientSecret; + + @Value("${DISCORD_REDIRECT_URI}") + private String redirectUri; + + @Value("${DISCORD_BOT_TOKEN}") + private String discordBotToken; + private RestTemplate restTemplate = new RestTemplate(); + + public String getAccessToken(String code) { + String getTokenUrl = "https://discord.com/api/oauth2/token"; + + HttpHeaders getTokenHeaders = new HttpHeaders(); + getTokenHeaders.setContentType(MediaType.APPLICATION_FORM_URLENCODED); + + MultiValueMap getTokenParams = new LinkedMultiValueMap<>(); + getTokenParams.add("client_id", clientId); + getTokenParams.add("client_secret", clientSecret); + getTokenParams.add("code", code); + getTokenParams.add("grant_type", "authorization_code"); + getTokenParams.add("redirect_uri", redirectUri); + + HttpEntity> request = new HttpEntity<>(getTokenParams, getTokenHeaders); + ResponseEntity response = restTemplate.postForEntity(getTokenUrl, request, Map.class); + Map responseBody = response.getBody(); + + if (responseBody.containsKey("error") && responseBody.get("error") == "invalid_grant" ) { + throw new InvalidGrantException(); + } + String accessToken = (String) responseBody.get("access_token"); + return accessToken; + } + + public String getDiscordUserIdFromAccessToken(String accessToken) { + String getUserInfoUrl = "https://discord.com/api/v10/users/@me"; + HttpHeaders accessTokenHeaders = new HttpHeaders(); + accessTokenHeaders.setBearerAuth(accessToken); + + HttpEntity authRequest = new HttpEntity<>(accessTokenHeaders); + ResponseEntity userInfoResponse = restTemplate.exchange(getUserInfoUrl, HttpMethod.GET, authRequest, Map.class); + Map userInfo = userInfoResponse.getBody(); + + String discordUserId = (String) userInfo.get("id"); + return discordUserId; + } + + public String getDmChannel(String discordUserId) { + String getDMChannelUrl = "https://discord.com/api/v10/users/@me/channels"; + Map getDmParams = new HashMap<>(); + getDmParams.put("recipient_id", discordUserId); + + HttpHeaders botTokenHeader = new HttpHeaders(); + botTokenHeader.setContentType(MediaType.APPLICATION_JSON); + botTokenHeader.set("Authorization", "Bot " + discordBotToken); + + HttpEntity> createDmRequest = new HttpEntity<>(getDmParams, botTokenHeader); + ResponseEntity dmResponse = restTemplate.postForEntity(getDMChannelUrl, createDmRequest, Map.class); + Map dmResponseBody = dmResponse.getBody(); + + String channelId = (String) dmResponseBody.get("id"); + return channelId; + } + + public String getDiscordUserId(HttpServletRequest tokenRequest) { + Long userId = tokenService.validateAccessToken(tokenRequest); + Sns snsInfo = snsReporitory.findByUser_IdAndCategory(userId, SnsCategory.DISCORD) + .orElseThrow(NoSuchSnsIdException::new); + String discordUserId = snsInfo.getToken(); + return discordUserId; + } + + public void sendDm(String channelId, String message) { + String sendMessageUrl = "https://discord.com/api/v10/channels/" + channelId + "/messages"; + HttpHeaders sendDmHeaders = new HttpHeaders(); + sendDmHeaders.set("Authorization", "Bot " + discordBotToken); + sendDmHeaders.setContentType(MediaType.APPLICATION_JSON); + + Map sendDmParams = new HashMap<>(); + sendDmParams.put("content", message); + + HttpEntity> sendMessageRequest = new HttpEntity<>(sendDmParams, sendDmHeaders); + restTemplate.postForEntity(sendMessageUrl, sendMessageRequest, Map.class); + } + +// @Scheduled(cron = "0 */1 * * * *") + public void sendMessageReminderBoard(HttpServletRequest tokenRequest) { + LocalDateTime threeDaysAgo = LocalDateTime.now().minusDays(1); + + Board unSolvedBoard = boardRepository.findUnsolvedBoardAddedBefore(threeDaysAgo); + + if (unSolvedBoard != null) { + String message = String.format(":star-struck: %d일 전 도전했던 %d. %s 문제를 아직 풀지 못했어요. \n \n다시 도전해보세요! :facepunch: \n \n<%s|:link: 문제 풀러가기>", + ChronoUnit.DAYS.between(unSolvedBoard.getCreatedAt(), LocalDateTime.now()), + unSolvedBoard.getProblem().getNumber(), + unSolvedBoard.getProblem().getTitle(), + unSolvedBoard.getProblem().getUrl()); + String discordUserId = getDiscordUserId(tokenRequest); + String channelId = getDmChannel(discordUserId); + sendDm(channelId,message); + } else { + log.info("풀지 못한 문제가 존재하지 않습니다. "); + } + } +} diff --git a/src/main/java/com/Alchive/backend/slack/SlackController.java b/src/main/java/com/Alchive/backend/sns/SlackController.java similarity index 97% rename from src/main/java/com/Alchive/backend/slack/SlackController.java rename to src/main/java/com/Alchive/backend/sns/SlackController.java index a40fb25..739895c 100644 --- a/src/main/java/com/Alchive/backend/slack/SlackController.java +++ b/src/main/java/com/Alchive/backend/sns/SlackController.java @@ -1,4 +1,4 @@ -package com.Alchive.backend.slack; +package com.Alchive.backend.sns; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; diff --git a/src/main/java/com/Alchive/backend/slack/SlackService.java b/src/main/java/com/Alchive/backend/sns/SlackService.java similarity index 97% rename from src/main/java/com/Alchive/backend/slack/SlackService.java rename to src/main/java/com/Alchive/backend/sns/SlackService.java index 0594e21..f2a0c2c 100644 --- a/src/main/java/com/Alchive/backend/slack/SlackService.java +++ b/src/main/java/com/Alchive/backend/sns/SlackService.java @@ -1,4 +1,4 @@ -package com.Alchive.backend.slack; +package com.Alchive.backend.sns; import com.Alchive.backend.domain.board.Board; import com.Alchive.backend.domain.board.BoardStatus; @@ -67,7 +67,7 @@ public void sendMessageCreateBoard(BoardCreateRequest boardCreateRequest, BoardR sendMessage(message); } - @Scheduled(cron = "0 0 * * * *") // 정각마다 알림 +// @Scheduled(cron = "0 0 * * * *") // 정각마다 알림 public void sendMessageReminderBoard() { LocalDateTime threeDaysAgo = LocalDateTime.now().minusDays(1); diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 5f36286..eb1afe2 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -38,4 +38,8 @@ management: endpoints: web: exposure: - include: "prometheus" \ No newline at end of file + include: "prometheus" + +discord: + bot: + token: ${DISCORD_BOT_TOKEN} \ No newline at end of file From d64c6e0f4ccb6557f0c347ca200462c17570c2ed Mon Sep 17 00:00:00 2001 From: nahowo Date: Sun, 10 Nov 2024 21:30:54 +0900 Subject: [PATCH 3/6] =?UTF-8?q?feat:=20repository=20query=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/Alchive/backend/controller/BoardController.java | 2 +- .../com/Alchive/backend/repository/BoardRepository.java | 4 ++-- src/main/java/com/Alchive/backend/sns/DiscordService.java | 5 +++-- .../java/com/Alchive/backend/sns/SlackController.java | 5 +++-- src/main/java/com/Alchive/backend/sns/SlackService.java | 8 ++++++-- 5 files changed, 15 insertions(+), 9 deletions(-) diff --git a/src/main/java/com/Alchive/backend/controller/BoardController.java b/src/main/java/com/Alchive/backend/controller/BoardController.java index 040da40..3e1b853 100644 --- a/src/main/java/com/Alchive/backend/controller/BoardController.java +++ b/src/main/java/com/Alchive/backend/controller/BoardController.java @@ -56,7 +56,7 @@ public ResponseEntity getBoardList(@RequestParam(value = "offset @PostMapping("") public ResponseEntity createBoard(HttpServletRequest tokenRequest, @RequestBody @Valid BoardCreateRequest boardCreateRequest) { BoardResponseDTO board = boardService.createBoard(tokenRequest, boardCreateRequest); - slackService.sendMessageCreateBoard(boardCreateRequest, board); +// slackService.sendMessageCreateBoard(boardCreateRequest, board); return ResponseEntity.ok(ResultResponse.of(BOARD_CREATE_SUCCESS, board)); } diff --git a/src/main/java/com/Alchive/backend/repository/BoardRepository.java b/src/main/java/com/Alchive/backend/repository/BoardRepository.java index 16b18cf..43f1356 100644 --- a/src/main/java/com/Alchive/backend/repository/BoardRepository.java +++ b/src/main/java/com/Alchive/backend/repository/BoardRepository.java @@ -15,6 +15,6 @@ public interface BoardRepository extends JpaRepository { Optional findByProblem_PlatformAndProblem_NumberAndUser_Id(ProblemPlatform platform, int problemNumber, Long userId); - @Query(value = "SELECT * FROM Board WHERE createdAt <= :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); } diff --git a/src/main/java/com/Alchive/backend/sns/DiscordService.java b/src/main/java/com/Alchive/backend/sns/DiscordService.java index 8353559..28d57a2 100644 --- a/src/main/java/com/Alchive/backend/sns/DiscordService.java +++ b/src/main/java/com/Alchive/backend/sns/DiscordService.java @@ -131,11 +131,12 @@ public void sendDm(String channelId, String message) { restTemplate.postForEntity(sendMessageUrl, sendMessageRequest, Map.class); } -// @Scheduled(cron = "0 */1 * * * *") +// @Scheduled(cron = "0 */1 * * * *") // todo: Quartz로 동적 스케줄링 작성하기 public void sendMessageReminderBoard(HttpServletRequest tokenRequest) { LocalDateTime threeDaysAgo = LocalDateTime.now().minusDays(1); + Long userId = tokenService.validateAccessToken(tokenRequest); - Board unSolvedBoard = boardRepository.findUnsolvedBoardAddedBefore(threeDaysAgo); + Board unSolvedBoard = boardRepository.findUnsolvedBoardAddedBefore(threeDaysAgo, userId); if (unSolvedBoard != null) { String message = String.format(":star-struck: %d일 전 도전했던 %d. %s 문제를 아직 풀지 못했어요. \n \n다시 도전해보세요! :facepunch: \n \n<%s|:link: 문제 풀러가기>", diff --git a/src/main/java/com/Alchive/backend/sns/SlackController.java b/src/main/java/com/Alchive/backend/sns/SlackController.java index 739895c..772d4b3 100644 --- a/src/main/java/com/Alchive/backend/sns/SlackController.java +++ b/src/main/java/com/Alchive/backend/sns/SlackController.java @@ -2,6 +2,7 @@ import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.servlet.http.HttpServletRequest; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; @@ -27,7 +28,7 @@ public void addedSlackBot() { @Operation(summary = "문제 리마인더", description = "30분마다 해결하지 못한 문제를 리마인드해주는 메서드입니다. ") @GetMapping("/reminder") - public void sendReminder() { - slackService.sendMessageReminderBoard(); + public void sendReminder(HttpServletRequest tokenRequest) { + slackService.sendMessageReminderBoard(tokenRequest); } } \ No newline at end of file diff --git a/src/main/java/com/Alchive/backend/sns/SlackService.java b/src/main/java/com/Alchive/backend/sns/SlackService.java index f2a0c2c..c3475f7 100644 --- a/src/main/java/com/Alchive/backend/sns/SlackService.java +++ b/src/main/java/com/Alchive/backend/sns/SlackService.java @@ -1,5 +1,6 @@ package com.Alchive.backend.sns; +import com.Alchive.backend.config.jwt.TokenService; import com.Alchive.backend.domain.board.Board; import com.Alchive.backend.domain.board.BoardStatus; import com.Alchive.backend.dto.request.BoardCreateRequest; @@ -8,6 +9,7 @@ import com.slack.api.Slack; import com.slack.api.methods.MethodsClient; import com.slack.api.methods.request.chat.ChatPostMessageRequest; +import jakarta.servlet.http.HttpServletRequest; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; @@ -30,6 +32,7 @@ public class SlackService { private String token; private final BoardRepository boardRepository; + private final TokenService tokenService; private String channel = "#alchive-bot"; @@ -68,10 +71,11 @@ public void sendMessageCreateBoard(BoardCreateRequest boardCreateRequest, BoardR } // @Scheduled(cron = "0 0 * * * *") // 정각마다 알림 - public void sendMessageReminderBoard() { + public void sendMessageReminderBoard(HttpServletRequest tokenRequest) { LocalDateTime threeDaysAgo = LocalDateTime.now().minusDays(1); + Long userId = tokenService.validateAccessToken(tokenRequest); - Board unSolvedBoard = boardRepository.findUnsolvedBoardAddedBefore(threeDaysAgo); + Board unSolvedBoard = boardRepository.findUnsolvedBoardAddedBefore(threeDaysAgo, userId); if (unSolvedBoard != null) { String message = String.format(":star-struck: %d일 전 도전했던 %d. %s 문제를 아직 풀지 못했어요. \n \n다시 도전해보세요! :facepunch: \n \n<%s|:link: 문제 풀러가기>", From 2d12151e19f20f7e7f3409b380103b53912f46aa Mon Sep 17 00:00:00 2001 From: nahowo Date: Sun, 10 Nov 2024 22:30:10 +0900 Subject: [PATCH 4/6] =?UTF-8?q?feat:=20JDA=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/Alchive/backend/config/JDAConfig.java | 21 ++++++++ .../backend/sns/DiscordController.java | 3 +- .../Alchive/backend/sns/DiscordService.java | 52 ++++++++++++------- 3 files changed, 54 insertions(+), 22 deletions(-) create mode 100644 src/main/java/com/Alchive/backend/config/JDAConfig.java diff --git a/src/main/java/com/Alchive/backend/config/JDAConfig.java b/src/main/java/com/Alchive/backend/config/JDAConfig.java new file mode 100644 index 0000000..b3e5993 --- /dev/null +++ b/src/main/java/com/Alchive/backend/config/JDAConfig.java @@ -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(); + } +} diff --git a/src/main/java/com/Alchive/backend/sns/DiscordController.java b/src/main/java/com/Alchive/backend/sns/DiscordController.java index 85b2ac7..3596ad8 100644 --- a/src/main/java/com/Alchive/backend/sns/DiscordController.java +++ b/src/main/java/com/Alchive/backend/sns/DiscordController.java @@ -78,8 +78,7 @@ public ResponseEntity installDiscordBot(HttpServletRequest token @PostMapping("dm/send") public ResponseEntity sendDiscordDm(HttpServletRequest tokenRequest, @RequestParam String message) { String discordUserId = discordService.getDiscordUserId(tokenRequest); - String channelId = discordService.getDmChannel(discordUserId); - discordService.sendDm(channelId, message); + discordService.sendDmJda(discordUserId, message); return ResponseEntity.ok(ResultResponse.of(DISCORD_DM_SEND_SUCCESS)); } } diff --git a/src/main/java/com/Alchive/backend/sns/DiscordService.java b/src/main/java/com/Alchive/backend/sns/DiscordService.java index 28d57a2..fa1c889 100644 --- a/src/main/java/com/Alchive/backend/sns/DiscordService.java +++ b/src/main/java/com/Alchive/backend/sns/DiscordService.java @@ -18,6 +18,7 @@ import net.dv8tion.jda.api.JDA; import net.dv8tion.jda.api.JDABuilder; import net.dv8tion.jda.api.entities.User; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.http.*; import org.springframework.scheduling.annotation.EnableScheduling; @@ -38,7 +39,7 @@ @EnableScheduling @Slf4j public class DiscordService { - private JDA jda; + private final JDA jda; private final BoardRepository boardRepository; private final SnsReporitory snsReporitory; private final TokenService tokenService; @@ -93,23 +94,6 @@ public String getDiscordUserIdFromAccessToken(String accessToken) { return discordUserId; } - public String getDmChannel(String discordUserId) { - String getDMChannelUrl = "https://discord.com/api/v10/users/@me/channels"; - Map getDmParams = new HashMap<>(); - getDmParams.put("recipient_id", discordUserId); - - HttpHeaders botTokenHeader = new HttpHeaders(); - botTokenHeader.setContentType(MediaType.APPLICATION_JSON); - botTokenHeader.set("Authorization", "Bot " + discordBotToken); - - HttpEntity> createDmRequest = new HttpEntity<>(getDmParams, botTokenHeader); - ResponseEntity dmResponse = restTemplate.postForEntity(getDMChannelUrl, createDmRequest, Map.class); - Map dmResponseBody = dmResponse.getBody(); - - String channelId = (String) dmResponseBody.get("id"); - return channelId; - } - public String getDiscordUserId(HttpServletRequest tokenRequest) { Long userId = tokenService.validateAccessToken(tokenRequest); Sns snsInfo = snsReporitory.findByUser_IdAndCategory(userId, SnsCategory.DISCORD) @@ -131,6 +115,35 @@ public void sendDm(String channelId, String message) { restTemplate.postForEntity(sendMessageUrl, sendMessageRequest, Map.class); } + public String getDmChannel(String discordUserId) { + String getDMChannelUrl = "https://discord.com/api/v10/users/@me/channels"; + Map getDmParams = new HashMap<>(); + getDmParams.put("recipient_id", discordUserId); + + HttpHeaders botTokenHeader = new HttpHeaders(); + botTokenHeader.setContentType(MediaType.APPLICATION_JSON); + botTokenHeader.set("Authorization", "Bot " + discordBotToken); + + HttpEntity> createDmRequest = new HttpEntity<>(getDmParams, botTokenHeader); + ResponseEntity dmResponse = restTemplate.postForEntity(getDMChannelUrl, createDmRequest, Map.class); + Map dmResponseBody = dmResponse.getBody(); + + String channelId = (String) dmResponseBody.get("id"); + return channelId; + } + + // JDA 사용 메서드 + public void sendDmJda(String discordUserId, String message) { + User user = jda.retrieveUserById(discordUserId).complete(); + if (user != null) { + user.openPrivateChannel().queue(channel -> + channel.sendMessage(message).queue() + ); + } else { + log.info("유저가 존재하지 않습니다. "); + } + } + // @Scheduled(cron = "0 */1 * * * *") // todo: Quartz로 동적 스케줄링 작성하기 public void sendMessageReminderBoard(HttpServletRequest tokenRequest) { LocalDateTime threeDaysAgo = LocalDateTime.now().minusDays(1); @@ -145,8 +158,7 @@ public void sendMessageReminderBoard(HttpServletRequest tokenRequest) { unSolvedBoard.getProblem().getTitle(), unSolvedBoard.getProblem().getUrl()); String discordUserId = getDiscordUserId(tokenRequest); - String channelId = getDmChannel(discordUserId); - sendDm(channelId,message); + sendDmJda(discordUserId,message); } else { log.info("풀지 못한 문제가 존재하지 않습니다. "); } From a5285c6502f9256c4ea8efadd76cd71e72fd7bb8 Mon Sep 17 00:00:00 2001 From: nahowo Date: Sun, 10 Nov 2024 22:47:58 +0900 Subject: [PATCH 5/6] =?UTF-8?q?feat:=20JDA=20=EC=98=88=EC=99=B8=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/Alchive/backend/config/error/ErrorCode.java | 3 ++- .../error/exception/sns/NoSuchDiscordUserException.java | 8 ++++++++ src/main/java/com/Alchive/backend/sns/DiscordService.java | 3 ++- 3 files changed, 12 insertions(+), 2 deletions(-) create mode 100644 src/main/java/com/Alchive/backend/config/error/exception/sns/NoSuchDiscordUserException.java diff --git a/src/main/java/com/Alchive/backend/config/error/ErrorCode.java b/src/main/java/com/Alchive/backend/config/error/ErrorCode.java index 3a7e41d..21f7eba 100644 --- a/src/main/java/com/Alchive/backend/config/error/ErrorCode.java +++ b/src/main/java/com/Alchive/backend/config/error/ErrorCode.java @@ -36,7 +36,8 @@ public enum ErrorCode { SNS_NOT_FOUND(HttpStatus.NOT_FOUND.value(), "N001", "소셜이 존재하지 않음"), // DISCORD - INVALID_GRANT(HttpStatus.FORBIDDEN.value(), "D001", "승인되지 않은 코드"); + INVALID_GRANT(HttpStatus.FORBIDDEN.value(), "D001", "승인되지 않은 코드"), + DISCORD_USER_NOT_FOUND(HttpStatus.NOT_FOUND.value(), "D002", "존재하지 않는 디스코드 유저"); private final int httpStatus; private final String code; diff --git a/src/main/java/com/Alchive/backend/config/error/exception/sns/NoSuchDiscordUserException.java b/src/main/java/com/Alchive/backend/config/error/exception/sns/NoSuchDiscordUserException.java new file mode 100644 index 0000000..227532a --- /dev/null +++ b/src/main/java/com/Alchive/backend/config/error/exception/sns/NoSuchDiscordUserException.java @@ -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); } +} diff --git a/src/main/java/com/Alchive/backend/sns/DiscordService.java b/src/main/java/com/Alchive/backend/sns/DiscordService.java index fa1c889..f5b9d0d 100644 --- a/src/main/java/com/Alchive/backend/sns/DiscordService.java +++ b/src/main/java/com/Alchive/backend/sns/DiscordService.java @@ -1,6 +1,7 @@ package com.Alchive.backend.sns; import com.Alchive.backend.config.error.exception.sns.InvalidGrantException; +import com.Alchive.backend.config.error.exception.sns.NoSuchDiscordUserException; import com.Alchive.backend.config.error.exception.sns.NoSuchSnsIdException; import com.Alchive.backend.config.jwt.TokenService; import com.Alchive.backend.domain.board.Board; @@ -140,7 +141,7 @@ public void sendDmJda(String discordUserId, String message) { channel.sendMessage(message).queue() ); } else { - log.info("유저가 존재하지 않습니다. "); + throw new NoSuchDiscordUserException(); } } From d004056e807776da5da12459d6dcb69560a4ed5b Mon Sep 17 00:00:00 2001 From: nahowo Date: Tue, 12 Nov 2024 21:43:02 +0900 Subject: [PATCH 6/6] =?UTF-8?q?feat:=20=EC=8A=AC=EB=9E=99=20=EB=B4=87=20?= =?UTF-8?q?=EC=97=B0=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 9 +- .../backend/config/result/ResultCode.java | 5 +- .../com/Alchive/backend/domain/sns/Sns.java | 20 ++- .../backend/dto/request/SnsCreateRequest.java | 7 +- .../backend/dto/response/SnsResponseDTO.java | 12 +- .../backend/sns/DiscordController.java | 25 +--- .../Alchive/backend/sns/DiscordService.java | 2 +- .../Alchive/backend/sns/SlackController.java | 68 +++++++--- .../com/Alchive/backend/sns/SlackService.java | 117 ++++++++++-------- 9 files changed, 160 insertions(+), 105 deletions(-) diff --git a/build.gradle b/build.gradle index 6ada8b4..5372148 100644 --- a/build.gradle +++ b/build.gradle @@ -52,12 +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") + implementation 'net.dv8tion:JDA:5.0.0-beta.5' } tasks.named('test') { diff --git a/src/main/java/com/Alchive/backend/config/result/ResultCode.java b/src/main/java/com/Alchive/backend/config/result/ResultCode.java index eafc133..792d6ef 100644 --- a/src/main/java/com/Alchive/backend/config/result/ResultCode.java +++ b/src/main/java/com/Alchive/backend/config/result/ResultCode.java @@ -36,7 +36,10 @@ public enum ResultCode { SNS_CREATE_SUCCESS("N002", "소셜 생성 성공"), // DISCORD - DISCORD_DM_SEND_SUCCESS("D001", "디스코드 DM 전송 성공"); + DISCORD_DM_SEND_SUCCESS("D001", "디스코드 DM 전송 성공"), + + // SLACK + SLACK_DM_SEND_SUCCESS("L001", "슬랙 DM 전송 성공"); private final String code; private final String message; } diff --git a/src/main/java/com/Alchive/backend/domain/sns/Sns.java b/src/main/java/com/Alchive/backend/domain/sns/Sns.java index dcde82e..24efefe 100644 --- a/src/main/java/com/Alchive/backend/domain/sns/Sns.java +++ b/src/main/java/com/Alchive/backend/domain/sns/Sns.java @@ -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; @@ -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(); } diff --git a/src/main/java/com/Alchive/backend/dto/request/SnsCreateRequest.java b/src/main/java/com/Alchive/backend/dto/request/SnsCreateRequest.java index 43fe7cd..1310e39 100644 --- a/src/main/java/com/Alchive/backend/dto/request/SnsCreateRequest.java +++ b/src/main/java/com/Alchive/backend/dto/request/SnsCreateRequest.java @@ -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; } diff --git a/src/main/java/com/Alchive/backend/dto/response/SnsResponseDTO.java b/src/main/java/com/Alchive/backend/dto/response/SnsResponseDTO.java index 6fc5d92..585492f 100644 --- a/src/main/java/com/Alchive/backend/dto/response/SnsResponseDTO.java +++ b/src/main/java/com/Alchive/backend/dto/response/SnsResponseDTO.java @@ -15,8 +15,10 @@ 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) { @@ -24,8 +26,10 @@ public SnsResponseDTO(Sns sns) { 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(); } } diff --git a/src/main/java/com/Alchive/backend/sns/DiscordController.java b/src/main/java/com/Alchive/backend/sns/DiscordController.java index 3596ad8..a7796f6 100644 --- a/src/main/java/com/Alchive/backend/sns/DiscordController.java +++ b/src/main/java/com/Alchive/backend/sns/DiscordController.java @@ -1,6 +1,5 @@ package com.Alchive.backend.sns; -import com.Alchive.backend.config.error.exception.sns.InvalidGrantException; import com.Alchive.backend.config.result.ResultResponse; import com.Alchive.backend.domain.sns.SnsCategory; import com.Alchive.backend.dto.request.SnsCreateRequest; @@ -11,12 +10,8 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import java.util.HashMap; -import java.util.Map; -import org.springframework.beans.factory.annotation.Value; import org.springframework.http.*; import org.springframework.web.bind.annotation.*; -import org.springframework.web.client.RestTemplate; import static com.Alchive.backend.config.result.ResultCode.DISCORD_DM_SEND_SUCCESS; @@ -26,26 +21,12 @@ @Slf4j @RequestMapping("/api/v1/discord") public class DiscordController { - @Value("${DISCORD_CLIENT_ID}") - private String clientId; - - @Value("${DISCORD_CLIENT_SECRET}") - private String clientSecret; - - @Value("${DISCORD_REDIRECT_URI}") - private String redirectUri; - - @Value("${DISCORD_BOT_TOKEN}") - private String discordBotToken; - private final SnsService snsService; private final DiscordService discordService; @Operation(summary = "디스코드 봇 연결", description = "디스코드 액세스 토큰을 요청하고 DM 채널을 연결하는 api입니다. ") @GetMapping("/dm/open") - public ResponseEntity installDiscordBot(HttpServletRequest tokenRequest, @RequestParam String code) { - RestTemplate restTemplate = new RestTemplate(); - + public ResponseEntity openDiscordDm(HttpServletRequest tokenRequest, @RequestParam String code) { // Access Token 요청 String accessToken = discordService.getAccessToken(code); log.info("Access Token 반환 완료: " + accessToken); @@ -61,8 +42,8 @@ public ResponseEntity installDiscordBot(HttpServletRequest token // Discord SNS 정보 저장 SnsCreateRequest snsCreateRequest = SnsCreateRequest.builder() .category(SnsCategory.DISCORD) - .token(discordUserId) // Discord User Id - .channel(channelId) // Discord Channel Id + .sns_id(discordUserId) // Discord User Id + .channel_id(channelId) // Discord Channel Id .time("0 0 18 ? * MON") .build(); snsService.createSns(tokenRequest, snsCreateRequest); diff --git a/src/main/java/com/Alchive/backend/sns/DiscordService.java b/src/main/java/com/Alchive/backend/sns/DiscordService.java index f5b9d0d..7e4907e 100644 --- a/src/main/java/com/Alchive/backend/sns/DiscordService.java +++ b/src/main/java/com/Alchive/backend/sns/DiscordService.java @@ -99,7 +99,7 @@ public String getDiscordUserId(HttpServletRequest tokenRequest) { Long userId = tokenService.validateAccessToken(tokenRequest); Sns snsInfo = snsReporitory.findByUser_IdAndCategory(userId, SnsCategory.DISCORD) .orElseThrow(NoSuchSnsIdException::new); - String discordUserId = snsInfo.getToken(); + String discordUserId = snsInfo.getSns_id(); return discordUserId; } diff --git a/src/main/java/com/Alchive/backend/sns/SlackController.java b/src/main/java/com/Alchive/backend/sns/SlackController.java index 772d4b3..b7a3a3f 100644 --- a/src/main/java/com/Alchive/backend/sns/SlackController.java +++ b/src/main/java/com/Alchive/backend/sns/SlackController.java @@ -1,14 +1,22 @@ package com.Alchive.backend.sns; +import com.Alchive.backend.config.error.exception.sns.NoSuchSnsIdException; +import com.Alchive.backend.config.jwt.TokenService; +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.dto.request.SnsCreateRequest; +import com.Alchive.backend.repository.SnsReporitory; +import com.Alchive.backend.service.SnsService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.servlet.http.HttpServletRequest; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import static com.Alchive.backend.config.result.ResultCode.SLACK_DM_SEND_SUCCESS; @Slf4j @Tag(name = "슬랙", description = "슬랙 관련 API입니다. ") @@ -16,19 +24,51 @@ @RequestMapping("/api/v1/slack") @RequiredArgsConstructor public class SlackController { - @Autowired private final SlackService slackService; + private final SnsService snsService; + private final SnsReporitory snsReporitory; + private final TokenService tokenService; + + @Operation(summary = "슬랙 봇 연결", description = "슬랙 액세스 토큰을 요청하고 DM 채널을 연결하는 api입니다. ") + @GetMapping("/dm/open") + public ResponseEntity openSlackDm(HttpServletRequest tokenRequest, @RequestParam String code) { + // Bot Access Token, User Access Token, Slack User Id 요청 + SnsCreateRequest snsCreateRequest = slackService.getSlackInfo(code); + log.info("사용자 slack 정보를 불러왔습니다. "); + + // Slack SNS 정보 저장 + snsService.createSns(tokenRequest, snsCreateRequest); + log.info("사용자 slack 정보를 저장했습니다. "); - @Operation(summary = "슬랙 봇 설정", description = "슬랙 봇 추가 시 메시지를 전송하는 메서드입니다. ") - @GetMapping("/added") - public void addedSlackBot() { - slackService.sendMessage(":wave: Hi from a bot written in Alchive!"); - log.info("Slack Test"); + // 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 = "문제 리마인더", description = "30분마다 해결하지 못한 문제를 리마인드해주는 메서드입니다. ") - @GetMapping("/reminder") - public void sendReminder(HttpServletRequest tokenRequest) { - slackService.sendMessageReminderBoard(tokenRequest); + @Operation(summary = "슬랙 DM 전송", description = "슬랙 DM으로 메시지를 전송하는 api입니다. ") + @PostMapping("dm/send") + public ResponseEntity sendSlackDm(HttpServletRequest tokenRequest, @RequestParam String message) { + Long userId = tokenService.validateAccessToken(tokenRequest); + Sns sns = snsReporitory.findByUser_IdAndCategory(userId, SnsCategory.SLACK) + .orElseThrow(NoSuchSnsIdException::new); + slackService.sendDm(sns.getSns_id(), sns.getBot_token(), message); + return ResponseEntity.ok(ResultResponse.of(SLACK_DM_SEND_SUCCESS)); } + +// @Operation(summary = "슬랙 봇 설정", description = "슬랙 봇 추가 시 메시지를 전송하는 메서드입니다. ") +// @GetMapping("/added") +// public void addedSlackBot() { +// slackService.sendMessage(":wave: Hi from a bot written in Alchive!"); +// log.info("Slack Test"); +// } +// +// @Operation(summary = "문제 리마인더", description = "30분마다 해결하지 못한 문제를 리마인드해주는 메서드입니다. ") +// @GetMapping("/reminder") +// public void sendReminder(HttpServletRequest tokenRequest) { +// slackService.sendMessageReminderBoard(tokenRequest); +// } } \ No newline at end of file diff --git a/src/main/java/com/Alchive/backend/sns/SlackService.java b/src/main/java/com/Alchive/backend/sns/SlackService.java index c3475f7..5230bf8 100644 --- a/src/main/java/com/Alchive/backend/sns/SlackService.java +++ b/src/main/java/com/Alchive/backend/sns/SlackService.java @@ -1,12 +1,19 @@ package com.Alchive.backend.sns; +import com.Alchive.backend.config.error.exception.sns.InvalidGrantException; import com.Alchive.backend.config.jwt.TokenService; import com.Alchive.backend.domain.board.Board; import com.Alchive.backend.domain.board.BoardStatus; +import com.Alchive.backend.domain.sns.Sns; +import com.Alchive.backend.domain.sns.SnsCategory; import com.Alchive.backend.dto.request.BoardCreateRequest; +import com.Alchive.backend.dto.request.SnsCreateRequest; import com.Alchive.backend.dto.response.BoardResponseDTO; import com.Alchive.backend.repository.BoardRepository; +import com.Alchive.backend.repository.SnsReporitory; +import com.Alchive.backend.service.SnsService; import com.slack.api.Slack; +import com.slack.api.bolt.App; import com.slack.api.methods.MethodsClient; import com.slack.api.methods.request.chat.ChatPostMessageRequest; import jakarta.servlet.http.HttpServletRequest; @@ -14,12 +21,19 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Configuration; +import org.springframework.http.*; import org.springframework.scheduling.annotation.EnableScheduling; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Service; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.web.client.RestTemplate; import java.time.LocalDateTime; import java.time.temporal.ChronoUnit; +import java.util.HashMap; +import java.util.List; +import java.util.Map; @Slf4j @@ -28,64 +42,67 @@ @EnableScheduling @Configuration public class SlackService { - @Value("${SLACK_TOKEN}") - private String token; + @Value("${SLACK_CLIENT_ID}") + private String clientId; - private final BoardRepository boardRepository; - private final TokenService tokenService; + @Value("${SLACK_CLIENT_SECRET}") + private String clientSecret; - private String channel = "#alchive-bot"; + @Value("${SLACK_REDIRECT_URI}") + private String redirectUri; - public void sendMessage(String message) { - try { - MethodsClient methods = Slack.getInstance().methods(token); + @Value("${SLACK_BOT_TOKEN}") + private String slackBotToken; - ChatPostMessageRequest request = ChatPostMessageRequest.builder() - .channel(channel) - .text(message) - .build(); + private RestTemplate restTemplate = new RestTemplate(); - methods.chatPostMessage(request); - log.info("Slack - Message 전송 완료 : {}", message); - } catch (Exception e) { - log.warn("Slack Error - {}", e.getMessage()); - } - } + public SnsCreateRequest getSlackInfo(String code) { + String getTokenUrl = "https://slack.com/api/oauth.v2.access"; + + HttpHeaders getTokenHeaders = new HttpHeaders(); + getTokenHeaders.setContentType(MediaType.APPLICATION_FORM_URLENCODED); - public void sendMessageCreateBoard(BoardCreateRequest boardCreateRequest, BoardResponseDTO board) { - String message = ""; - if (boardCreateRequest.getStatus() == BoardStatus.CORRECT) { - message = String.format(":partying_face: %d. %s 문제를 해결했습니다! \n \n", - boardCreateRequest.getProblemCreateRequest().getNumber(), - boardCreateRequest.getProblemCreateRequest().getTitle(), - board.getId(), board.getId()); - } else if (boardCreateRequest.getStatus() == BoardStatus.NOT_SUBMITTED) { - message = String.format(":round_pushpin: %d. %s 문제를 저장했습니다. \n \n리마인드 알림을 보내드릴게요! :saluting_face: \n \n<%s|:link: 문제 보러가기>", - boardCreateRequest.getProblemCreateRequest().getNumber(), - boardCreateRequest.getProblemCreateRequest().getTitle(), - boardCreateRequest.getProblemCreateRequest().getUrl()); - } else { - return; + MultiValueMap getTokenParams = new LinkedMultiValueMap<>(); + getTokenParams.add("client_id", clientId); + getTokenParams.add("client_secret", clientSecret); + getTokenParams.add("code", code); + getTokenParams.add("redirect_uri", redirectUri); + + HttpEntity> request = new HttpEntity<>(getTokenParams, getTokenHeaders); + ResponseEntity reponse = restTemplate.postForEntity(getTokenUrl, request, Map.class); + Map responseBody = reponse.getBody(); + + if (responseBody.get("ok") == "false") { + throw new InvalidGrantException(); } - sendMessage(message); + + Map authedUser = (Map) responseBody.get("authed_user"); + String slackUserId = (String) authedUser.get("id"); + String botAccessToken = (String) responseBody.get("access_token"); + String userAccessToken = (String) authedUser.get("access_token"); + + SnsCreateRequest snsCreateRequest = SnsCreateRequest.builder() + .category(SnsCategory.SLACK) + .sns_id(slackUserId) + .bot_token(botAccessToken) + .user_token(userAccessToken) + .time("0 0 18 ? * MON") + .build(); + + return snsCreateRequest; } -// @Scheduled(cron = "0 0 * * * *") // 정각마다 알림 - public void sendMessageReminderBoard(HttpServletRequest tokenRequest) { - LocalDateTime threeDaysAgo = LocalDateTime.now().minusDays(1); - Long userId = tokenService.validateAccessToken(tokenRequest); - - Board unSolvedBoard = boardRepository.findUnsolvedBoardAddedBefore(threeDaysAgo, userId); - - if (unSolvedBoard != null) { - String message = String.format(":star-struck: %d일 전 도전했던 %d. %s 문제를 아직 풀지 못했어요. \n \n다시 도전해보세요! :facepunch: \n \n<%s|:link: 문제 풀러가기>", - ChronoUnit.DAYS.between(unSolvedBoard.getCreatedAt(), LocalDateTime.now()), - unSolvedBoard.getProblem().getNumber(), - unSolvedBoard.getProblem().getTitle(), - unSolvedBoard.getProblem().getUrl()); - sendMessage(message); - } else { - log.info("풀지 못한 문제가 존재하지 않습니다. "); - } + public void sendDm(String slackUserId, String botAccessToken, String message) { + String sendDmUrl = "https://slack.com/api/chat.postMessage"; + HttpHeaders sendDmHeaders = new HttpHeaders(); + sendDmHeaders.setContentType(MediaType.APPLICATION_JSON); + sendDmHeaders.setBearerAuth(botAccessToken); + + Map sendDmParams = new HashMap<>(); + sendDmParams.put("channel", slackUserId); + sendDmParams.put("text", message); + + HttpEntity> request = new HttpEntity<>(sendDmParams, sendDmHeaders); + restTemplate.postForEntity(sendDmUrl, request, Map.class); } }