feat(user-creator-chat): WebSocket room handler를 구현한다
This commit is contained in:
@@ -26,6 +26,9 @@ import kr.co.vividnext.sodalive.v2.usercreatorchat.dto.UserCreatorChatRoomOpenRe
|
||||
import kr.co.vividnext.sodalive.v2.usercreatorchat.repository.UserCreatorChatMessageRepository
|
||||
import kr.co.vividnext.sodalive.v2.usercreatorchat.repository.UserCreatorChatParticipantRepository
|
||||
import kr.co.vividnext.sodalive.v2.usercreatorchat.repository.UserCreatorChatRoomRepository
|
||||
import kr.co.vividnext.sodalive.v2.usercreatorchat.websocket.UserCreatorChatPresenceService
|
||||
import kr.co.vividnext.sodalive.v2.usercreatorchat.websocket.UserCreatorChatRoomMessageBroker
|
||||
import kr.co.vividnext.sodalive.v2.usercreatorchat.websocket.UserCreatorChatWebSocketMessageType
|
||||
import org.springframework.beans.factory.annotation.Value
|
||||
import org.springframework.context.ApplicationEventPublisher
|
||||
import org.springframework.data.domain.PageRequest
|
||||
@@ -43,6 +46,8 @@ class UserCreatorChatService(
|
||||
private val memberRepository: MemberRepository,
|
||||
private val blockMemberRepository: BlockMemberRepository,
|
||||
private val realtimeService: UserCreatorChatRealtimeService,
|
||||
private val presenceService: UserCreatorChatPresenceService,
|
||||
private val roomMessageBroker: UserCreatorChatRoomMessageBroker,
|
||||
private val applicationEventPublisher: ApplicationEventPublisher,
|
||||
private val objectMapper: ObjectMapper,
|
||||
private val s3Uploader: S3Uploader,
|
||||
@@ -114,17 +119,30 @@ class UserCreatorChatService(
|
||||
): SendUserCreatorChatMessageResponse {
|
||||
if (request.textMessage.isBlank()) throw SodaException(messageKey = "common.error.invalid_request")
|
||||
val context = resolveSendContext(member, roomId)
|
||||
val message = messageRepository.save(
|
||||
UserCreatorChatMessage(
|
||||
chatRoom = context.room,
|
||||
participant = context.senderParticipant,
|
||||
messageType = UserCreatorChatMessageType.TEXT,
|
||||
textMessage = request.textMessage
|
||||
)
|
||||
)
|
||||
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")
|
||||
val senderParticipant = validateParticipant(roomId, memberId)
|
||||
val sender = senderParticipant.member
|
||||
val context = resolveSendContext(sender, roomId)
|
||||
val message = saveTextMessage(context, textMessage)
|
||||
val senderMessage = toMessageItemDto(message, sender)
|
||||
val opponent = context.opponentParticipant.member
|
||||
if (presenceService.hasPresence(roomId, opponent.id!!)) {
|
||||
val opponentMessage = toMessageItemDto(message, opponent)
|
||||
roomMessageBroker.publish(
|
||||
roomId = roomId,
|
||||
memberId = opponent.id!!,
|
||||
payload = websocketMessagePayload(UserCreatorChatWebSocketMessageType.MESSAGE, roomId, opponentMessage)
|
||||
)
|
||||
}
|
||||
return senderMessage
|
||||
}
|
||||
|
||||
@Transactional
|
||||
fun sendVoiceMessage(
|
||||
member: Member,
|
||||
@@ -162,6 +180,21 @@ class UserCreatorChatService(
|
||||
realtimeService.disconnect(roomId, member.id!!)
|
||||
}
|
||||
|
||||
fun validateParticipant(roomId: Long, memberId: Long): UserCreatorChatParticipant {
|
||||
return requireParticipant(roomId, memberId)
|
||||
}
|
||||
|
||||
private fun saveTextMessage(context: SendContext, textMessage: String): UserCreatorChatMessage {
|
||||
return messageRepository.save(
|
||||
UserCreatorChatMessage(
|
||||
chatRoom = context.room,
|
||||
participant = context.senderParticipant,
|
||||
messageType = UserCreatorChatMessageType.TEXT,
|
||||
textMessage = textMessage
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private fun resolveSendContext(member: Member, roomId: Long): SendContext {
|
||||
val room = findRoom(roomId)
|
||||
val senderParticipant = requireParticipant(roomId, member.id!!)
|
||||
@@ -208,6 +241,21 @@ class UserCreatorChatService(
|
||||
)
|
||||
}
|
||||
|
||||
private fun websocketMessagePayload(
|
||||
type: UserCreatorChatWebSocketMessageType,
|
||||
roomId: Long,
|
||||
payload: UserCreatorChatMessageItemDto
|
||||
): String {
|
||||
return objectMapper.writeValueAsString(
|
||||
mapOf(
|
||||
"type" to type,
|
||||
"requestId" to null,
|
||||
"roomId" to roomId,
|
||||
"payload" to payload
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private fun validateRecipient(sender: Member, recipient: Member) {
|
||||
if (!recipient.isActive) throw SodaException(messageKey = "message.error.recipient_inactive")
|
||||
if (recipient.memberKind == MemberKind.AI_CHARACTER) {
|
||||
|
||||
@@ -1,7 +1,117 @@
|
||||
package kr.co.vividnext.sodalive.v2.usercreatorchat.websocket
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper
|
||||
import kr.co.vividnext.sodalive.common.SodaException
|
||||
import kr.co.vividnext.sodalive.v2.usercreatorchat.service.UserCreatorChatService
|
||||
import org.springframework.stereotype.Component
|
||||
import org.springframework.web.socket.CloseStatus
|
||||
import org.springframework.web.socket.TextMessage
|
||||
import org.springframework.web.socket.WebSocketSession
|
||||
import org.springframework.web.socket.handler.TextWebSocketHandler
|
||||
|
||||
@Component
|
||||
class UserCreatorChatWebSocketHandler : TextWebSocketHandler()
|
||||
class UserCreatorChatWebSocketHandler(
|
||||
private val service: UserCreatorChatService,
|
||||
private val presenceService: UserCreatorChatPresenceService,
|
||||
private val sessionRegistry: UserCreatorChatWebSocketSessionRegistry,
|
||||
private val objectMapper: ObjectMapper
|
||||
) : TextWebSocketHandler() {
|
||||
override fun handleTextMessage(session: WebSocketSession, message: TextMessage) {
|
||||
val request = objectMapper.readValue(message.payload, UserCreatorChatWebSocketMessage::class.java)
|
||||
when (request.type) {
|
||||
UserCreatorChatWebSocketMessageType.JOIN_ROOM -> handleJoinRoom(session, request)
|
||||
UserCreatorChatWebSocketMessageType.SEND_TEXT -> handleSendText(session, request)
|
||||
else -> sendError(session, request, "common.error.invalid_request")
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleJoinRoom(session: WebSocketSession, request: UserCreatorChatWebSocketMessage) {
|
||||
try {
|
||||
val memberId = memberId(session)
|
||||
service.validateParticipant(roomId = request.roomId, memberId = memberId)
|
||||
clearJoinedRoom(session)
|
||||
sessionRegistry.register(roomId = request.roomId, memberId = memberId, session = session)
|
||||
presenceService.markJoined(roomId = request.roomId, memberId = memberId, sessionId = session.id)
|
||||
session.attributes[JOINED_ROOM_ID_ATTRIBUTE] = request.roomId
|
||||
sendEnvelope(
|
||||
session,
|
||||
UserCreatorChatWebSocketMessageType.JOINED,
|
||||
request.requestId,
|
||||
request.roomId,
|
||||
emptyMap<String, Any>()
|
||||
)
|
||||
} catch (e: SodaException) {
|
||||
sendError(session, request, e.messageKey ?: "common.error.invalid_request")
|
||||
session.close(CloseStatus.POLICY_VIOLATION)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleSendText(session: WebSocketSession, request: UserCreatorChatWebSocketMessage) {
|
||||
try {
|
||||
if (session.attributes[JOINED_ROOM_ID_ATTRIBUTE] != request.roomId) {
|
||||
throw SodaException(messageKey = "chat.room.join_required")
|
||||
}
|
||||
val textMessage = request.payload["textMessage"]?.asText().orEmpty()
|
||||
val message = service.sendTextMessageByWebSocket(
|
||||
memberId = memberId(session),
|
||||
roomId = request.roomId,
|
||||
textMessage = textMessage
|
||||
)
|
||||
sendEnvelope(session, UserCreatorChatWebSocketMessageType.SEND_ACK, request.requestId, request.roomId, message)
|
||||
} catch (e: SodaException) {
|
||||
sendError(session, request, e.messageKey ?: "common.error.invalid_request")
|
||||
}
|
||||
}
|
||||
|
||||
override fun afterConnectionClosed(session: WebSocketSession, status: CloseStatus) {
|
||||
clearJoinedRoom(session)
|
||||
}
|
||||
|
||||
private fun memberId(session: WebSocketSession): Long {
|
||||
return session.attributes[UserCreatorChatWebSocketAuthInterceptor.MEMBER_ID_ATTRIBUTE] as? Long
|
||||
?: throw SodaException(messageKey = "common.error.bad_credentials")
|
||||
}
|
||||
|
||||
private fun clearJoinedRoom(session: WebSocketSession) {
|
||||
val roomId = session.attributes.remove(JOINED_ROOM_ID_ATTRIBUTE) as? Long
|
||||
if (roomId != null) {
|
||||
presenceService.markLeft(roomId = roomId, memberId = memberId(session), sessionId = session.id)
|
||||
}
|
||||
sessionRegistry.remove(session.id)
|
||||
}
|
||||
|
||||
private fun sendError(session: WebSocketSession, request: UserCreatorChatWebSocketMessage, messageKey: String) {
|
||||
sendEnvelope(
|
||||
session,
|
||||
UserCreatorChatWebSocketMessageType.ERROR,
|
||||
request.requestId,
|
||||
request.roomId,
|
||||
mapOf("messageKey" to messageKey)
|
||||
)
|
||||
}
|
||||
|
||||
private fun sendEnvelope(
|
||||
session: WebSocketSession,
|
||||
type: UserCreatorChatWebSocketMessageType,
|
||||
requestId: String?,
|
||||
roomId: Long,
|
||||
payload: Any
|
||||
) {
|
||||
session.sendMessage(
|
||||
TextMessage(
|
||||
objectMapper.writeValueAsString(
|
||||
mapOf(
|
||||
"type" to type,
|
||||
"requestId" to requestId,
|
||||
"roomId" to roomId,
|
||||
"payload" to payload
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val JOINED_ROOM_ID_ATTRIBUTE = "userCreatorChatJoinedRoomId"
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user