feat(dm): WebSocket envelope 계약을 보정한다
This commit is contained in:
+37
-3
@@ -2,6 +2,9 @@ package kr.co.vividnext.sodalive.v2.main.chat.dm.data
|
||||
|
||||
import io.reactivex.rxjava3.core.Single
|
||||
import kr.co.vividnext.sodalive.common.ApiResponse
|
||||
import okhttp3.MediaType.Companion.toMediaType
|
||||
import okhttp3.MultipartBody
|
||||
import okhttp3.RequestBody.Companion.toRequestBody
|
||||
|
||||
class DmChatRepository(
|
||||
private val api: DmChatApi,
|
||||
@@ -37,6 +40,17 @@ class DmChatRepository(
|
||||
limit = limit
|
||||
)
|
||||
|
||||
fun sendVoiceMessage(
|
||||
token: String,
|
||||
roomId: Long,
|
||||
voiceMessageFile: MultipartBody.Part
|
||||
): Single<ApiResponse<SendDmVoiceMessageResponse>> = api.sendDmVoiceMessage(
|
||||
authHeader = bearer(token),
|
||||
roomId = roomId,
|
||||
voiceMessageFile = voiceMessageFile,
|
||||
request = VOICE_REQUEST_JSON.toRequestBody(JSON_MEDIA_TYPE.toMediaType())
|
||||
)
|
||||
|
||||
fun connectSocket(
|
||||
token: String,
|
||||
listener: DmChatSocketClient.Listener
|
||||
@@ -44,9 +58,21 @@ class DmChatRepository(
|
||||
socketClient?.connect(token = token, listener = listener)
|
||||
}
|
||||
|
||||
fun sendJoinRoom(roomId: Long): Boolean = socketClient?.sendJoinRoom(roomId = roomId) ?: false
|
||||
fun sendJoinRoom(
|
||||
roomId: Long,
|
||||
requestId: String
|
||||
): Boolean = socketClient?.sendJoinRoom(
|
||||
roomId = roomId,
|
||||
requestId = requestId
|
||||
) ?: false
|
||||
|
||||
fun sendLeaveRoom(roomId: Long): Boolean = socketClient?.sendLeaveRoom(roomId = roomId) ?: false
|
||||
fun sendLeaveRoom(
|
||||
roomId: Long,
|
||||
requestId: String
|
||||
): Boolean = socketClient?.sendLeaveRoom(
|
||||
roomId = roomId,
|
||||
requestId = requestId
|
||||
) ?: false
|
||||
|
||||
fun sendSocketText(
|
||||
roomId: Long,
|
||||
@@ -58,7 +84,13 @@ class DmChatRepository(
|
||||
textMessage = textMessage
|
||||
) ?: false
|
||||
|
||||
fun sendPing(): Boolean = socketClient?.sendPing() ?: false
|
||||
fun sendPing(
|
||||
roomId: Long,
|
||||
requestId: String
|
||||
): Boolean = socketClient?.sendPing(
|
||||
roomId = roomId,
|
||||
requestId = requestId
|
||||
) ?: false
|
||||
|
||||
fun closeSocket() {
|
||||
socketClient?.close()
|
||||
@@ -68,5 +100,7 @@ class DmChatRepository(
|
||||
|
||||
private companion object {
|
||||
const val DEFAULT_LIMIT = 20
|
||||
const val VOICE_REQUEST_JSON = "{\"recipientId\":null}"
|
||||
const val JSON_MEDIA_TYPE = "application/json"
|
||||
}
|
||||
}
|
||||
|
||||
+55
-14
@@ -1,18 +1,22 @@
|
||||
package kr.co.vividnext.sodalive.v2.main.chat.dm.data
|
||||
|
||||
import androidx.annotation.Keep
|
||||
import com.google.gson.Gson
|
||||
import com.google.gson.JsonObject
|
||||
import com.google.gson.annotations.SerializedName
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import okhttp3.WebSocket
|
||||
import okhttp3.WebSocketListener
|
||||
import java.io.IOException
|
||||
|
||||
class DmChatSocketClient(
|
||||
private val okHttpClient: OkHttpClient,
|
||||
private val gson: Gson,
|
||||
private val baseUrl: String,
|
||||
private val webSocketFactory: (Request, WebSocketListener) -> WebSocket = okHttpClient::newWebSocket
|
||||
private val webSocketFactory: (Request, WebSocketListener) -> WebSocket = okHttpClient::newWebSocket,
|
||||
private val rawJsonLogger: (String) -> Unit = {}
|
||||
) {
|
||||
interface Listener {
|
||||
fun onEvent(event: DmChatSocketEvent)
|
||||
@@ -40,8 +44,11 @@ class DmChatSocketClient(
|
||||
.header(HEADER_AUTHORIZATION, bearer(token))
|
||||
.build()
|
||||
val socketListener = object : WebSocketListener() {
|
||||
private var closeNotified: Boolean = false
|
||||
|
||||
override fun onMessage(webSocket: WebSocket, text: String) {
|
||||
if (webSocket != activeSocket) return
|
||||
rawJsonLogger(text)
|
||||
parser.parse(text)?.let { event -> this@DmChatSocketClient.listener?.onEvent(event) }
|
||||
}
|
||||
|
||||
@@ -49,19 +56,39 @@ class DmChatSocketClient(
|
||||
if (webSocket != activeSocket) return
|
||||
this@DmChatSocketClient.listener?.onFailure(t)
|
||||
}
|
||||
|
||||
override fun onClosing(webSocket: WebSocket, code: Int, reason: String) {
|
||||
notifySocketClosed(webSocket, code, reason)
|
||||
}
|
||||
|
||||
override fun onClosed(webSocket: WebSocket, code: Int, reason: String) {
|
||||
notifySocketClosed(webSocket, code, reason)
|
||||
}
|
||||
|
||||
private fun notifySocketClosed(webSocket: WebSocket, code: Int, reason: String) {
|
||||
if (webSocket != activeSocket || closeNotified) return
|
||||
closeNotified = true
|
||||
this@DmChatSocketClient.listener?.onFailure(
|
||||
IOException("WebSocket closed: code=$code reason=$reason")
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
webSocket = webSocketFactory(request, socketListener).also { activeSocket = it }
|
||||
}
|
||||
|
||||
fun sendJoinRoom(roomId: Long): Boolean = send(
|
||||
fun sendJoinRoom(roomId: Long, requestId: String): Boolean = send(
|
||||
type = DmChatSocketClientType.JOIN_ROOM,
|
||||
payload = DmChatSocketRoomPayload(roomId = roomId)
|
||||
requestId = requestId,
|
||||
roomId = roomId,
|
||||
payload = JsonObject()
|
||||
)
|
||||
|
||||
fun sendLeaveRoom(roomId: Long): Boolean = send(
|
||||
fun sendLeaveRoom(roomId: Long, requestId: String): Boolean = send(
|
||||
type = DmChatSocketClientType.LEAVE_ROOM,
|
||||
payload = DmChatSocketRoomPayload(roomId = roomId)
|
||||
requestId = requestId,
|
||||
roomId = roomId,
|
||||
payload = JsonObject()
|
||||
)
|
||||
|
||||
fun sendText(
|
||||
@@ -70,15 +97,15 @@ class DmChatSocketClient(
|
||||
textMessage: String
|
||||
): Boolean = send(
|
||||
type = DmChatSocketClientType.SEND_TEXT,
|
||||
payload = DmChatSocketSendTextPayload(
|
||||
roomId = roomId,
|
||||
requestId = requestId,
|
||||
textMessage = textMessage
|
||||
)
|
||||
requestId = requestId,
|
||||
roomId = roomId,
|
||||
payload = DmChatSocketSendTextPayload(textMessage = textMessage)
|
||||
)
|
||||
|
||||
fun sendPing(): Boolean = send(
|
||||
fun sendPing(roomId: Long, requestId: String): Boolean = send(
|
||||
type = DmChatSocketClientType.PING,
|
||||
requestId = requestId,
|
||||
roomId = roomId,
|
||||
payload = JsonObject()
|
||||
)
|
||||
|
||||
@@ -93,10 +120,21 @@ class DmChatSocketClient(
|
||||
|
||||
private fun send(
|
||||
type: DmChatSocketClientType,
|
||||
requestId: String,
|
||||
roomId: Long,
|
||||
payload: Any
|
||||
): Boolean {
|
||||
val socket = webSocket ?: return false
|
||||
return socket.send(gson.toJson(DmChatSocketOutboundEnvelope(type = type.value, payload = payload)))
|
||||
val rawJson = gson.toJson(
|
||||
DmChatSocketOutboundEnvelope(
|
||||
type = type.value,
|
||||
requestId = requestId,
|
||||
roomId = roomId,
|
||||
payload = payload
|
||||
)
|
||||
)
|
||||
rawJsonLogger(rawJson)
|
||||
return socket.send(rawJson)
|
||||
}
|
||||
|
||||
private fun socketUrl(): String = baseUrl
|
||||
@@ -106,9 +144,12 @@ class DmChatSocketClient(
|
||||
|
||||
private fun bearer(token: String) = "Bearer $token"
|
||||
|
||||
@Keep
|
||||
private data class DmChatSocketOutboundEnvelope(
|
||||
val type: String,
|
||||
val payload: Any
|
||||
@SerializedName("type") val type: String,
|
||||
@SerializedName("requestId") val requestId: String,
|
||||
@SerializedName("roomId") val roomId: Long,
|
||||
@SerializedName("payload") val payload: Any
|
||||
)
|
||||
|
||||
private companion object {
|
||||
|
||||
+42
-44
@@ -9,38 +9,19 @@ import com.google.gson.annotations.SerializedName
|
||||
@Keep
|
||||
data class DmChatSocketEnvelope(
|
||||
@SerializedName("type") val type: String,
|
||||
@SerializedName("requestId") val requestId: String?,
|
||||
@SerializedName("roomId") val roomId: Long?,
|
||||
@SerializedName("payload") val payload: JsonObject?
|
||||
)
|
||||
|
||||
@Keep
|
||||
data class DmChatSocketRoomPayload(
|
||||
@SerializedName("roomId") val roomId: Long
|
||||
)
|
||||
|
||||
@Keep
|
||||
data class DmChatSocketSendTextPayload(
|
||||
@SerializedName("roomId") val roomId: Long,
|
||||
@SerializedName("requestId") val requestId: String,
|
||||
@SerializedName("textMessage") val textMessage: String
|
||||
)
|
||||
|
||||
@Keep
|
||||
data class DmChatSocketMessagePayload(
|
||||
@SerializedName("requestId") val requestId: String?,
|
||||
@SerializedName("message") val message: DmChatMessageResponse
|
||||
)
|
||||
|
||||
@Keep
|
||||
data class DmChatSocketSendAckPayload(
|
||||
@SerializedName("requestId") val requestId: String,
|
||||
@SerializedName("message") val message: DmChatMessageResponse
|
||||
)
|
||||
|
||||
@Keep
|
||||
data class DmChatSocketErrorPayload(
|
||||
@SerializedName("requestId") val requestId: String?,
|
||||
@SerializedName("code") val code: String?,
|
||||
@SerializedName("message") val message: String?
|
||||
@SerializedName("messageKey") val messageKey: String?
|
||||
)
|
||||
|
||||
enum class DmChatSocketClientType(val value: String) {
|
||||
@@ -51,32 +32,46 @@ enum class DmChatSocketClientType(val value: String) {
|
||||
}
|
||||
|
||||
sealed class DmChatSocketEvent {
|
||||
data object Joined : DmChatSocketEvent()
|
||||
data class Joined(
|
||||
val requestId: String?,
|
||||
val roomId: Long?
|
||||
) : DmChatSocketEvent()
|
||||
data class Message(
|
||||
val requestId: String?,
|
||||
val roomId: Long?,
|
||||
val message: DmChatMessageResponse
|
||||
) : DmChatSocketEvent()
|
||||
data class SendAck(
|
||||
val requestId: String,
|
||||
val roomId: Long?,
|
||||
val message: DmChatMessageResponse
|
||||
) : DmChatSocketEvent()
|
||||
data class Error(
|
||||
val requestId: String?,
|
||||
val code: String?,
|
||||
val message: String?
|
||||
val roomId: Long?,
|
||||
val messageKey: String?
|
||||
) : DmChatSocketEvent()
|
||||
data class Pong(
|
||||
val requestId: String?,
|
||||
val roomId: Long?
|
||||
) : DmChatSocketEvent()
|
||||
data object Pong : DmChatSocketEvent()
|
||||
}
|
||||
|
||||
class DmChatSocketParser(private val gson: Gson) {
|
||||
fun parse(text: String): DmChatSocketEvent? = try {
|
||||
val envelope = gson.fromJson(text, DmChatSocketEnvelope::class.java)
|
||||
when (envelope.type) {
|
||||
TYPE_JOINED -> DmChatSocketEvent.Joined
|
||||
TYPE_MESSAGE -> parseMessage(envelope.payload)
|
||||
TYPE_SEND_ACK -> parseSendAck(envelope.payload)
|
||||
TYPE_ERROR -> parseError(envelope.payload)
|
||||
TYPE_PONG -> DmChatSocketEvent.Pong
|
||||
TYPE_JOINED -> DmChatSocketEvent.Joined(
|
||||
requestId = envelope.requestId,
|
||||
roomId = envelope.roomId
|
||||
)
|
||||
TYPE_MESSAGE -> parseMessage(envelope)
|
||||
TYPE_SEND_ACK -> parseSendAck(envelope)
|
||||
TYPE_ERROR -> parseError(envelope)
|
||||
TYPE_PONG -> DmChatSocketEvent.Pong(
|
||||
requestId = envelope.requestId,
|
||||
roomId = envelope.roomId
|
||||
)
|
||||
else -> null
|
||||
}
|
||||
} catch (e: JsonSyntaxException) {
|
||||
@@ -87,28 +82,31 @@ class DmChatSocketParser(private val gson: Gson) {
|
||||
null
|
||||
}
|
||||
|
||||
private fun parseMessage(payload: JsonObject?): DmChatSocketEvent.Message? {
|
||||
val messagePayload = gson.fromJson(payload, DmChatSocketMessagePayload::class.java) ?: return null
|
||||
private fun parseMessage(envelope: DmChatSocketEnvelope): DmChatSocketEvent.Message? {
|
||||
val message = gson.fromJson(envelope.payload, DmChatMessageResponse::class.java) ?: return null
|
||||
return DmChatSocketEvent.Message(
|
||||
requestId = messagePayload.requestId,
|
||||
message = messagePayload.message
|
||||
requestId = envelope.requestId,
|
||||
roomId = envelope.roomId,
|
||||
message = message
|
||||
)
|
||||
}
|
||||
|
||||
private fun parseSendAck(payload: JsonObject?): DmChatSocketEvent.SendAck? {
|
||||
val ackPayload = gson.fromJson(payload, DmChatSocketSendAckPayload::class.java) ?: return null
|
||||
private fun parseSendAck(envelope: DmChatSocketEnvelope): DmChatSocketEvent.SendAck? {
|
||||
val requestId = envelope.requestId ?: return null
|
||||
val message = gson.fromJson(envelope.payload, DmChatMessageResponse::class.java) ?: return null
|
||||
return DmChatSocketEvent.SendAck(
|
||||
requestId = ackPayload.requestId,
|
||||
message = ackPayload.message
|
||||
requestId = requestId,
|
||||
roomId = envelope.roomId,
|
||||
message = message
|
||||
)
|
||||
}
|
||||
|
||||
private fun parseError(payload: JsonObject?): DmChatSocketEvent.Error? {
|
||||
val errorPayload = gson.fromJson(payload, DmChatSocketErrorPayload::class.java) ?: return null
|
||||
private fun parseError(envelope: DmChatSocketEnvelope): DmChatSocketEvent.Error? {
|
||||
val errorPayload = gson.fromJson(envelope.payload, DmChatSocketErrorPayload::class.java) ?: return null
|
||||
return DmChatSocketEvent.Error(
|
||||
requestId = errorPayload.requestId,
|
||||
code = errorPayload.code,
|
||||
message = errorPayload.message
|
||||
requestId = envelope.requestId,
|
||||
roomId = envelope.roomId,
|
||||
messageKey = errorPayload.messageKey
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user