From d004056e807776da5da12459d6dcb69560a4ed5b Mon Sep 17 00:00:00 2001 From: nahowo Date: Tue, 12 Nov 2024 21:43:02 +0900 Subject: [PATCH] =?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); } }