diff --git a/build.gradle.kts b/build.gradle.kts index dfb9490..84666cf 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -55,6 +55,9 @@ dependencies { implementation("org.json:json:20230227") implementation("com.google.code.findbugs:jsr305:3.0.2") + // firebase admin sdk + implementation("com.google.firebase:firebase-admin:9.2.0") + developmentOnly("org.springframework.boot:spring-boot-devtools") runtimeOnly("com.h2database:h2") runtimeOnly("com.mysql:mysql-connector-j") diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/configs/FirebaseConfig.kt b/src/main/kotlin/kr/co/vividnext/sodalive/configs/FirebaseConfig.kt new file mode 100644 index 0000000..abf3132 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/configs/FirebaseConfig.kt @@ -0,0 +1,25 @@ +package kr.co.vividnext.sodalive.configs + +import com.google.auth.oauth2.GoogleCredentials +import com.google.firebase.FirebaseApp +import com.google.firebase.FirebaseOptions +import org.springframework.beans.factory.annotation.Value +import org.springframework.context.annotation.Configuration +import java.io.FileInputStream +import javax.annotation.PostConstruct + +@Configuration +class FirebaseConfig( + @Value("\${firebase.secret-key-path}") + private val secretKeyPath: String +) { + + @PostConstruct + fun initialize() { + FirebaseOptions.builder() + .setCredentials(GoogleCredentials.fromStream(FileInputStream(secretKeyPath))) + .build() + + FirebaseApp.initializeApp() + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/content/AudioContentService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/content/AudioContentService.kt index 1bceec0..acaf24c 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/content/AudioContentService.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/content/AudioContentService.kt @@ -17,10 +17,13 @@ import kr.co.vividnext.sodalive.content.order.OrderRepository import kr.co.vividnext.sodalive.content.order.OrderType import kr.co.vividnext.sodalive.content.theme.AudioContentThemeQueryRepository import kr.co.vividnext.sodalive.explorer.ExplorerQueryRepository +import kr.co.vividnext.sodalive.fcm.FcmEvent +import kr.co.vividnext.sodalive.fcm.FcmEventType import kr.co.vividnext.sodalive.member.Member import kr.co.vividnext.sodalive.member.block.BlockMemberRepository import kr.co.vividnext.sodalive.utils.generateFileName import org.springframework.beans.factory.annotation.Value +import org.springframework.context.ApplicationEventPublisher import org.springframework.data.repository.findByIdOrNull import org.springframework.stereotype.Service import org.springframework.transaction.annotation.Transactional @@ -45,6 +48,7 @@ class AudioContentService( private val s3Uploader: S3Uploader, private val objectMapper: ObjectMapper, private val audioContentCloudFront: AudioContentCloudFront, + private val applicationEventPublisher: ApplicationEventPublisher, @Value("\${cloud.aws.s3.content-bucket}") private val audioContentBucket: String, @@ -260,6 +264,17 @@ class AudioContentService( audioContent.isActive = true audioContent.content = content audioContent.duration = duration + + applicationEventPublisher.publishEvent( + FcmEvent( + type = FcmEventType.UPLOAD_CONTENT, + title = audioContent.member!!.nickname, + message = "콘텐츠를 업로드 하였습니다. - ${audioContent.title}", + isAuth = audioContent.isAdult, + contentId = contentId, + creatorId = audioContent.member!!.id + ) + ) } fun getDetail(id: Long, member: Member, timezone: String): GetAudioContentDetailResponse { diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/fcm/FcmController.kt b/src/main/kotlin/kr/co/vividnext/sodalive/fcm/FcmController.kt new file mode 100644 index 0000000..a35b138 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/fcm/FcmController.kt @@ -0,0 +1,47 @@ +package kr.co.vividnext.sodalive.fcm + +import org.springframework.context.ApplicationEventPublisher +import org.springframework.security.access.prepost.PreAuthorize +import org.springframework.web.bind.annotation.PostMapping +import org.springframework.web.bind.annotation.RequestBody +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RestController + +@RestController +@RequestMapping("/push") +@PreAuthorize("hasRole('ADMIN')") +class FcmController(private val applicationEventPublisher: ApplicationEventPublisher) { + @PostMapping + fun send( + @RequestBody request: PushRequest + ) = run { + if (request.memberIds.isNotEmpty()) { + applicationEventPublisher.publishEvent( + FcmEvent( + type = FcmEventType.INDIVIDUAL, + title = request.title, + message = request.message, + recipients = request.memberIds + ) + ) + } else { + applicationEventPublisher.publishEvent( + FcmEvent( + type = FcmEventType.ALL, + title = request.title, + message = request.message, + container = "ios" + ) + ) + + applicationEventPublisher.publishEvent( + FcmEvent( + type = FcmEventType.ALL, + title = request.title, + message = request.message, + container = "aos" + ) + ) + } + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/fcm/FcmEvent.kt b/src/main/kotlin/kr/co/vividnext/sodalive/fcm/FcmEvent.kt new file mode 100644 index 0000000..9707f86 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/fcm/FcmEvent.kt @@ -0,0 +1,137 @@ +package kr.co.vividnext.sodalive.fcm + +import kr.co.vividnext.sodalive.member.MemberRepository +import org.springframework.context.event.EventListener +import org.springframework.stereotype.Component + +enum class FcmEventType { + ALL, INDIVIDUAL, CREATE_LIVE, START_LIVE, UPLOAD_CONTENT, SEND_MESSAGE +} + +class FcmEvent( + val type: FcmEventType, + val title: String, + val message: String, + val container: String = "", + val recipients: List = listOf(), + val isAuth: Boolean = false, + val roomId: Long? = null, + val contentId: Long? = null, + val messageId: Long? = null, + val creatorId: Long? = null +) + +@Component +class FcmSendListener( + private val pushService: FcmService, + private val memberRepository: MemberRepository +) { + @EventListener + fun send(fcmEvent: FcmEvent) { + when (fcmEvent.type) { + FcmEventType.ALL -> { + if (fcmEvent.container.isNotBlank()) { + val pushTokens = memberRepository.getAllRecipientPushTokens( + fcmEvent.isAuth, + fcmEvent.container + ) + + for (tokens in pushTokens) { + pushService.send( + tokens = tokens, + title = fcmEvent.title, + message = fcmEvent.message, + container = fcmEvent.container + ) + } + } + } + + FcmEventType.INDIVIDUAL -> { + if (fcmEvent.recipients.isNotEmpty()) { + val pushTokens = memberRepository.getIndividualRecipientPushTokens( + recipients = fcmEvent.recipients, + isAuth = fcmEvent.isAuth + ) + + val iosPushTokens = pushTokens["ios"] + val aosPushToken = pushTokens["aos"] + + if (iosPushTokens != null) { + for (tokens in iosPushTokens) { + pushService.send( + tokens = tokens, + title = fcmEvent.title, + message = fcmEvent.message, + container = fcmEvent.container + ) + } + } + + if (aosPushToken != null) { + for (tokens in aosPushToken) { + pushService.send( + tokens = tokens, + title = fcmEvent.title, + message = fcmEvent.message, + container = fcmEvent.container + ) + } + } + } + } + + FcmEventType.CREATE_LIVE, FcmEventType.START_LIVE -> { + if (fcmEvent.container.isNotBlank()) { + val pushTokens = memberRepository.getCreateLiveRoomNotificationRecipientPushTokens( + creatorId = fcmEvent.creatorId!!, + isAuth = fcmEvent.isAuth, + container = fcmEvent.container + ) + + for (tokens in pushTokens) { + pushService.send( + tokens = tokens, + title = fcmEvent.title, + message = fcmEvent.message, + container = fcmEvent.container, + roomId = fcmEvent.roomId + ) + } + } + } + + FcmEventType.UPLOAD_CONTENT -> { + if (fcmEvent.container.isNotBlank()) { + val pushTokens = memberRepository.getUploadContentNotificationRecipientPushTokens( + creatorId = fcmEvent.creatorId!!, + isAuth = fcmEvent.isAuth, + container = fcmEvent.container + ) + + for (tokens in pushTokens) { + pushService.send( + tokens = tokens, + title = fcmEvent.title, + message = fcmEvent.message, + container = fcmEvent.container, + contentId = fcmEvent.contentId + ) + } + } + } + + FcmEventType.SEND_MESSAGE -> { + val response = memberRepository.getMessageRecipientPushToken(messageId = fcmEvent.messageId!!) + + pushService.send( + tokens = listOf(response.pushToken), + title = fcmEvent.title, + message = fcmEvent.message, + container = response.container, + messageId = fcmEvent.messageId + ) + } + } + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/fcm/FcmService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/fcm/FcmService.kt new file mode 100644 index 0000000..7a678fa --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/fcm/FcmService.kt @@ -0,0 +1,39 @@ +package kr.co.vividnext.sodalive.fcm + +import com.google.firebase.messaging.FirebaseMessaging +import com.google.firebase.messaging.MulticastMessage +import org.springframework.scheduling.annotation.Async +import org.springframework.stereotype.Service + +@Service +class FcmService { + @Async + fun send( + tokens: List, + title: String, + message: String, + container: String, + roomId: Long? = null, + messageId: Long? = null, + contentId: Long? = null + ) { + val multicastMessage = MulticastMessage.builder() + .putData("title", title) + .putData("message", message) + .addAllTokens(tokens) + + if (roomId != null) { + multicastMessage.putData("room_id", roomId.toString()) + } + + if (messageId != null) { + multicastMessage.putData("message_id", messageId.toString()) + } + + if (contentId != null) { + multicastMessage.putData("content_id", contentId.toString()) + } + + FirebaseMessaging.getInstance().sendEachForMulticast(multicastMessage.build()) + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/fcm/GetMessageRecipientPushTokenResponse.kt b/src/main/kotlin/kr/co/vividnext/sodalive/fcm/GetMessageRecipientPushTokenResponse.kt new file mode 100644 index 0000000..e20dd2e --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/fcm/GetMessageRecipientPushTokenResponse.kt @@ -0,0 +1,8 @@ +package kr.co.vividnext.sodalive.fcm + +import com.querydsl.core.annotations.QueryProjection + +data class GetMessageRecipientPushTokenResponse @QueryProjection constructor( + val pushToken: String, + val container: String +) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/fcm/PushRequest.kt b/src/main/kotlin/kr/co/vividnext/sodalive/fcm/PushRequest.kt new file mode 100644 index 0000000..c0966b4 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/fcm/PushRequest.kt @@ -0,0 +1,7 @@ +package kr.co.vividnext.sodalive.fcm + +data class PushRequest( + val memberIds: List, + val title: String, + val message: String +) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/live/room/LiveRoomService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/LiveRoomService.kt index 171c8a3..50d6a21 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/live/room/LiveRoomService.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/LiveRoomService.kt @@ -19,6 +19,8 @@ import kr.co.vividnext.sodalive.can.use.UseCanCalculateStatus import kr.co.vividnext.sodalive.common.SodaException import kr.co.vividnext.sodalive.explorer.ExplorerQueryRepository import kr.co.vividnext.sodalive.extensions.convertLocalDateTime +import kr.co.vividnext.sodalive.fcm.FcmEvent +import kr.co.vividnext.sodalive.fcm.FcmEventType import kr.co.vividnext.sodalive.live.reservation.LiveReservationRepository import kr.co.vividnext.sodalive.live.room.cancel.CancelLiveRequest import kr.co.vividnext.sodalive.live.room.cancel.LiveRoomCancel @@ -45,6 +47,7 @@ import kr.co.vividnext.sodalive.member.MemberRole import kr.co.vividnext.sodalive.member.block.BlockMemberRepository import kr.co.vividnext.sodalive.utils.generateFileName import org.springframework.beans.factory.annotation.Value +import org.springframework.context.ApplicationEventPublisher import org.springframework.data.domain.Pageable import org.springframework.data.repository.findByIdOrNull import org.springframework.stereotype.Service @@ -66,6 +69,7 @@ class LiveRoomService( private val kickOutService: LiveRoomKickOutService, private val blockMemberRepository: BlockMemberRepository, + private val applicationEventPublisher: ApplicationEventPublisher, private val useCanCalculateRepository: UseCanCalculateRepository, private val reservationRepository: LiveReservationRepository, private val explorerQueryRepository: ExplorerQueryRepository, @@ -231,6 +235,21 @@ class LiveRoomService( room.bgImage = request.coverImageUrl } + applicationEventPublisher.publishEvent( + FcmEvent( + type = FcmEventType.CREATE_LIVE, + title = createdRoom.member!!.nickname, + message = if (createdRoom.channelName != null) { + "라이브를 시작했습니다. - ${createdRoom.title}" + } else { + "라이브를 개설했습니다. - ${createdRoom.title}" + }, + isAuth = createdRoom.isAdult, + roomId = createdRoom.id, + creatorId = createdRoom.member!!.id + ) + ) + return CreateLiveRoomResponse(createdRoom.id, createdRoom.channelName) } @@ -351,6 +370,17 @@ class LiveRoomService( room.channelName = "SODA_LIVE_CHANNEL_" + "${member.id}_${dateTime.year}_${dateTime.month}_${dateTime.dayOfMonth}_" + "${dateTime.hour}_${dateTime.minute}" + + applicationEventPublisher.publishEvent( + FcmEvent( + type = FcmEventType.START_LIVE, + title = room.member!!.nickname, + message = "라이브를 시작했습니다 - ${room.title}", + isAuth = room.isAdult, + roomId = room.id, + creatorId = room.member!!.id + ) + ) } @Transactional diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/member/MemberRepository.kt b/src/main/kotlin/kr/co/vividnext/sodalive/member/MemberRepository.kt index 6eaa3fd..9561f1d 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/member/MemberRepository.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/member/MemberRepository.kt @@ -1,7 +1,14 @@ package kr.co.vividnext.sodalive.member import com.querydsl.jpa.impl.JPAQueryFactory +import kr.co.vividnext.sodalive.fcm.GetMessageRecipientPushTokenResponse +import kr.co.vividnext.sodalive.fcm.QGetMessageRecipientPushTokenResponse import kr.co.vividnext.sodalive.member.QMember.member +import kr.co.vividnext.sodalive.member.auth.QAuth.auth +import kr.co.vividnext.sodalive.member.block.BlockMemberRepository +import kr.co.vividnext.sodalive.member.following.QCreatorFollowing.creatorFollowing +import kr.co.vividnext.sodalive.member.notification.QMemberNotification.memberNotification +import kr.co.vividnext.sodalive.message.QMessage.message import org.springframework.data.jpa.repository.JpaRepository import org.springframework.stereotype.Repository @@ -15,10 +22,28 @@ interface MemberQueryRepository { fun findByPushToken(pushToken: String): List fun findByNicknameAndOtherCondition(nickname: String, memberId: Long): List fun findCreatorByIdOrNull(memberId: Long): Member? + fun getAllRecipientPushTokens(isAuth: Boolean, container: String): List> + fun getCreateLiveRoomNotificationRecipientPushTokens( + creatorId: Long, + isAuth: Boolean, + container: String + ): List> + + fun getUploadContentNotificationRecipientPushTokens( + creatorId: Long, + isAuth: Boolean, + container: String + ): List> + + fun getMessageRecipientPushToken(messageId: Long): GetMessageRecipientPushTokenResponse + fun getIndividualRecipientPushTokens(recipients: List, isAuth: Boolean): Map>> } @Repository -class MemberQueryRepositoryImpl(private val queryFactory: JPAQueryFactory) : MemberQueryRepository { +class MemberQueryRepositoryImpl( + private val queryFactory: JPAQueryFactory, + private val blockMemberRepository: BlockMemberRepository +) : MemberQueryRepository { override fun findByPushToken(pushToken: String): List { return queryFactory .selectFrom(member) @@ -47,4 +72,141 @@ class MemberQueryRepositoryImpl(private val queryFactory: JPAQueryFactory) : Mem ) .fetchFirst() } + + override fun getAllRecipientPushTokens(isAuth: Boolean, container: String): List> { + var where = member.isActive.isTrue + .and(member.email.notIn("admin@sodalive.net")) + .and(member.container.eq(container)) + + if (isAuth) { + where = where.and(member.auth.isNotNull) + } + + return queryFactory + .select(member.pushToken) + .from(member) + .leftJoin(member.auth, auth) + .where(where) + .fetch() + .toSet() + .chunked(500) + } + + override fun getCreateLiveRoomNotificationRecipientPushTokens( + creatorId: Long, + isAuth: Boolean, + container: String + ): List> { + val member = QMember.member + val creator = QMember.member + + var where = creatorFollowing.isActive.isTrue + .and(creator.id.eq(creatorId)) + .and(member.email.notIn("admin@sodalive.net")) + .and(member.container.eq(container)) + .and(memberNotification.live.isTrue) + .and( + member.id.notIn( + blockMemberRepository.getBlockedMemberList(creatorId) + ) + ) + + if (isAuth) { + where = where.and(member.auth.isNotNull) + } + + return queryFactory + .select(member.pushToken) + .from(creatorFollowing) + .innerJoin(creatorFollowing.creator, creator) + .innerJoin(creatorFollowing.member, member) + .innerJoin(member.notification, memberNotification) + .leftJoin(member.auth, auth) + .where(where) + .fetch() + .toSet() + .chunked(500) + } + + override fun getUploadContentNotificationRecipientPushTokens( + creatorId: Long, + isAuth: Boolean, + container: String + ): List> { + val member = QMember.member + val creator = QMember.member + + var where = creatorFollowing.isActive.isTrue + .and(creator.id.eq(creatorId)) + .and(member.email.notIn("admin@sodalive.net")) + .and(member.container.eq(container)) + .and(memberNotification.uploadContent.isTrue) + .and( + member.id.notIn( + blockMemberRepository.getBlockedMemberList(creatorId) + ) + ) + + if (isAuth) { + where = where.and(member.auth.isNotNull) + } + + return queryFactory + .select(member.pushToken) + .from(creatorFollowing) + .innerJoin(creatorFollowing.creator, creator) + .innerJoin(creatorFollowing.member, member) + .innerJoin(member.notification, memberNotification) + .leftJoin(member.auth, auth) + .where(where) + .fetch() + .toSet() + .chunked(500) + } + + override fun getMessageRecipientPushToken(messageId: Long): GetMessageRecipientPushTokenResponse { + return queryFactory + .select( + QGetMessageRecipientPushTokenResponse( + member.pushToken, + member.container + ) + ) + .from(message) + .innerJoin(message.recipient, member) + .where(message.id.eq(messageId)) + .fetchFirst() + } + + override fun getIndividualRecipientPushTokens( + recipients: List, + isAuth: Boolean + ): Map>> { + var where = member.isActive.isTrue + .and(member.email.notIn("admin@sodalive.net")) + + if (isAuth) { + where = where.and(member.auth.isNotNull) + } + + val aosPushTokens = queryFactory + .select(member.pushToken) + .from(member) + .leftJoin(member.auth, auth) + .where(where.and(member.container.eq("aos"))) + .fetch() + .toSet() + .chunked(500) + + val iosPushTokens = queryFactory + .select(member.pushToken) + .from(member) + .leftJoin(member.auth, auth) + .where(where.and(member.container.eq("ios"))) + .fetch() + .toSet() + .chunked(500) + + return mapOf("aos" to aosPushTokens, "ios" to iosPushTokens) + } } diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/member/block/BlockMemberRepository.kt b/src/main/kotlin/kr/co/vividnext/sodalive/member/block/BlockMemberRepository.kt index 29eb50e..f81572d 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/member/block/BlockMemberRepository.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/member/block/BlockMemberRepository.kt @@ -11,6 +11,7 @@ interface BlockMemberRepository : JpaRepository, BlockMemberQ interface BlockMemberQueryRepository { fun getBlockAccount(blockedMemberId: Long, memberId: Long): BlockMember? fun isBlocked(blockedMemberId: Long, memberId: Long): Boolean + fun getBlockedMemberList(creatorId: Long): List } @Repository @@ -39,4 +40,15 @@ class BlockMemberQueryRepositoryImpl(private val queryFactory: JPAQueryFactory) return blockedAccount != null } + + override fun getBlockedMemberList(creatorId: Long): List { + return queryFactory + .select(blockMember.blockedMemberId) + .from(blockMember) + .where( + blockMember.memberId.eq(creatorId) + .and(blockMember.isActive.isTrue) + ) + .fetch() + } } diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/message/MessageService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/message/MessageService.kt index a6e3ec5..07f22c8 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/message/MessageService.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/message/MessageService.kt @@ -4,11 +4,14 @@ import com.amazonaws.services.s3.model.ObjectMetadata import com.fasterxml.jackson.databind.ObjectMapper import kr.co.vividnext.sodalive.aws.s3.S3Uploader import kr.co.vividnext.sodalive.common.SodaException +import kr.co.vividnext.sodalive.fcm.FcmEvent +import kr.co.vividnext.sodalive.fcm.FcmEventType import kr.co.vividnext.sodalive.member.Member import kr.co.vividnext.sodalive.member.MemberRepository import kr.co.vividnext.sodalive.member.block.BlockMemberRepository import kr.co.vividnext.sodalive.utils.generateFileName import org.springframework.beans.factory.annotation.Value +import org.springframework.context.ApplicationEventPublisher import org.springframework.data.domain.Pageable import org.springframework.data.repository.findByIdOrNull import org.springframework.stereotype.Service @@ -24,6 +27,7 @@ class MessageService( private val memberRepository: MemberRepository, private val blockMemberRepository: BlockMemberRepository, + private val applicationEventPublisher: ApplicationEventPublisher, private val objectMapper: ObjectMapper, private val s3Uploader: S3Uploader, @@ -56,6 +60,15 @@ class MessageService( message.recipient = recipient repository.save(message) + + applicationEventPublisher.publishEvent( + FcmEvent( + type = FcmEventType.SEND_MESSAGE, + title = "메시지", + message = "${sender.nickname}님으로 부터 문자메시지가 도착했습니다.", + messageId = message.id + ) + ) } fun getSentTextMessages(member: Member, pageable: Pageable, timezone: String): GetTextMessageResponse { @@ -115,6 +128,15 @@ class MessageService( ) message.voiceMessage = messagePath + + applicationEventPublisher.publishEvent( + FcmEvent( + type = FcmEventType.SEND_MESSAGE, + title = "메시지", + message = "${sender.nickname}님으로 부터 음성메시지가 도착했습니다.", + messageId = message.id + ) + ) } fun getSentVoiceMessages(member: Member, pageable: Pageable, timezone: String): GetVoiceMessageResponse { diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index c33f8dc..1941134 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -20,6 +20,9 @@ agora: appId: ${AGORA_APP_ID} appCertificate: ${AGORA_APP_CERTIFICATE} +firebase: + secretKeyPath: ${GOOGLE_APPLICATION_CREDENTIALS} + cloud: aws: credentials: