Skip to content

Commit

Permalink
Merge pull request #33 from Akatsuki-USW/feature/FCM
Browse files Browse the repository at this point in the history
[WBS1-187] Feature/fcm
  • Loading branch information
woo0doo authored Jul 20, 2023
2 parents f9a5f2d + a0ffab1 commit ed4919e
Show file tree
Hide file tree
Showing 17 changed files with 461 additions and 9 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ build/
application.yml
/src/main/generated/
memo.md
/src/main/resources/buzzzzing-firebase-private-key.json

### STS ###
.apt_generated
Expand Down
7 changes: 7 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,13 @@ dependencies {
// S3
implementation 'org.springframework.cloud:spring-cloud-starter-aws:2.2.6.RELEASE'

// fcm 추가
implementation("com.google.firebase:firebase-admin:6.8.1")
implementation("com.squareup.okhttp3:okhttp:4.9.1")

// json simple
implementation("com.googlecode.json-simple:json-simple:1.1.1")

}

tasks.named('test') {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,17 @@ public enum StatusCode {
NOT_FOUND_REPORT_TARGET(404,3020,"report target not found."),
ALREADY_BAN_USER(400,3030,"already ban user."),
NOT_CORRECT_USER_AND_TARGET(404, 3040,"not correct writer and report target id"),
OVER_CONTENT_LENGTH(400,3080,"limit of the number of words.");
OVER_CONTENT_LENGTH(400,3080,"limit of the number of words."),


/**
* Notification
*/
GET_FCM_ACCESS_TOKEN_ERROR(400,4500,"fcm access token get failed"),
FCM_MESSAGE_JSON_PARSING_ERROR(400,4510,"fcm message json parsing failed"),
SEND_FCM_PUSH_ERROR(400,4520,"send fcm push message failed"),
NOT_FOUND_NOTIFICATION(404, 4530, "not found notification error");


private final int HttpCode;
private final int statusCode;
Expand Down
14 changes: 14 additions & 0 deletions src/main/java/bokjak/bokjakserver/config/WebConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package bokjak.bokjakserver.config;

import org.json.simple.parser.JSONParser;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class WebConfig {

@Bean
public JSONParser jsonParser() {
return new JSONParser();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package bokjak.bokjakserver.domain.notification.controller;

import bokjak.bokjakserver.common.dto.ApiResponse;
import bokjak.bokjakserver.domain.notification.dto.NotificationDto;
import bokjak.bokjakserver.domain.notification.dto.NotificationDto.NotificationResponse;
import bokjak.bokjakserver.domain.notification.service.NotificationService;
import bokjak.bokjakserver.domain.user.model.User;
import bokjak.bokjakserver.domain.user.service.UserService;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;

import static bokjak.bokjakserver.common.dto.ApiResponse.*;

@RestController
@RequiredArgsConstructor
@RequestMapping("/notification")
public class NotificationController {

private final NotificationService notificationService;
private final UserService userService;

@GetMapping("/users/me")
public ApiResponse<NotificationDto.NotificationListResponse> getMyNotifications() {
User currentUser = userService.getCurrentUser();
return success(notificationService.getMyNotifications(currentUser));
}

@PutMapping("/{notificationId}/read")
public ApiResponse<NotificationResponse> readNotification(@PathVariable Long notificationId) {
User currentUser = userService.getCurrentUser();
return success(notificationService.readNotification(notificationId, currentUser));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package bokjak.bokjakserver.domain.notification.dto;

public class FcmDto {
public record FcmMessage(
Boolean validate_only,
Message message
) {}

public record Message(
String token,
Notification data
) {}

public record Notification(
String title,
String body,
String redirectTargetId,
String type
) {}

public record PushMessage(
Long receiverId,
String title,
String body
) {}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
package bokjak.bokjakserver.domain.notification.dto;

import bokjak.bokjakserver.domain.comment.model.Comment;
import bokjak.bokjakserver.domain.notification.model.Notification;
import bokjak.bokjakserver.domain.notification.model.NotificationType;
import bokjak.bokjakserver.domain.spot.model.Spot;
import bokjak.bokjakserver.domain.user.model.User;
import lombok.Builder;

import java.time.LocalDateTime;
import java.util.List;

public class NotificationDto {
public record NotifyParams(
User receiver, NotificationType type, Long redirectTargetId, String title, String content
) {
@Builder
public NotifyParams{}

public static NotifyParams ofCreateSpotComment(
User spotAuthor,
User commentAuthor,
Spot spot,
Comment comment
) {
String content = """
'%s'님이 '%s'님의 게시글에 댓글을 남겼어요 : %s
""".formatted(
commentAuthor.getNickname(),
spotAuthor.getNickname(),
comment.getContent()
);
return NotifyParams.builder()
.receiver(spotAuthor)
.type(NotificationType.CREATE_SPOT_COMMENT)
.redirectTargetId(spot.getId())
.title(spot.getTitle())
.content(content)
.build();
}

public static NotifyParams ofCreateFeedCommentComment(
User commentAuthor,
Spot spot,
Comment parentComment,
Comment comment
) {
String content = """
'%s'님이 '%s'님의 댓글에 대댓글을 남겼어요 : %s
""".formatted(
commentAuthor.getNickname(),
parentComment.getUser().getNickname(),
comment.getUser().getNickname(),
comment.getContent()
);
return NotifyParams.builder()
.receiver(commentAuthor)
.type(NotificationType.CREATE_SPOT_COMMENT_COMMENT)
.redirectTargetId(spot.getId())
.title(spot.getTitle())
.content(content)
.build();
}
}
public record NotificationResponse(
Long notificationId,
NotificationType notificationType,
String targetEntity,
Long redirectTargetId,
String title,
String body,
LocalDateTime createdAt,
boolean isRead
) {
@Builder
public NotificationResponse {}

public static NotificationResponse of(Notification notification) {
NotificationType notificationType = notification.getType();
String targetClassName = notificationType.getRedirectTargetClass().getSimpleName();
return NotificationResponse.builder()
.notificationId(notification.getId())
.notificationType(notificationType)
.targetEntity(targetClassName)
.redirectTargetId(notification.getRedirectTargetId())
.title(notification.getTitle())
.body(notification.getContent())
.createdAt(notification.getCreatedAt())
.isRead(notification.isRead())
.build();
}

}

public record NotificationListResponse(List<NotificationResponse> notifications) {
@Builder
public NotificationListResponse {}

public static NotificationListResponse of(List<NotificationResponse> notifications) {
return NotificationListResponse.builder()
.notifications(notifications)
.build();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package bokjak.bokjakserver.domain.notification.exception;

import bokjak.bokjakserver.common.exception.BuzException;
import bokjak.bokjakserver.common.exception.StatusCode;

public class NotificationException extends BuzException {

public NotificationException(StatusCode statusCode) {
super(statusCode);
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package bokjak.bokjakserver.domain.notification.model;

import bokjak.bokjakserver.domain.spot.model.Spot;
import bokjak.bokjakserver.domain.user.model.User;
import lombok.Getter;
import lombok.RequiredArgsConstructor;

Expand All @@ -11,12 +12,14 @@ public enum NotificationType {
/**
* 내 게시글에 답글
*/
CREATE_SPOT_COMMENT(ReceiverType.AUTHOR, Object.class),
CREATE_SPOT_COMMENT(ReceiverType.AUTHOR, Spot.class),

/**
* 내 답글에 답글(대댓글)
*/
CREATE_SPOT_COMMENT_COMMENT(ReceiverType.AUTHOR, Spot.class);
CREATE_SPOT_COMMENT_COMMENT(ReceiverType.AUTHOR, Spot.class),

TEST_USER_ITSELF(ReceiverType.USER, User.class);

private enum ReceiverType {
AUTHOR,USER
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package bokjak.bokjakserver.domain.notification.repository;

import bokjak.bokjakserver.domain.notification.model.Notification;
import org.springframework.data.jpa.repository.JpaRepository;

public interface NotificationRepository extends JpaRepository<Notification, Long> {

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
package bokjak.bokjakserver.domain.notification.service;

import bokjak.bokjakserver.common.exception.StatusCode;
import bokjak.bokjakserver.domain.notification.dto.FcmDto;
import bokjak.bokjakserver.domain.notification.dto.FcmDto.FcmMessage;
import bokjak.bokjakserver.domain.notification.dto.FcmDto.Message;
import bokjak.bokjakserver.domain.notification.dto.NotificationDto;
import bokjak.bokjakserver.domain.notification.exception.NotificationException;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.auth.oauth2.GoogleCredentials;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import okhttp3.*;
import org.json.simple.JSONObject;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.ClassPathResource;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import org.json.simple.parser.JSONParser;

import java.io.IOException;
import java.util.List;
import java.util.concurrent.CompletableFuture;


@Slf4j
@Service
@RequiredArgsConstructor
public class FcmService {

private final ObjectMapper objectMapper;
private final JSONParser jsonParser;


private static String FCM_PRIVATE_KEY_PATH = "buzzzzing-firebase-private-key.json";
private static String fireBaseScope = "https://www.googleapis.com/auth/cloud-platform";
private static String PROJECT_ID_URL = "https://fcm.googleapis.com/v1/projects/buzzzzing-c258e/messages:send";


private String getAccessToken() {
try {
String firebaseConfigPath = FCM_PRIVATE_KEY_PATH;
GoogleCredentials credentials = GoogleCredentials
.fromStream(new ClassPathResource(firebaseConfigPath).getInputStream())
.createScoped(List.of(fireBaseScope));
credentials.refreshIfExpired();
return credentials.getAccessToken().getTokenValue();
} catch (IOException e) {
log.warn("FCM getAccessToken Error : {}", e.getMessage());
throw new NotificationException(StatusCode.GET_FCM_ACCESS_TOKEN_ERROR);
}
}

public String makeMessage(String targetToken, NotificationDto.NotifyParams params) {
try {
FcmMessage fcmMessage = new FcmMessage(
false,
new Message(
targetToken,
new FcmDto.Notification(
params.title(),
params.content(),
String.valueOf(params.redirectTargetId()),
params.type().toString()
)
)
);
return objectMapper.writeValueAsString(fcmMessage);
} catch (JsonProcessingException e) {
log.warn("FCM [makeMessage] Error : {}", e.getMessage());
throw new NotificationException(StatusCode.FCM_MESSAGE_JSON_PARSING_ERROR);
}
}

@Async
public CompletableFuture<Boolean> sendPushMessage(String fcmToken, NotificationDto.NotifyParams params) {
String message = makeMessage(fcmToken, params);
String accessToken = getAccessToken();
OkHttpClient client = new OkHttpClient();
Request request = new Request.Builder()
.url(PROJECT_ID_URL)
.addHeader("Authorization", "Bearer " + accessToken)
.addHeader("Content-Type", "application/json; UTF-8")
.post(RequestBody.create(message, MediaType.parse("application/json; charset=urf-8")))
.build();
try (Response response = client.newCall(request).execute()){
if (!response.isSuccessful() && response.body() != null) {
JSONObject responseBody = (JSONObject) jsonParser.parse(response.body().string());
String errorMessage = ((JSONObject) responseBody.get("error")).get("message").toString();
log.warn("FCM [sendPushMessage] okHttp response is not OK : {}", errorMessage);
return CompletableFuture.completedFuture(false);
}
return CompletableFuture.completedFuture(true);
} catch (Exception e) {
log.warn("FCM [sendPushMessage] I/O Exception : {}", e.getMessage());
throw new NotificationException(StatusCode.SEND_FCM_PUSH_ERROR);
}
}
}
Loading

0 comments on commit ed4919e

Please sign in to comment.