feat(dm): WebSocket 클라이언트를 추가한다

This commit is contained in:
2026-06-18 17:41:41 +09:00
parent e76562067f
commit c5bcaf7329
2 changed files with 398 additions and 0 deletions

View File

@@ -0,0 +1,122 @@
package kr.co.vividnext.sodalive.v2.main.chat.dm.data
import com.google.gson.Gson
import com.google.gson.JsonObject
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.Response
import okhttp3.WebSocket
import okhttp3.WebSocketListener
class DmChatSocketClient(
private val okHttpClient: OkHttpClient,
private val gson: Gson,
private val baseUrl: String,
private val webSocketFactory: (Request, WebSocketListener) -> WebSocket = okHttpClient::newWebSocket
) {
interface Listener {
fun onEvent(event: DmChatSocketEvent)
fun onFailure(throwable: Throwable)
}
private val parser = DmChatSocketParser(gson)
private var webSocket: WebSocket? = null
@Volatile
private var listener: Listener? = null
@Volatile
private var activeSocket: WebSocket? = null
@Synchronized
fun connect(token: String, listener: Listener) {
close()
this.listener = listener
val socketUrl = socketUrl()
val request = Request.Builder()
.url(socketUrl)
.tag(String::class.java, socketUrl)
.header(HEADER_AUTHORIZATION, bearer(token))
.build()
val socketListener = object : WebSocketListener() {
override fun onMessage(webSocket: WebSocket, text: String) {
if (webSocket != activeSocket) return
parser.parse(text)?.let { event -> this@DmChatSocketClient.listener?.onEvent(event) }
}
override fun onFailure(webSocket: WebSocket, t: Throwable, response: Response?) {
if (webSocket != activeSocket) return
this@DmChatSocketClient.listener?.onFailure(t)
}
}
webSocket = webSocketFactory(request, socketListener).also { activeSocket = it }
}
fun sendJoinRoom(roomId: Long): Boolean = send(
type = DmChatSocketClientType.JOIN_ROOM,
payload = DmChatSocketRoomPayload(roomId = roomId)
)
fun sendLeaveRoom(roomId: Long): Boolean = send(
type = DmChatSocketClientType.LEAVE_ROOM,
payload = DmChatSocketRoomPayload(roomId = roomId)
)
fun sendText(
roomId: Long,
requestId: String,
textMessage: String
): Boolean = send(
type = DmChatSocketClientType.SEND_TEXT,
payload = DmChatSocketSendTextPayload(
roomId = roomId,
requestId = requestId,
textMessage = textMessage
)
)
fun sendPing(): Boolean = send(
type = DmChatSocketClientType.PING,
payload = JsonObject()
)
@Synchronized
fun close() {
val socket = webSocket ?: return
webSocket = null
activeSocket = null
listener = null
socket.close(NORMAL_CLOSE_CODE, null)
}
private fun send(
type: DmChatSocketClientType,
payload: Any
): Boolean {
val socket = webSocket ?: return false
return socket.send(gson.toJson(DmChatSocketOutboundEnvelope(type = type.value, payload = payload)))
}
private fun socketUrl(): String = baseUrl
.trimEnd('/')
.replacePrefix(oldValue = "https://", newValue = "wss://")
.replacePrefix(oldValue = "http://", newValue = "ws://") + SOCKET_PATH
private fun bearer(token: String) = "Bearer $token"
private data class DmChatSocketOutboundEnvelope(
val type: String,
val payload: Any
)
private companion object {
const val HEADER_AUTHORIZATION = "Authorization"
const val NORMAL_CLOSE_CODE = 1000
const val SOCKET_PATH = "/ws/v2/user-creator-chat"
}
}
private fun String.replacePrefix(oldValue: String, newValue: String): String =
if (startsWith(oldValue)) newValue + removePrefix(oldValue) else this