Skip to content

Commit

Permalink
merge : FCM 서버에 알림 요청하기 #133
Browse files Browse the repository at this point in the history
Feature/#133 fcm서버에 알림 정보 보내기
  • Loading branch information
java-saeng authored Jul 31, 2023
2 parents 7a69c4a + 0320321 commit 6adedd7
Show file tree
Hide file tree
Showing 12 changed files with 242 additions and 15 deletions.
8 changes: 8 additions & 0 deletions .github/workflows/backend-dev-deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,14 @@ jobs:
- name: 백엔드 메인으로 checkout
uses: actions/checkout@v3

- name: firebase key 생성
run: |
echo "$FIREBASE_KEY" > firebase-kerdy.json
- name: firebase key 이동
run: |
cp firebase-kerdy.json src/main/resources
- name: JDK 11로 설정
uses: actions/setup-java@v3
with:
Expand Down
3 changes: 3 additions & 0 deletions backend/emm-sale/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,9 @@ dependencies {

// jwt
implementation 'io.jsonwebtoken:jjwt:0.9.1'

// firebase
implementation 'com.google.firebase:firebase-admin:9.2.0'
}

tasks.named('test') {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.emmsale.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;

@Configuration
public class RestTemplateConfig {

@Bean
public RestTemplate restTemplate() {
return new RestTemplate();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import com.emmsale.login.application.dto.GithubProfileResponse;
import com.emmsale.login.exception.LoginException;
import com.emmsale.login.exception.LoginExceptionType;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
Expand All @@ -15,10 +16,9 @@
import org.springframework.web.client.RestTemplate;

@Component
@RequiredArgsConstructor
public class GithubClient {

private static final RestTemplate REST_TEMPLATE = new RestTemplate();

@Value("${github.client.id}")
private String clientId;
@Value("${github.client.secret}")
Expand All @@ -28,6 +28,8 @@ public class GithubClient {
@Value("${github.url.profile}")
private String profileUrl;

private final RestTemplate restTemplate;

public String getAccessTokenFromGithub(final String code) {
final GithubAccessTokenRequest githubAccessTokenRequest = buildGithubAccessTokenRequest(code);

Expand All @@ -54,7 +56,7 @@ private GithubProfileResponse getGithubProfileResponse(final String accessToken)

final HttpEntity<String> httpEntity = new HttpEntity<>(headers);

return REST_TEMPLATE
return restTemplate
.exchange(profileUrl, HttpMethod.GET, httpEntity, GithubProfileResponse.class)
.getBody();
}
Expand All @@ -76,7 +78,7 @@ private String getAccessTokenResponse(final GithubAccessTokenRequest githubAcces
headers
);

return REST_TEMPLATE
return restTemplate
.exchange(accessTokenUrl, HttpMethod.POST, httpEntity, GithubAccessTokenResponse.class)
.getBody()
.getAccessToken();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
package com.emmsale.notification.application;

import static com.emmsale.notification.exception.NotificationExceptionType.*;
import static com.emmsale.notification.exception.NotificationExceptionType.CONVERTING_JSON_ERROR;
import static com.emmsale.notification.exception.NotificationExceptionType.NOT_FOUND_FCM_TOKEN;

import com.emmsale.member.domain.Member;
import com.emmsale.member.domain.MemberRepository;
import com.emmsale.member.exception.MemberException;
import com.emmsale.member.exception.MemberExceptionType;
import com.emmsale.notification.application.dto.FcmMessage;
import com.emmsale.notification.application.dto.FcmMessage.Data;
import com.emmsale.notification.application.dto.FcmMessage.Message;
import com.emmsale.notification.domain.FcmToken;
import com.emmsale.notification.domain.FcmTokenRepository;
import com.emmsale.notification.domain.Notification;
import com.emmsale.notification.exception.NotificationException;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.auth.oauth2.GoogleCredentials;
import java.io.IOException;
import java.util.List;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.ClassPathResource;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;

@Service
@RequiredArgsConstructor
@Slf4j
public class FirebaseCloudMessageClient {

private static final String PREFIX_ACCESS_TOKEN = "Bearer ";
private static final String PREFIX_FCM_REQUEST_URL = "https://fcm.googleapis.com/v1/projects/";
private static final String POSTFIX_FCM_REQUEST_URL = "/messages:send";
private static final String FIREBASE_KEY_PATH = "firebase-kerdy.json";
private static final boolean DEFAULT_VALIDATE_ONLY = false;

private final ObjectMapper objectMapper;
private final MemberRepository memberRepository;
private final RestTemplate restTemplate;
private final FcmTokenRepository fcmTokenRepository;

@Value("${firebase.project.id}")
private String projectId;

public void sendMessageTo(final Long receiverId, final Notification notification) {

final FcmToken fcmToken = fcmTokenRepository.findByMemberId(receiverId)
.orElseThrow(() -> new NotificationException(NOT_FOUND_FCM_TOKEN));

final String message = makeMessage(fcmToken.getToken(), notification);

final HttpHeaders httpHeaders = new HttpHeaders();
httpHeaders.add(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE);
httpHeaders.add(HttpHeaders.AUTHORIZATION, PREFIX_ACCESS_TOKEN + getAccessToken());

final HttpEntity<String> httpEntity = new HttpEntity<>(message, httpHeaders);

final String fcmRequestUrl = PREFIX_FCM_REQUEST_URL + projectId + POSTFIX_FCM_REQUEST_URL;

final ResponseEntity<String> exchange = restTemplate.exchange(
fcmRequestUrl,
HttpMethod.POST,
httpEntity,
String.class
);

if (exchange.getStatusCode().isError()) {
log.error("firebase 접속 에러 = {}", exchange.getBody());
}
}

private String makeMessage(final String targetToken, final Notification notification) {

final Long senderId = notification.getSenderId();
final Member sender = memberRepository.findById(senderId)
.orElseThrow(() -> new MemberException(MemberExceptionType.NOT_FOUND_MEMBER));

final Data messageData = new Data(
sender.getName(), senderId.toString(),
notification.getReceiverId().toString(), notification.getMessage(),
sender.getOpenProfileUrl()
);

final Message message = new Message(messageData, targetToken);

final FcmMessage fcmMessage = new FcmMessage(DEFAULT_VALIDATE_ONLY, message);

try {
return objectMapper.writeValueAsString(fcmMessage);
} catch (JsonProcessingException e) {
log.error("메세지 보낼 때 JSON 변환 에러", e);
throw new NotificationException(CONVERTING_JSON_ERROR);
}
}

private String getAccessToken() {
final String firebaseConfigPath = FIREBASE_KEY_PATH;

try {
final GoogleCredentials googleCredentials = GoogleCredentials
.fromStream(new ClassPathResource(firebaseConfigPath).getInputStream())
.createScoped(List.of("https://www.googleapis.com/auth/cloud-platform"));

googleCredentials.refreshIfExpired();

return googleCredentials.getAccessToken().getTokenValue();
} catch (IOException e) {
log.error("구글 토큰 요청 에러", e);
throw new NotificationException(GOOGLE_REQUEST_TOKEN_ERROR);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ public class NotificationCommandService {
private final NotificationRepository notificationRepository;
private final FcmTokenRepository fcmTokenRepository;
private final MemberRepository memberRepository;
private final FirebaseCloudMessageClient firebaseCloudMessageClient;

public NotificationResponse create(final NotificationRequest notificationRequest) {
final Long senderId = notificationRequest.getSenderId();
Expand All @@ -49,7 +50,8 @@ public NotificationResponse create(final NotificationRequest notificationRequest
notificationRequest.getMessage()
));

//FCM에 notification 보내기
firebaseCloudMessageClient.sendMessageTo(receiverId, savedNotification);

return NotificationResponse.from(savedNotification);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package com.emmsale.notification.application;

import static com.emmsale.notification.exception.NotificationExceptionType.*;

import com.emmsale.notification.application.dto.NotificationResponse;
import com.emmsale.notification.domain.Notification;
import com.emmsale.notification.domain.NotificationRepository;
Expand All @@ -18,9 +20,7 @@ public class NotificationQueryService {

public NotificationResponse findNotificationBy(final Long notificationId) {
final Notification savedNotification = notificationRepository.findById(notificationId)
.orElseThrow(
() -> new NotificationException(NotificationExceptionType.NOT_FOUND_NOTIFICATION)
);
.orElseThrow(() -> new NotificationException(NOT_FOUND_NOTIFICATION));

return NotificationResponse.from(savedNotification);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package com.emmsale.notification.application.dto;

import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Getter;
import lombok.RequiredArgsConstructor;

@RequiredArgsConstructor
@Getter
public class FcmMessage {

@JsonProperty("validate_only")
private final boolean validateOnly;
private final Message message;

@RequiredArgsConstructor
@Getter
public static class Message {

private final Data data;
private final String token;
}

@RequiredArgsConstructor
@Getter
public static class Data {

private final String senderName;
private final String senderId;
private final String receiverId;
private final String message;
private final String openProfileUrl;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,23 @@ public enum NotificationExceptionType implements BaseExceptionType {
NOT_FOUND_NOTIFICATION(
HttpStatus.NOT_FOUND,
"알림이 존재하지 않습니다."
),

NOT_FOUND_FCM_TOKEN(
HttpStatus.NOT_FOUND,
"해당 사용자의 기기를 구별할 수 있는 FCM 토큰이 존재하지 않습니다."
),

CONVERTING_JSON_ERROR(
HttpStatus.INTERNAL_SERVER_ERROR,
"알림 메시지를 보낼 때 JSON으로 변환하는 과정에서 발생한 에러입니다."
),

GOOGLE_REQUEST_TOKEN_ERROR(
HttpStatus.INTERNAL_SERVER_ERROR,
"구글에 토큰 요청할 때 발생한 에러"
)

;

private final HttpStatus httpStatus;
Expand Down
4 changes: 4 additions & 0 deletions backend/emm-sale/src/main/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,7 @@ security:
token:
secret-key: secret_key
expire-length: 3_600_000_000

firebase:
project:
id: kerdy
10 changes: 5 additions & 5 deletions backend/emm-sale/src/main/resources/http/notification.http
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
### 알림 저장

POST http://localhost:8080/notification
POST http://localhost:8080/notifications
Content-Type: application/json

{
"senderId" : 1,
"receiverId" : 2,
"message" : "알람이다",
"receiverId" : 3,
"message" : "테스트 알람이다",
"eventId" : 1
}

### FCM 토큰 저장 API (새롭게 저장)

POST http://localhost:8080/notification/token
POST http://localhost:8080/notifications/token
Content-Type: application/json

{
Expand All @@ -22,7 +22,7 @@ Content-Type: application/json

### FCM 토큰 저장 API (기존에 있던 것 수정)

POST http://localhost:8080/notification/token
POST http://localhost:8080/notifications/token
Content-Type: application/json

{
Expand Down
Loading

0 comments on commit 6adedd7

Please sign in to comment.