-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #33 from Akatsuki-USW/feature/FCM
[WBS1-187] Feature/fcm
- Loading branch information
Showing
17 changed files
with
461 additions
and
9 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | ||
} | ||
} |
33 changes: 33 additions & 0 deletions
33
src/main/java/bokjak/bokjakserver/domain/notification/controller/NotificationController.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)); | ||
} | ||
} |
26 changes: 26 additions & 0 deletions
26
src/main/java/bokjak/bokjakserver/domain/notification/dto/FcmDto.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
) {} | ||
} |
105 changes: 105 additions & 0 deletions
105
src/main/java/bokjak/bokjakserver/domain/notification/dto/NotificationDto.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | ||
} | ||
} | ||
} |
11 changes: 11 additions & 0 deletions
11
src/main/java/bokjak/bokjakserver/domain/notification/exception/NotificationException.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
8 changes: 8 additions & 0 deletions
8
src/main/java/bokjak/bokjakserver/domain/notification/repository/NotificationRepository.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> { | ||
|
||
} |
100 changes: 100 additions & 0 deletions
100
src/main/java/bokjak/bokjakserver/domain/notification/service/FcmService.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} | ||
} |
Oops, something went wrong.