Skip to content

Commit

Permalink
feat: Setting FCM (#12)
Browse files Browse the repository at this point in the history
  • Loading branch information
devmizz authored Aug 25, 2024
1 parent 6cf3be8 commit aac032f
Show file tree
Hide file tree
Showing 24 changed files with 580 additions and 16 deletions.
11 changes: 9 additions & 2 deletions .github/workflows/showpotAlarm-dev-cd.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,19 @@ jobs:
- name: Copy Secrets
uses: microsoft/variable-substitution@v1
with:
files: './app/src/main/resources/application-dev.yml,
./app/domain/common-domain/src/main/resources/application-domain-dev.yml'
files: |
- ./app/src/main/resources/application-dev.yml
- ./app/domain/common-domain/src/main/resources/application-domain-dev.yml
- ./app/src/main/resources/application-fcm-dev.yml
env:
spring.datasource.url: ${{ secrets.APPLICATION_DATASOURCE_URL_DEV }}
spring.datasource.username: ${{ secrets.APPLICATION_DATASOURCE_USERNAME }}
spring.datasource.password: ${{ secrets.APPLICATION_DATASOURCE_PASSWORD }}
fcm.project_id: ${{ secrets.FCM_PROJECT_ID }}
fcm.private_key_id: ${{ secrets.FCM_PRIVATE_KEY_ID }}
fcm.private_key: ${{ secrets.FCM_PRIVATE_KEY }}
fcm.client_email: ${{ secrets.FCM_CLIENT_EMAIL }}
fcm.client_id: ${{ secrets.FCM_CLIENT_ID }}

- name: Build with Gradle Wrapper
run: ./gradlew clean build -Dspring.profiles.active=dev
Expand Down
10 changes: 9 additions & 1 deletion .github/workflows/showpotAlarm-dev-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,19 @@ jobs:
- name: Copy Secrets
uses: microsoft/variable-substitution@v1
with:
files: './app/src/main/resources/application-dev.yml, ./app/domain/common-domain/src/main/resources/application-domain-dev.yml'
files: |
./app/src/main/resources/application-dev.yml,
./app/domain/common-domain/src/main/resources/application-domain-dev.yml,
./app/src/main/resources/application-fcm-dev.yml
env:
spring.datasource.url: ${{ secrets.APPLICATION_DATASOURCE_URL_DEV }}
spring.datasource.username: ${{ secrets.APPLICATION_DATASOURCE_USERNAME }}
spring.datasource.password: ${{ secrets.APPLICATION_DATASOURCE_PASSWORD }}
fcm.project_id: ${{ secrets.FCM_PROJECT_ID }}
fcm.private_key_id: ${{ secrets.FCM_PRIVATE_KEY_ID }}
fcm.private_key: ${{ secrets.FCM_PRIVATE_KEY }}
fcm.client_email: ${{ secrets.FCM_CLIENT_EMAIL }}
fcm.client_id: ${{ secrets.FCM_CLIENT_ID }}

- name: Build with Gradle Wrapper
run: ./gradlew clean build -Dspring.profiles.active=dev
Expand Down
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -179,4 +179,7 @@ gradle-app.setting
### QClass ###
**/src/main/generated/

# End of https://www.toptal.com/developers/gitignore/api/java,intellij+all,macos,gradle
# End of https://www.toptal.com/developers/gitignore/api/java,intellij+all,macos,gradle

**/application-fcm-local.yml
**/application-fcm-test.yml
11 changes: 11 additions & 0 deletions app/batch/src/main/java/org/example/service/MessageService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package org.example.service;

import org.example.service.dto.request.MultipleTargetsMessageBatchRequest;
import org.example.service.dto.request.SingleTargetMessageBatchRequest;

public interface MessageService {

void send(SingleTargetMessageBatchRequest request);

void send(MultipleTargetsMessageBatchRequest request);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package org.example.service.dto.request;

import java.util.List;
import org.example.message.MessageParam;

public record MultipleTargetsMessageBatchRequest(
List<String> fcmTokens,
MessageParam message
) {

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package org.example.service.dto.request;

import lombok.Builder;
import org.example.message.MessageParam;

@Builder
public record SingleTargetMessageBatchRequest(
String fcmToken,
MessageParam message
) {

}
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,14 @@ public class ArtistSubscription extends BaseEntity {
@Column(nullable = false)
private UUID artistId;

@Column(nullable = false)
private String artistName;

@Builder
public ArtistSubscription(String userFcmToken, UUID artistId) {
public ArtistSubscription(String userFcmToken, UUID artistId, String artistName) {
this.userFcmToken = userFcmToken;
this.artistId = artistId;
this.artistName = artistName;
}

public void subscribe() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,14 @@ public class GenreSubscription extends BaseEntity {
@Column(nullable = false)
private UUID genreId;

@Column(nullable = false)
private String genreName;

@Builder
private GenreSubscription(String userFcmToken, UUID genreId) {
private GenreSubscription(String userFcmToken, UUID genreId, String genreName) {
this.userFcmToken = userFcmToken;
this.genreId = genreId;
this.genreName = genreName;
}

public void subscribe() {
Expand Down
2 changes: 2 additions & 0 deletions app/infrastructure/fcm/build.gradle
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
dependencies {
implementation project(':app:batch')

implementation 'com.google.firebase:firebase-admin:9.2.0'
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
package org.showpot.client;

import com.google.firebase.messaging.BatchResponse;
import com.google.firebase.messaging.FirebaseMessaging;
import com.google.firebase.messaging.FirebaseMessagingException;
import com.google.firebase.messaging.Message;
import com.google.firebase.messaging.MulticastMessage;
import com.google.firebase.messaging.Notification;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;

@Component
@RequiredArgsConstructor
@Slf4j
public class FCMClient {

private final FirebaseMessaging firebaseMessaging;

public void sendNotification(String fcmToken, Notification notification) {
try {
firebaseMessaging.send(
Message.builder()
.setToken(fcmToken)
.setNotification(notification)
.build()
);
} catch (FirebaseMessagingException e) {
log.error(
"""
푸시 알림 전송에 실패했습니다.
실패 사유: {}
fcm_token: {}""",
e.getMessagingErrorCode().name(),
fcmToken
);
}
}

public void sendNotification(List<String> fcmTokens, Notification notification) {
if (fcmTokens.size() > 500) {
log.warn("한 번에 요청할 수 있는 푸시 알림의 수는 500개입니다.");
throw new IllegalArgumentException("한 번에 요청할 수 있는 푸시 알림의 수는 500개입니다.");
}

try {
BatchResponse response = firebaseMessaging.sendEachForMulticast(
MulticastMessage.builder()
.addAllTokens(fcmTokens)
.setNotification(notification)
.build()
);

loggingFirebaseMessaging(response);
} catch (FirebaseMessagingException e) {
log.error("푸시 알림 전송에 실패했습니다.", e);
}
}

private void loggingFirebaseMessaging(BatchResponse response) {
log.info(
"""
푸시 알림 전송 결과: 성공 {}, 실패 {}
에러코드별 실패 횟수
{}
""",
response.getSuccessCount(),
response.getFailureCount(),
getCountByErrorCode(response)
);
}

private Map<String, Integer> getCountByErrorCode(BatchResponse response) {
return response.getResponses().stream()
.filter(it -> !it.isSuccessful())
.collect(
Collectors.groupingBy(
it -> it.getException().getMessagingErrorCode().name(),
Collectors.summingInt(it -> 1)
)
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package org.showpot.config;

import com.google.auth.oauth2.GoogleCredentials;
import com.google.firebase.FirebaseApp;
import com.google.firebase.FirebaseOptions;
import com.google.firebase.messaging.FirebaseMessaging;
import java.io.IOException;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.showpot.property.FCMProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;

@Configuration
@EnableConfigurationProperties(FCMProperty.class)
@ComponentScan(basePackages = "org.showpot")
@RequiredArgsConstructor
public class FCMConfig {

private final FCMProperty fcmProperty;

@Bean
public FirebaseMessaging firebaseMessaging() {
FirebaseApp firebaseApp;

try {
firebaseApp = getApp();
} catch (IOException e) {
throw new RuntimeException(e);
}

return FirebaseMessaging.getInstance(firebaseApp);
}

private FirebaseApp getApp() throws IOException {
List<FirebaseApp> apps = FirebaseApp.getApps();

if (apps.isEmpty()) {
return initializeApp();
}

for (FirebaseApp app : apps) {
if (app.getName().equals(FirebaseApp.DEFAULT_APP_NAME)) {
return app;
}
}

return initializeApp();
}

private FirebaseApp initializeApp() throws IOException {
FirebaseOptions options = FirebaseOptions.builder()
.setCredentials(
GoogleCredentials.fromStream(fcmProperty.toInputStream())
)
.build();

return FirebaseApp.initializeApp(options);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package org.showpot.dto.param;

import com.google.firebase.messaging.Notification;
import lombok.Builder;
import org.example.message.MessageParam;

@Builder
public record FCMMessageParam(
String title,
String body
) {

public static FCMMessageParam from(MessageParam param) {
return FCMMessageParam.builder()
.title(param.title())
.body(param.body())
.build();
}

public Notification toNotification() {
return Notification.builder()
.setTitle(title())
.setBody(body())
.build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package org.showpot.property;

import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import org.springframework.boot.context.properties.ConfigurationProperties;

@ConfigurationProperties(prefix = "fcm")
public record FCMProperty(
String type,
@JsonProperty("project_id")
String projectId,
@JsonProperty("private_key_id")
String privateKeyId,
@JsonProperty("private_key")
String privateKey,
@JsonProperty("client_email")
String clientEmail,
@JsonProperty("client_id")
String clientId,
@JsonProperty("auth_uri")
String authUri,
@JsonProperty("token_uri")
String tokenUri,
@JsonProperty("auth_provider_x509_cert_url")
String authProviderX509CertUrl,
@JsonProperty("client_x509_cert_url")
String clientX509CertUrl,
@JsonProperty("universe_domain")
String universeDomain
) {

public InputStream toInputStream() throws IOException {
ObjectMapper objectMapper = new ObjectMapper();
String jsonString = objectMapper.writeValueAsString(this);

return new ByteArrayInputStream(jsonString.getBytes(StandardCharsets.UTF_8));
}
}
Loading

0 comments on commit aac032f

Please sign in to comment.