Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file removed .DS_Store
Binary file not shown.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@ out/
### VS Code ###
.vscode/

### Mac OS ###
.DS_Store

### 환경 변수
.env
/src/main/resources/env.properties
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@
@RequestMapping("/api/v2/boards")
public class BoardController {
private final BoardService boardService;
private final SlackService slackService;

@Operation(summary = "게시물 저장 여부 조회", description = "게시물의 저장 여부를 조회하는 메서드입니다. ")
@PostMapping("/saved")
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +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.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;
Expand All @@ -24,45 +20,20 @@
@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, "안녕하세요! 이제부터 풀지 못한 문제들을 정해진 시간에 알려드릴게요. ");

public ResponseEntity<ResultResponse> initializeDiscordChannelAndSendWelcomeDM(@AuthenticationPrincipal User user, @RequestParam String code) {
discordService.initializeDiscordChannelAndSaveSnsInfo(user, code);
discordService.sendDiscordDm(user, "안녕하세요! 이제부터 풀지 못한 문제들을 정해진 시간에 알려드릴게요. ");
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);
discordService.sendJdaDm(user, message);
return ResponseEntity.ok(ResultResponse.of(DISCORD_DM_SEND_SUCCESS));
}
}
22 changes: 3 additions & 19 deletions src/main/java/com/Alchive/backend/controller/SlackController.java
Original file line number Diff line number Diff line change
@@ -1,9 +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.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;
Expand All @@ -23,33 +21,19 @@
@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 채널에 추가했습니다. ");
slackService.initializeSlackChannelAndSaveSnsInfo(user, code);
slackService.sendSlackDm(user, "안녕하세요! 이제부터 풀지 못한 문제들을 정해진 시간에 알려드릴게요. ");
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);
slackService.sendSlackDm(user, message);
return ResponseEntity.ok(ResultResponse.of(SLACK_DM_SEND_SUCCESS));
}
}
Original file line number Diff line number Diff line change
@@ -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.domain.user.User;
import com.Alchive.backend.dto.request.SnsCreateRequest;
import com.Alchive.backend.dto.response.SnsResponseDTO;
Expand Down Expand Up @@ -31,7 +32,7 @@ public ResponseEntity<ResultResponse> getSns(@PathVariable Long snsId) {

@Operation(summary = "소셜 정보 생성", description = "소셜 정보를 생성하는 메서드입니다. ")
@PostMapping("")
public ResponseEntity<ResultResponse> createSns(@AuthenticationPrincipal User user, SnsCreateRequest request) {
public ResponseEntity<ResultResponse> createSns(@AuthenticationPrincipal User user, @RequestBody SnsCreateRequest request) {
snsService.createSns(user, request);
return ResponseEntity.ok(ResultResponse.of(SNS_CREATE_SUCCESS));
}
Expand Down
165 changes: 85 additions & 80 deletions src/main/java/com/Alchive/backend/service/DiscordService.java
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
package com.Alchive.backend.service;

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.domain.board.Board;
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.BoardRepository;
import com.Alchive.backend.repository.SnsReporitory;
import lombok.RequiredArgsConstructor;
Expand All @@ -18,7 +18,6 @@
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;
Expand All @@ -30,10 +29,13 @@
@EnableScheduling
@Slf4j
public class DiscordService {
private final static String DISCORD_USER_INFO_REQUEST_URL = "https://discord.com/api/v10/users/@me";
private final static String DISCORD_ACCESS_TOKEN_REQUEST_URL = "https://discord.com/api/oauth2/token";
private final static String DISCORD_DM_CHANNEL_REQUEST_URL = "https://discord.com/api/v10/users/@me/channels";
private final JDA jda;
private final BoardRepository boardRepository;
private final SnsReporitory snsReporitory;

private final SnsService snsService;
@Value("${DISCORD_CLIENT_ID}")
private String clientId;

Expand All @@ -45,110 +47,113 @@ public class DiscordService {

@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<String, String> 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<MultiValueMap<String, String>> request = new HttpEntity<>(getTokenParams, getTokenHeaders);
ResponseEntity<Map> response = restTemplate.postForEntity(getTokenUrl, request, Map.class);
Map<String, Object> responseBody = response.getBody();
public void initializeDiscordChannelAndSaveSnsInfo(com.Alchive.backend.domain.user.User user, String code) {
String accessToken = getAccessToken(code);
String discordUserId = getDiscordUserIdFromAccessToken(accessToken);
String channelId = getDmChannel(discordUserId);

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);
}

if (responseBody.containsKey("error") && responseBody.get("error") == "invalid_grant" ) {
throw new InvalidGrantException();
}
String accessToken = (String) responseBody.get("access_token");
private String getAccessToken(String code) {
HttpHeaders httpHeader = new HttpHeaders();
httpHeader.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
MultiValueMap<String, String> httpBody = new LinkedMultiValueMap<>();
httpBody.add("client_id", clientId);
httpBody.add("client_secret", clientSecret);
httpBody.add("grant_type", "authorization_code");
httpBody.add("redirect_uri", redirectUri);
httpBody.add("code", code);

HttpEntity<MultiValueMap<String, String>> request = new HttpEntity<>(httpBody, httpHeader);
ResponseEntity<Map> response = snsService.sendRestTemplateExchange(DISCORD_ACCESS_TOKEN_REQUEST_URL, HttpMethod.POST, request);
String accessToken = (String) snsService.parseResponse(response, "access_token");
return accessToken;
}

public String getDiscordUserIdFromAccessToken(String accessToken) {
String getUserInfoUrl = "https://discord.com/api/v10/users/@me";
private String getDiscordUserIdFromAccessToken(String accessToken) {
HttpHeaders accessTokenHeaders = new HttpHeaders();
accessTokenHeaders.setBearerAuth(accessToken);
HttpEntity<String> request = new HttpEntity<>(accessTokenHeaders);
ResponseEntity<Map> response = snsService.sendRestTemplateExchange(DISCORD_USER_INFO_REQUEST_URL, HttpMethod.GET, request);
String discordUserId = (String) snsService.parseResponse(response, "id");
return discordUserId;
}

HttpEntity<String> authRequest = new HttpEntity<>(accessTokenHeaders);
ResponseEntity<Map> userInfoResponse = restTemplate.exchange(getUserInfoUrl, HttpMethod.GET, authRequest, Map.class);
Map<String, Object> userInfo = userInfoResponse.getBody();
private String getDmChannel(String discordUserId) {
Map<String, String> httpBody = new HashMap<>();
httpBody.put("recipient_id", discordUserId);
HttpHeaders httpHeader = new HttpHeaders();
httpHeader.setContentType(MediaType.APPLICATION_JSON);
httpHeader.set("Authorization", "Bot " + discordBotToken);
HttpEntity<Map<String, String>> request = new HttpEntity<>(httpBody, httpHeader);

String discordUserId = (String) userInfo.get("id");
return discordUserId;
ResponseEntity<Map> response = snsService.sendRestTemplateExchange(DISCORD_DM_CHANNEL_REQUEST_URL, HttpMethod.POST, request);
String channelId = (String) snsService.parseResponse(response, "id");
return channelId;
}

public Sns getDiscordInfo(com.Alchive.backend.domain.user.User user) {
Long userId = user.getId();
Sns discordInfo = snsReporitory.findByUser_IdAndCategory(userId, SnsCategory.DISCORD)
.orElseThrow(NoSuchSnsIdException::new);
return discordInfo;
// @Scheduled(cron = "0 */1 * * * *") // todo: Quartz로 동적 스케줄링 작성하기
private void sendMessageReminderBoard(com.Alchive.backend.domain.user.User user) {
LocalDateTime threeDaysAgo = LocalDateTime.now().minusDays(1);

Board unSolvedBoard = boardRepository.findUnsolvedBoardAddedBefore(threeDaysAgo, user.getId());
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());

sendJdaDm(user, message);
} else {
log.info("풀지 못한 문제가 존재하지 않습니다. ");
}
}

public void sendDm(String channelId, String message) {
public void sendDiscordDm(com.Alchive.backend.domain.user.User user, String message) {
String channelId = getUserChannelId(user);
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<String, String> sendDmParams = new HashMap<>();
sendDmParams.put("content", message);
HttpEntity<Map<String, String>> request = new HttpEntity<>(sendDmParams, sendDmHeaders);

HttpEntity<Map<String, String>> sendMessageRequest = new HttpEntity<>(sendDmParams, sendDmHeaders);
restTemplate.postForEntity(sendMessageUrl, sendMessageRequest, Map.class);
snsService.sendRestTemplateExchange(sendMessageUrl, HttpMethod.POST, request);
}

public String getDmChannel(String discordUserId) {
String getDMChannelUrl = "https://discord.com/api/v10/users/@me/channels";
Map<String, String> getDmParams = new HashMap<>();
getDmParams.put("recipient_id", discordUserId);

HttpHeaders botTokenHeader = new HttpHeaders();
botTokenHeader.setContentType(MediaType.APPLICATION_JSON);
botTokenHeader.set("Authorization", "Bot " + discordBotToken);

HttpEntity<Map<String, String>> createDmRequest = new HttpEntity<>(getDmParams, botTokenHeader);
ResponseEntity<Map> dmResponse = restTemplate.postForEntity(getDMChannelUrl, createDmRequest, Map.class);
Map<String, Object> dmResponseBody = dmResponse.getBody();
private String getUserChannelId(com.Alchive.backend.domain.user.User user) {
return getDiscordInfo(user).getChannel_id();
}

String channelId = (String) dmResponseBody.get("id");
return channelId;
private Sns getDiscordInfo(com.Alchive.backend.domain.user.User user) {
Long userId = user.getId();
Sns discordInfo = snsReporitory.findByUser_IdAndCategory(userId, SnsCategory.DISCORD)
.orElseThrow(NoSuchSnsIdException::new);
return discordInfo;
}

// 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 {
throw new NoSuchDiscordUserException();
}
public void sendJdaDm(com.Alchive.backend.domain.user.User user, String message) {
String discordUserId = getDiscordInfo(user).getSns_id();
User jdaUser = jda.retrieveUserById(discordUserId).complete();
checkUserNull(jdaUser);
jdaUser.openPrivateChannel().queue(channel ->
channel.sendMessage(message).queue());
}

// @Scheduled(cron = "0 */1 * * * *") // todo: Quartz로 동적 스케줄링 작성하기
public void sendMessageReminderBoard(com.Alchive.backend.domain.user.User user) {
LocalDateTime threeDaysAgo = LocalDateTime.now().minusDays(1);

Board unSolvedBoard = boardRepository.findUnsolvedBoardAddedBefore(threeDaysAgo, user.getId());
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());

Sns discordInfo = getDiscordInfo(user);
sendDmJda(discordInfo.getSns_id(), message);
} else {
log.info("풀지 못한 문제가 존재하지 않습니다. ");
private void checkUserNull(User user) {
if (user == null) {
throw new NoSuchDiscordUserException();
}
}
}
Loading
Loading