refactor(dm): 채팅 저장소를 WebSocket 기준으로 전환한다
This commit is contained in:
@@ -186,9 +186,8 @@ import kr.co.vividnext.sodalive.v2.main.chat.data.ChatRoomApi
|
|||||||
import kr.co.vividnext.sodalive.v2.main.chat.data.ChatRoomRepository
|
import kr.co.vividnext.sodalive.v2.main.chat.data.ChatRoomRepository
|
||||||
import kr.co.vividnext.sodalive.v2.main.chat.dm.DmChatRoomViewModel
|
import kr.co.vividnext.sodalive.v2.main.chat.dm.DmChatRoomViewModel
|
||||||
import kr.co.vividnext.sodalive.v2.main.chat.dm.data.DmChatApi
|
import kr.co.vividnext.sodalive.v2.main.chat.dm.data.DmChatApi
|
||||||
import kr.co.vividnext.sodalive.v2.main.chat.dm.data.DmChatEventClient
|
|
||||||
import kr.co.vividnext.sodalive.v2.main.chat.dm.data.DmChatRealtimeClient
|
|
||||||
import kr.co.vividnext.sodalive.v2.main.chat.dm.data.DmChatRepository
|
import kr.co.vividnext.sodalive.v2.main.chat.dm.data.DmChatRepository
|
||||||
|
import kr.co.vividnext.sodalive.v2.main.chat.dm.data.DmChatSocketClient
|
||||||
import kr.co.vividnext.sodalive.v2.main.home.HomeCreatorRankingViewModel
|
import kr.co.vividnext.sodalive.v2.main.home.HomeCreatorRankingViewModel
|
||||||
import kr.co.vividnext.sodalive.v2.main.home.HomeRecommendationViewModel
|
import kr.co.vividnext.sodalive.v2.main.home.HomeRecommendationViewModel
|
||||||
import kr.co.vividnext.sodalive.v2.main.home.data.HomeCreatorRankingApi
|
import kr.co.vividnext.sodalive.v2.main.home.data.HomeCreatorRankingApi
|
||||||
@@ -313,7 +312,7 @@ class AppDI(private val context: Context, isDebugMode: Boolean) {
|
|||||||
single { ApiBuilder().build(get(), CharacterCommentApi::class.java) }
|
single { ApiBuilder().build(get(), CharacterCommentApi::class.java) }
|
||||||
single { ApiBuilder().build(get(), OriginalWorkApi::class.java) }
|
single { ApiBuilder().build(get(), OriginalWorkApi::class.java) }
|
||||||
single { ApiBuilder().build(get<Retrofit>(named("agoraRetrofit")), V2vApi::class.java) }
|
single { ApiBuilder().build(get<Retrofit>(named("agoraRetrofit")), V2vApi::class.java) }
|
||||||
single<DmChatRealtimeClient> { DmChatEventClient(okHttpClient = get(), gson = get(), baseUrl = baseUrl) }
|
single { DmChatSocketClient(okHttpClient = get(), gson = get(), baseUrl = baseUrl) }
|
||||||
}
|
}
|
||||||
|
|
||||||
private val viewModelModule = module {
|
private val viewModelModule = module {
|
||||||
@@ -462,7 +461,7 @@ class AppDI(private val context: Context, isDebugMode: Boolean) {
|
|||||||
factory { PointStatusRepository(get()) }
|
factory { PointStatusRepository(get()) }
|
||||||
factory { HomeRepository(get()) }
|
factory { HomeRepository(get()) }
|
||||||
factory { ChatRoomRepository(get()) }
|
factory { ChatRoomRepository(get()) }
|
||||||
factory { DmChatRepository(api = get(), realtimeClient = get()) }
|
factory { DmChatRepository(api = get(), socketClient = get()) }
|
||||||
factory { HomeCreatorRankingRepository(get()) }
|
factory { HomeCreatorRankingRepository(get()) }
|
||||||
factory { HomeRecommendationRepository(get()) }
|
factory { HomeRecommendationRepository(get()) }
|
||||||
factory {
|
factory {
|
||||||
|
|||||||
@@ -30,17 +30,4 @@ interface DmChatApi {
|
|||||||
@Query("cursor") cursor: Long?,
|
@Query("cursor") cursor: Long?,
|
||||||
@Query("limit") limit: Int = 20
|
@Query("limit") limit: Int = 20
|
||||||
): Single<ApiResponse<DmChatMessagesPageResponse>>
|
): Single<ApiResponse<DmChatMessagesPageResponse>>
|
||||||
|
|
||||||
@POST("/api/v2/user-creator-chat/rooms/{roomId}/messages/text")
|
|
||||||
fun sendDmTextMessage(
|
|
||||||
@Header("Authorization") authHeader: String,
|
|
||||||
@Path("roomId") roomId: Long,
|
|
||||||
@Body request: SendDmTextMessageRequest
|
|
||||||
): Single<ApiResponse<SendDmChatMessageResponse>>
|
|
||||||
|
|
||||||
@POST("/api/v2/user-creator-chat/rooms/{roomId}/events/disconnect")
|
|
||||||
fun disconnectRealtime(
|
|
||||||
@Header("Authorization") authHeader: String,
|
|
||||||
@Path("roomId") roomId: Long
|
|
||||||
): Single<ApiResponse<Boolean>>
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,15 +42,3 @@ data class DmChatMessageResponse(
|
|||||||
@SerializedName("senderNickname") val senderNickname: String,
|
@SerializedName("senderNickname") val senderNickname: String,
|
||||||
@SerializedName("senderProfileImageUrl") val senderProfileImageUrl: String
|
@SerializedName("senderProfileImageUrl") val senderProfileImageUrl: String
|
||||||
)
|
)
|
||||||
|
|
||||||
@Keep
|
|
||||||
data class SendDmTextMessageRequest(
|
|
||||||
@SerializedName("textMessage") val textMessage: String
|
|
||||||
)
|
|
||||||
|
|
||||||
@Keep
|
|
||||||
data class SendDmChatMessageResponse(
|
|
||||||
@SerializedName("message") val message: DmChatMessageResponse,
|
|
||||||
@SerializedName("deliveredRealtime") val deliveredRealtime: Boolean,
|
|
||||||
@SerializedName("pushSent") val pushSent: Boolean
|
|
||||||
)
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import kr.co.vividnext.sodalive.common.ApiResponse
|
|||||||
|
|
||||||
class DmChatRepository(
|
class DmChatRepository(
|
||||||
private val api: DmChatApi,
|
private val api: DmChatApi,
|
||||||
private val realtimeClient: DmChatRealtimeClient? = null
|
private val socketClient: DmChatSocketClient? = null
|
||||||
) {
|
) {
|
||||||
fun createOrGetRoom(
|
fun createOrGetRoom(
|
||||||
token: String,
|
token: String,
|
||||||
@@ -37,34 +37,31 @@ class DmChatRepository(
|
|||||||
limit = limit
|
limit = limit
|
||||||
)
|
)
|
||||||
|
|
||||||
fun sendTextMessage(
|
fun connectSocket(
|
||||||
token: String,
|
token: String,
|
||||||
roomId: Long,
|
listener: DmChatSocketClient.Listener
|
||||||
textMessage: String
|
|
||||||
): Single<ApiResponse<SendDmChatMessageResponse>> = api.sendDmTextMessage(
|
|
||||||
authHeader = bearer(token),
|
|
||||||
roomId = roomId,
|
|
||||||
request = SendDmTextMessageRequest(textMessage = textMessage)
|
|
||||||
)
|
|
||||||
|
|
||||||
fun disconnectRealtime(
|
|
||||||
token: String,
|
|
||||||
roomId: Long
|
|
||||||
): Single<ApiResponse<Boolean>> = api.disconnectRealtime(
|
|
||||||
authHeader = bearer(token),
|
|
||||||
roomId = roomId
|
|
||||||
)
|
|
||||||
|
|
||||||
fun connectRealtime(
|
|
||||||
token: String,
|
|
||||||
roomId: Long,
|
|
||||||
listener: DmChatEventClient.Listener
|
|
||||||
) {
|
) {
|
||||||
realtimeClient?.connect(token = token, roomId = roomId, listener = listener)
|
socketClient?.connect(token = token, listener = listener)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun cancelRealtime() {
|
fun sendJoinRoom(roomId: Long): Boolean = socketClient?.sendJoinRoom(roomId = roomId) ?: false
|
||||||
realtimeClient?.cancel()
|
|
||||||
|
fun sendLeaveRoom(roomId: Long): Boolean = socketClient?.sendLeaveRoom(roomId = roomId) ?: false
|
||||||
|
|
||||||
|
fun sendSocketText(
|
||||||
|
roomId: Long,
|
||||||
|
requestId: String,
|
||||||
|
textMessage: String
|
||||||
|
): Boolean = socketClient?.sendText(
|
||||||
|
roomId = roomId,
|
||||||
|
requestId = requestId,
|
||||||
|
textMessage = textMessage
|
||||||
|
) ?: false
|
||||||
|
|
||||||
|
fun sendPing(): Boolean = socketClient?.sendPing() ?: false
|
||||||
|
|
||||||
|
fun closeSocket() {
|
||||||
|
socketClient?.close()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun bearer(token: String) = "Bearer $token"
|
private fun bearer(token: String) = "Bearer $token"
|
||||||
|
|||||||
@@ -5,20 +5,32 @@ import kr.co.vividnext.sodalive.common.ApiResponse
|
|||||||
import kr.co.vividnext.sodalive.v2.main.chat.dm.data.CreateDmChatRoomRequest
|
import kr.co.vividnext.sodalive.v2.main.chat.dm.data.CreateDmChatRoomRequest
|
||||||
import kr.co.vividnext.sodalive.v2.main.chat.dm.data.CreateDmChatRoomResponse
|
import kr.co.vividnext.sodalive.v2.main.chat.dm.data.CreateDmChatRoomResponse
|
||||||
import kr.co.vividnext.sodalive.v2.main.chat.dm.data.DmChatApi
|
import kr.co.vividnext.sodalive.v2.main.chat.dm.data.DmChatApi
|
||||||
import kr.co.vividnext.sodalive.v2.main.chat.dm.data.DmChatMessageResponse
|
|
||||||
import kr.co.vividnext.sodalive.v2.main.chat.dm.data.DmChatMessagesPageResponse
|
import kr.co.vividnext.sodalive.v2.main.chat.dm.data.DmChatMessagesPageResponse
|
||||||
import kr.co.vividnext.sodalive.v2.main.chat.dm.data.DmChatRepository
|
import kr.co.vividnext.sodalive.v2.main.chat.dm.data.DmChatRepository
|
||||||
import kr.co.vividnext.sodalive.v2.main.chat.dm.data.DmChatRoomOpenResponse
|
import kr.co.vividnext.sodalive.v2.main.chat.dm.data.DmChatRoomOpenResponse
|
||||||
import kr.co.vividnext.sodalive.v2.main.chat.dm.data.SendDmChatMessageResponse
|
import kr.co.vividnext.sodalive.v2.main.chat.dm.data.DmChatSocketClient
|
||||||
import kr.co.vividnext.sodalive.v2.main.chat.dm.data.SendDmTextMessageRequest
|
import kr.co.vividnext.sodalive.v2.main.chat.dm.data.DmChatSocketEvent
|
||||||
|
import okhttp3.OkHttpClient
|
||||||
|
import okhttp3.Request
|
||||||
|
import okhttp3.WebSocket
|
||||||
|
import okhttp3.WebSocketListener
|
||||||
|
import okio.ByteString
|
||||||
import org.junit.Assert.assertEquals
|
import org.junit.Assert.assertEquals
|
||||||
import org.junit.Assert.assertNull
|
import org.junit.Assert.assertFalse
|
||||||
|
import org.junit.Assert.assertTrue
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
|
||||||
class DmChatRepositoryTest {
|
class DmChatRepositoryTest {
|
||||||
|
|
||||||
private val api = FakeDmChatApi()
|
private val api = FakeDmChatApi()
|
||||||
private val repository = DmChatRepository(api)
|
private val socketFactory = FakeWebSocketFactory()
|
||||||
|
private val socketClient = DmChatSocketClient(
|
||||||
|
okHttpClient = OkHttpClient(),
|
||||||
|
gson = com.google.gson.Gson(),
|
||||||
|
baseUrl = "https://api.example.com",
|
||||||
|
webSocketFactory = socketFactory::newWebSocket
|
||||||
|
)
|
||||||
|
private val repository = DmChatRepository(api = api, socketClient = socketClient)
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `createOrGetRoom은 bearer header와 creatorId를 API에 위임한다`() {
|
fun `createOrGetRoom은 bearer header와 creatorId를 API에 위임한다`() {
|
||||||
@@ -48,27 +60,38 @@ class DmChatRepositoryTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `sendTextMessage는 textMessage request를 API에 위임한다`() {
|
fun `connectSocket은 bearer header와 listener를 socket client에 위임한다`() {
|
||||||
repository.sendTextMessage(token = "test-token", roomId = 12L, textMessage = "안녕").blockingGet()
|
repository.connectSocket(token = "test-token", listener = TestListener())
|
||||||
|
|
||||||
assertEquals("Bearer test-token", api.lastAuthHeader)
|
assertEquals("Bearer test-token", socketFactory.request?.header("Authorization"))
|
||||||
assertEquals(12L, api.lastRoomId)
|
assertEquals("wss://api.example.com/ws/v2/user-creator-chat", socketFactory.request?.tag(String::class.java))
|
||||||
assertEquals(SendDmTextMessageRequest(textMessage = "안녕"), api.lastSendRequest)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `disconnectRealtime은 bearer header와 roomId를 API에 위임한다`() {
|
fun `socket send 메서드는 socket client envelope 전송에 위임한다`() {
|
||||||
repository.disconnectRealtime(token = "test-token", roomId = 12L).blockingGet()
|
repository.connectSocket(token = "test-token", listener = TestListener())
|
||||||
|
|
||||||
assertEquals("Bearer test-token", api.lastAuthHeader)
|
assertTrue(repository.sendJoinRoom(roomId = 12L))
|
||||||
assertEquals(12L, api.lastRoomId)
|
assertTrue(repository.sendLeaveRoom(roomId = 12L))
|
||||||
assertNull(api.lastSendRequest)
|
assertTrue(repository.sendSocketText(roomId = 12L, requestId = "request-1", textMessage = "안녕"))
|
||||||
|
assertTrue(repository.sendPing())
|
||||||
|
|
||||||
|
assertEquals(listOf("JOIN_ROOM", "LEAVE_ROOM", "SEND_TEXT", "PING"), socketFactory.webSocket.sentTypes())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `closeSocket은 socket close에 위임하고 미연결 전송은 false를 반환한다`() {
|
||||||
|
repository.connectSocket(token = "test-token", listener = TestListener())
|
||||||
|
|
||||||
|
repository.closeSocket()
|
||||||
|
|
||||||
|
assertEquals(1, socketFactory.webSocket.closeCount)
|
||||||
|
assertFalse(repository.sendPing())
|
||||||
}
|
}
|
||||||
|
|
||||||
private class FakeDmChatApi : DmChatApi {
|
private class FakeDmChatApi : DmChatApi {
|
||||||
var lastAuthHeader: String? = null
|
var lastAuthHeader: String? = null
|
||||||
var lastCreateRequest: CreateDmChatRoomRequest? = null
|
var lastCreateRequest: CreateDmChatRoomRequest? = null
|
||||||
var lastSendRequest: SendDmTextMessageRequest? = null
|
|
||||||
var lastRoomId: Long? = null
|
var lastRoomId: Long? = null
|
||||||
var lastCursor: Long? = null
|
var lastCursor: Long? = null
|
||||||
var lastLimit: Int? = null
|
var lastLimit: Int? = null
|
||||||
@@ -126,46 +149,47 @@ class DmChatRepositoryTest {
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun sendDmTextMessage(
|
private open class TestListener : DmChatSocketClient.Listener {
|
||||||
authHeader: String,
|
override fun onEvent(event: DmChatSocketEvent) = Unit
|
||||||
roomId: Long,
|
override fun onFailure(throwable: Throwable) = Unit
|
||||||
request: SendDmTextMessageRequest
|
}
|
||||||
): Single<ApiResponse<SendDmChatMessageResponse>> {
|
|
||||||
lastAuthHeader = authHeader
|
private class FakeWebSocketFactory {
|
||||||
lastRoomId = roomId
|
val webSocket = FakeWebSocket()
|
||||||
lastSendRequest = request
|
var request: Request? = null
|
||||||
return Single.just(
|
|
||||||
ApiResponse(
|
fun newWebSocket(request: Request, listener: WebSocketListener): WebSocket {
|
||||||
success = true,
|
this.request = request
|
||||||
data = SendDmChatMessageResponse(
|
return webSocket
|
||||||
message = message(),
|
}
|
||||||
deliveredRealtime = true,
|
}
|
||||||
pushSent = false
|
|
||||||
)
|
private class FakeWebSocket : WebSocket {
|
||||||
)
|
val sentTexts = mutableListOf<String>()
|
||||||
)
|
var closeCount = 0
|
||||||
|
|
||||||
|
override fun request(): Request = Request.Builder().url("wss://example.com").build()
|
||||||
|
|
||||||
|
override fun queueSize(): Long = 0L
|
||||||
|
|
||||||
|
override fun send(text: String): Boolean {
|
||||||
|
sentTexts += text
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun disconnectRealtime(
|
override fun send(bytes: ByteString): Boolean = true
|
||||||
authHeader: String,
|
|
||||||
roomId: Long
|
override fun close(code: Int, reason: String?): Boolean {
|
||||||
): Single<ApiResponse<Boolean>> {
|
closeCount += 1
|
||||||
lastAuthHeader = authHeader
|
return true
|
||||||
lastRoomId = roomId
|
|
||||||
return Single.just(ApiResponse(success = true, data = true))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun message() = DmChatMessageResponse(
|
override fun cancel() = Unit
|
||||||
messageId = 1L,
|
|
||||||
messageType = "TEXT",
|
fun sentTypes(): List<String> = sentTexts.map { text ->
|
||||||
mine = true,
|
com.google.gson.JsonParser.parseString(text).asJsonObject.get("type").asString
|
||||||
createdAt = 1000L,
|
}
|
||||||
textMessage = "안녕",
|
|
||||||
voiceMessageUrl = null,
|
|
||||||
senderId = 2L,
|
|
||||||
senderNickname = "나",
|
|
||||||
senderProfileImageUrl = "https://example.com/profile.png"
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user