feat(user-creator-chat): SSE REST 경계를 제거한다
This commit is contained in:
@@ -4,7 +4,6 @@ import kr.co.vividnext.sodalive.common.ApiResponse
|
||||
import kr.co.vividnext.sodalive.common.SodaException
|
||||
import kr.co.vividnext.sodalive.member.Member
|
||||
import kr.co.vividnext.sodalive.v2.usercreatorchat.dto.CreateUserCreatorChatRoomRequest
|
||||
import kr.co.vividnext.sodalive.v2.usercreatorchat.dto.SendUserCreatorTextMessageRequest
|
||||
import kr.co.vividnext.sodalive.v2.usercreatorchat.service.UserCreatorChatService
|
||||
import org.springframework.http.MediaType
|
||||
import org.springframework.security.core.annotation.AuthenticationPrincipal
|
||||
@@ -42,16 +41,6 @@ class UserCreatorChatController(
|
||||
ApiResponse.ok(service.openRoom(member, roomId, limit))
|
||||
}
|
||||
|
||||
@PostMapping("/{roomId}/events/disconnect")
|
||||
fun disconnectRealtime(
|
||||
@AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member?,
|
||||
@PathVariable roomId: Long
|
||||
) = run {
|
||||
if (member == null) throw SodaException(messageKey = "common.error.bad_credentials")
|
||||
service.disconnectRealtime(member, roomId)
|
||||
ApiResponse.ok(true)
|
||||
}
|
||||
|
||||
@GetMapping("/{roomId}/messages")
|
||||
fun getMessages(
|
||||
@AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member?,
|
||||
@@ -63,25 +52,6 @@ class UserCreatorChatController(
|
||||
ApiResponse.ok(service.getMessages(member, roomId, cursor, limit))
|
||||
}
|
||||
|
||||
@GetMapping("/{roomId}/events", produces = [MediaType.TEXT_EVENT_STREAM_VALUE])
|
||||
fun connectEvents(
|
||||
@AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member?,
|
||||
@PathVariable roomId: Long
|
||||
) = run {
|
||||
if (member == null) throw SodaException(messageKey = "common.error.bad_credentials")
|
||||
service.connect(member, roomId)
|
||||
}
|
||||
|
||||
@PostMapping("/{roomId}/messages/text")
|
||||
fun sendTextMessage(
|
||||
@AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member?,
|
||||
@PathVariable roomId: Long,
|
||||
@RequestBody request: SendUserCreatorTextMessageRequest
|
||||
) = run {
|
||||
if (member == null) throw SodaException(messageKey = "common.error.bad_credentials")
|
||||
ApiResponse.ok(service.sendTextMessage(member, roomId, request))
|
||||
}
|
||||
|
||||
@PostMapping("/{roomId}/messages/voice", consumes = [MediaType.MULTIPART_FORM_DATA_VALUE])
|
||||
fun sendVoiceMessage(
|
||||
@AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member?,
|
||||
|
||||
@@ -8,10 +8,6 @@ data class CreateUserCreatorChatRoomResponse(
|
||||
val roomId: Long
|
||||
)
|
||||
|
||||
data class SendUserCreatorTextMessageRequest(
|
||||
val textMessage: String
|
||||
)
|
||||
|
||||
data class SendUserCreatorVoiceMessageRequest(
|
||||
val recipientId: Long? = null
|
||||
)
|
||||
|
||||
@@ -1,87 +0,0 @@
|
||||
package kr.co.vividnext.sodalive.v2.usercreatorchat.service
|
||||
|
||||
import kr.co.vividnext.sodalive.v2.usercreatorchat.dto.UserCreatorChatMessageItemDto
|
||||
import org.springframework.data.redis.core.StringRedisTemplate
|
||||
import org.springframework.stereotype.Service
|
||||
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter
|
||||
import java.io.IOException
|
||||
import java.time.Duration
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
|
||||
@Service
|
||||
class UserCreatorChatRealtimeService(
|
||||
private val stringRedisTemplate: StringRedisTemplate
|
||||
) {
|
||||
private val emitters = ConcurrentHashMap<String, SseEmitter>()
|
||||
|
||||
fun connect(roomId: Long, memberId: Long): SseEmitter {
|
||||
val emitter = SseEmitter(SSE_TIMEOUT_MILLIS)
|
||||
val key = emitterKey(roomId, memberId)
|
||||
emitters[key] = emitter
|
||||
markPresent(roomId, memberId)
|
||||
|
||||
emitter.onCompletion { disconnect(roomId, memberId) }
|
||||
emitter.onTimeout { disconnect(roomId, memberId) }
|
||||
emitter.onError { disconnect(roomId, memberId) }
|
||||
|
||||
sendConnectEvent(emitter)
|
||||
return emitter
|
||||
}
|
||||
|
||||
fun disconnect(roomId: Long, memberId: Long) {
|
||||
emitters.remove(emitterKey(roomId, memberId))
|
||||
stringRedisTemplate.delete(presenceKey(roomId, memberId))
|
||||
}
|
||||
|
||||
fun isMemberInRoom(roomId: Long, memberId: Long): Boolean {
|
||||
return stringRedisTemplate.hasKey(presenceKey(roomId, memberId))
|
||||
}
|
||||
|
||||
fun sendMessage(roomId: Long, memberId: Long, message: UserCreatorChatMessageItemDto): Boolean {
|
||||
val emitter = emitters[emitterKey(roomId, memberId)] ?: return false
|
||||
return try {
|
||||
emitter.send(
|
||||
SseEmitter.event()
|
||||
.id(message.messageId.toString())
|
||||
.name("message")
|
||||
.reconnectTime(SSE_RECONNECT_MILLIS)
|
||||
.data(message)
|
||||
)
|
||||
markPresent(roomId, memberId)
|
||||
true
|
||||
} catch (_: IOException) {
|
||||
disconnect(roomId, memberId)
|
||||
false
|
||||
} catch (_: IllegalStateException) {
|
||||
disconnect(roomId, memberId)
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
private fun sendConnectEvent(emitter: SseEmitter) {
|
||||
try {
|
||||
emitter.send(
|
||||
SseEmitter.event()
|
||||
.name("connected")
|
||||
.reconnectTime(SSE_RECONNECT_MILLIS)
|
||||
.data("connected")
|
||||
)
|
||||
} catch (e: IOException) {
|
||||
emitter.completeWithError(e)
|
||||
}
|
||||
}
|
||||
|
||||
private fun markPresent(roomId: Long, memberId: Long) {
|
||||
stringRedisTemplate.opsForValue().set(presenceKey(roomId, memberId), "1", Duration.ofSeconds(PRESENCE_TTL_SECONDS))
|
||||
}
|
||||
|
||||
private fun emitterKey(roomId: Long, memberId: Long) = "$roomId:$memberId"
|
||||
|
||||
private fun presenceKey(roomId: Long, memberId: Long) = "v2:user-creator-chat:presence:$roomId:$memberId"
|
||||
|
||||
companion object {
|
||||
private const val SSE_TIMEOUT_MILLIS = 30L * 60L * 1000L
|
||||
private const val SSE_RECONNECT_MILLIS = 3000L
|
||||
private const val PRESENCE_TTL_SECONDS = 60L
|
||||
}
|
||||
}
|
||||
@@ -18,7 +18,6 @@ import kr.co.vividnext.sodalive.v2.usercreatorchat.UserCreatorChatParticipant
|
||||
import kr.co.vividnext.sodalive.v2.usercreatorchat.UserCreatorChatRoom
|
||||
import kr.co.vividnext.sodalive.v2.usercreatorchat.dto.CreateUserCreatorChatRoomResponse
|
||||
import kr.co.vividnext.sodalive.v2.usercreatorchat.dto.SendUserCreatorChatMessageResponse
|
||||
import kr.co.vividnext.sodalive.v2.usercreatorchat.dto.SendUserCreatorTextMessageRequest
|
||||
import kr.co.vividnext.sodalive.v2.usercreatorchat.dto.SendUserCreatorVoiceMessageRequest
|
||||
import kr.co.vividnext.sodalive.v2.usercreatorchat.dto.UserCreatorChatMessageItemDto
|
||||
import kr.co.vividnext.sodalive.v2.usercreatorchat.dto.UserCreatorChatMessagesPageResponse
|
||||
@@ -45,7 +44,6 @@ class UserCreatorChatService(
|
||||
private val messageRepository: UserCreatorChatMessageRepository,
|
||||
private val memberRepository: MemberRepository,
|
||||
private val blockMemberRepository: BlockMemberRepository,
|
||||
private val realtimeService: UserCreatorChatRealtimeService,
|
||||
private val presenceService: UserCreatorChatPresenceService,
|
||||
private val roomMessageBroker: UserCreatorChatRoomMessageBroker,
|
||||
private val applicationEventPublisher: ApplicationEventPublisher,
|
||||
@@ -111,18 +109,6 @@ class UserCreatorChatService(
|
||||
)
|
||||
}
|
||||
|
||||
@Transactional
|
||||
fun sendTextMessage(
|
||||
member: Member,
|
||||
roomId: Long,
|
||||
request: SendUserCreatorTextMessageRequest
|
||||
): SendUserCreatorChatMessageResponse {
|
||||
if (request.textMessage.isBlank()) throw SodaException(messageKey = "common.error.invalid_request")
|
||||
val context = resolveSendContext(member, roomId)
|
||||
val message = saveTextMessage(context, request.textMessage)
|
||||
return deliverMessage(message, member, context.opponentParticipant)
|
||||
}
|
||||
|
||||
@Transactional
|
||||
fun sendTextMessageByWebSocket(memberId: Long, roomId: Long, textMessage: String): UserCreatorChatMessageItemDto {
|
||||
if (textMessage.isBlank()) throw SodaException(messageKey = "common.error.invalid_request")
|
||||
@@ -169,17 +155,7 @@ class UserCreatorChatService(
|
||||
filePath = "user_creator_chat_voice/${message.id}/${generateFileName(prefix = "${message.id}-message-")}",
|
||||
metadata = metadata
|
||||
)
|
||||
return deliverMessage(message, member, context.opponentParticipant)
|
||||
}
|
||||
|
||||
fun connect(member: Member, roomId: Long) = run {
|
||||
requireParticipant(roomId, member.id!!)
|
||||
realtimeService.connect(roomId, member.id!!)
|
||||
}
|
||||
|
||||
fun disconnectRealtime(member: Member, roomId: Long) {
|
||||
requireParticipant(roomId, member.id!!)
|
||||
realtimeService.disconnect(roomId, member.id!!)
|
||||
return deliverRestMessage(message, member, context.opponentParticipant)
|
||||
}
|
||||
|
||||
fun validateParticipant(roomId: Long, memberId: Long): UserCreatorChatParticipant {
|
||||
@@ -206,17 +182,26 @@ class UserCreatorChatService(
|
||||
return SendContext(room, senderParticipant, opponentParticipant)
|
||||
}
|
||||
|
||||
private fun deliverMessage(
|
||||
private fun deliverRestMessage(
|
||||
message: UserCreatorChatMessage,
|
||||
member: Member,
|
||||
opponentParticipant: UserCreatorChatParticipant
|
||||
): SendUserCreatorChatMessageResponse {
|
||||
val opponent = opponentParticipant.member
|
||||
val item = toMessageItemDto(message, member)
|
||||
val opponentPresent = realtimeService.isMemberInRoom(message.chatRoom.id!!, opponent.id!!)
|
||||
val opponentPresent = presenceService.hasPresence(message.chatRoom.id!!, opponent.id!!)
|
||||
if (opponentPresent) {
|
||||
val delivered = realtimeService.sendMessage(message.chatRoom.id!!, opponent.id!!, item)
|
||||
return SendUserCreatorChatMessageResponse(message = item, deliveredRealtime = delivered, pushSent = false)
|
||||
val opponentMessage = toMessageItemDto(message, opponent)
|
||||
roomMessageBroker.publish(
|
||||
roomId = message.chatRoom.id!!,
|
||||
memberId = opponent.id!!,
|
||||
payload = websocketMessagePayload(
|
||||
UserCreatorChatWebSocketMessageType.MESSAGE,
|
||||
message.chatRoom.id!!,
|
||||
opponentMessage
|
||||
)
|
||||
)
|
||||
return SendUserCreatorChatMessageResponse(message = item, deliveredRealtime = true, pushSent = false)
|
||||
}
|
||||
|
||||
publishMessagePush(message, member, opponent)
|
||||
|
||||
Reference in New Issue
Block a user