feat(chat): DM 채팅 SSE 클라이언트를 추가한다
This commit is contained in:
@@ -0,0 +1,146 @@
|
||||
package kr.co.vividnext.sodalive.v2.main.chat.dm.data
|
||||
|
||||
import com.google.gson.Gson
|
||||
import com.google.gson.JsonSyntaxException
|
||||
import okhttp3.Call
|
||||
import okhttp3.Callback
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import java.io.IOException
|
||||
|
||||
class DmChatEventParser(private val gson: Gson) {
|
||||
sealed class Event {
|
||||
data object Connected : Event()
|
||||
data class Message(val message: DmChatMessageResponse) : Event()
|
||||
}
|
||||
|
||||
fun parse(frame: String): Event? {
|
||||
val lines = frame.lineSequence().filter { it.isNotBlank() }
|
||||
var eventName: String? = null
|
||||
val dataLines = mutableListOf<String>()
|
||||
|
||||
lines.forEach { line ->
|
||||
when {
|
||||
line.startsWith("event:") -> eventName = line.substringAfter(':').trim()
|
||||
line.startsWith("data:") -> dataLines += line.substringAfter(':').removeSingleLeadingSpace()
|
||||
}
|
||||
}
|
||||
|
||||
return when (eventName) {
|
||||
EVENT_CONNECTED -> Event.Connected
|
||||
EVENT_MESSAGE -> parseMessage(dataLines.joinToString(separator = "\n"))
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
private fun parseMessage(data: String): Event.Message? = try {
|
||||
Event.Message(gson.fromJson(data, DmChatMessageResponse::class.java))
|
||||
} catch (e: JsonSyntaxException) {
|
||||
null
|
||||
}
|
||||
|
||||
private fun String.removeSingleLeadingSpace(): String =
|
||||
if (startsWith(' ')) drop(1) else this
|
||||
|
||||
private companion object {
|
||||
const val EVENT_CONNECTED = "connected"
|
||||
const val EVENT_MESSAGE = "message"
|
||||
}
|
||||
}
|
||||
|
||||
class DmChatEventClient(
|
||||
private val okHttpClient: OkHttpClient,
|
||||
gson: Gson,
|
||||
private val baseUrl: String
|
||||
) {
|
||||
interface Listener {
|
||||
fun onConnected()
|
||||
fun onMessage(message: DmChatMessageResponse)
|
||||
fun onFailure(throwable: Throwable)
|
||||
}
|
||||
|
||||
private val parser = DmChatEventParser(gson)
|
||||
private var call: Call? = null
|
||||
|
||||
@Volatile
|
||||
private var listener: Listener? = null
|
||||
|
||||
@Synchronized
|
||||
fun connect(
|
||||
token: String,
|
||||
roomId: Long,
|
||||
listener: Listener
|
||||
) {
|
||||
cancel()
|
||||
this.listener = listener
|
||||
|
||||
val request = Request.Builder()
|
||||
.url(eventsUrl(roomId))
|
||||
.header(HEADER_AUTHORIZATION, bearer(token))
|
||||
.build()
|
||||
|
||||
call = okHttpClient.newCall(request).also { newCall ->
|
||||
newCall.enqueue(object : Callback {
|
||||
override fun onFailure(call: Call, e: IOException) {
|
||||
if (!call.isCanceled()) listener.onFailure(e)
|
||||
}
|
||||
|
||||
override fun onResponse(call: Call, response: Response) {
|
||||
response.use { usedResponse ->
|
||||
if (!usedResponse.isSuccessful) {
|
||||
if (!call.isCanceled()) listener.onFailure(IOException("Unexpected code ${usedResponse.code}"))
|
||||
return
|
||||
}
|
||||
val body = usedResponse.body ?: return
|
||||
try {
|
||||
readFrames(call, body.charStream().buffered())
|
||||
} catch (e: IOException) {
|
||||
if (!call.isCanceled()) listener.onFailure(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun cancel() {
|
||||
call?.cancel()
|
||||
call = null
|
||||
listener = null
|
||||
}
|
||||
|
||||
private fun readFrames(call: Call, reader: java.io.BufferedReader) {
|
||||
reader.use { bufferedReader ->
|
||||
val frame = StringBuilder()
|
||||
while (!call.isCanceled()) {
|
||||
val line = bufferedReader.readLine() ?: break
|
||||
if (line.isBlank()) {
|
||||
dispatch(frame.toString())
|
||||
frame.clear()
|
||||
} else {
|
||||
frame.append(line).append('\n')
|
||||
}
|
||||
}
|
||||
if (frame.isNotEmpty()) dispatch(frame.toString())
|
||||
}
|
||||
}
|
||||
|
||||
private fun dispatch(frame: String) {
|
||||
when (val event = parser.parse(frame)) {
|
||||
DmChatEventParser.Event.Connected -> listener?.onConnected()
|
||||
is DmChatEventParser.Event.Message -> listener?.onMessage(event.message)
|
||||
null -> Unit
|
||||
}
|
||||
}
|
||||
|
||||
private fun eventsUrl(roomId: Long): String =
|
||||
"${baseUrl.trimEnd('/')}/api/v2/user-creator-chat/rooms/$roomId/events"
|
||||
|
||||
private fun bearer(token: String) = "Bearer $token"
|
||||
|
||||
private companion object {
|
||||
const val HEADER_AUTHORIZATION = "Authorization"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user