Skip to content

Commit

Permalink
[ECO-5139] feat: switched API to v2
Browse files Browse the repository at this point in the history
  • Loading branch information
ttypic committed Nov 28, 2024
1 parent 68f6ca8 commit 9fa9cbe
Show file tree
Hide file tree
Showing 10 changed files with 101 additions and 129 deletions.
34 changes: 21 additions & 13 deletions chat-android/src/main/java/com/ably/chat/ChatApi.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import com.google.gson.JsonObject
import io.ably.lib.types.AblyException
import io.ably.lib.types.AsyncHttpPaginatedResponse
import io.ably.lib.types.ErrorInfo
import io.ably.lib.types.MessageAction
import io.ably.lib.types.Param
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException
Expand All @@ -30,19 +31,25 @@ internal class ChatApi(
val baseParams = options.toParams()
val params = fromSerial?.let { baseParams + Param("fromSerial", it) } ?: baseParams
return makeAuthorizedPaginatedRequest(
url = "/chat/v1/rooms/$roomId/messages",
url = "/chat/v2/rooms/$roomId/messages",
method = "GET",
params = params,
) {
Message(
timeserial = it.requireString("timeserial"),
clientId = it.requireString("clientId"),
roomId = it.requireString("roomId"),
text = it.requireString("text"),
createdAt = it.requireLong("createdAt"),
metadata = it.asJsonObject.get("metadata")?.toMap() ?: mapOf(),
headers = it.asJsonObject.get("headers")?.toMap() ?: mapOf(),
)
val latestActionName = it.requireJsonObject().get("latestAction")?.asString
val latestAction = latestActionName?.let { name -> messageActionNameToAction[name] }

latestAction?.let { action ->
Message(
serial = it.requireString("serial"),
clientId = it.requireString("clientId"),
roomId = it.requireString("roomId"),
text = it.requireString("text"),
createdAt = it.requireLong("createdAt"),
metadata = it.asJsonObject.get("metadata")?.toMap() ?: mapOf(),
headers = it.asJsonObject.get("headers")?.toMap() ?: mapOf(),
latestAction = action,
)
}
}
}

Expand All @@ -67,19 +74,20 @@ internal class ChatApi(
}

return makeAuthorizedRequest(
"/chat/v1/rooms/$roomId/messages",
"/chat/v2/rooms/$roomId/messages",
"POST",
body,
)?.let {
// (CHA-M3a)
Message(
timeserial = it.requireString("timeserial"),
serial = it.requireString("serial"),
clientId = clientId,
roomId = roomId,
text = params.text,
createdAt = it.requireLong("createdAt"),
metadata = params.metadata ?: mapOf(),
headers = params.headers ?: mapOf(),
latestAction = MessageAction.MESSAGE_CREATE,
)
} ?: throw AblyException.fromErrorInfo(ErrorInfo("Send message endpoint returned empty value", HttpStatusCode.InternalServerError))
}
Expand Down Expand Up @@ -158,7 +166,7 @@ internal class ChatApi(
url: String,
method: String,
params: List<Param> = listOf(),
transform: (JsonElement) -> T,
transform: (JsonElement) -> T?,
): PaginatedResult<T> = suspendCoroutine { continuation ->
realtimeClient.requestAsync(
method,
Expand Down
39 changes: 39 additions & 0 deletions chat-android/src/main/java/com/ably/chat/EventTypes.kt
Original file line number Diff line number Diff line change
@@ -1,13 +1,52 @@
package com.ably.chat

import io.ably.lib.types.MessageAction

/**
* All chat message events.
*/
enum class MessageEventType(val eventName: String) {
/** Fires when a new chat message is received. */
Created("message.created"),

/** Fires when a chat message is updated. */
Updated("message.updated"),

/** Fires when a chat message is deleted. */
Deleted("message.deleted"),
}

/**
* Realtime chat message names.
*/
object PubSubMessageNames {
/** Represents a regular chat message. */
const val ChatMessage = "chat.message"
}

val messageActionNameToAction = mapOf(
/** Represents a message with no action set. */
"message.unset" to MessageAction.MESSAGE_UNSET,

/** Action applied to a new message. */
"message.create" to MessageAction.MESSAGE_CREATE,

/** Action applied to an updated message. */
"message.update" to MessageAction.MESSAGE_UPDATE,

/** Action applied to a deleted message. */
"message.delete" to MessageAction.MESSAGE_DELETE,

/** Action applied to a new annotation. */
"annotation.create" to MessageAction.ANNOTATION_CREATE,

/** Action applied to a deleted annotation. */
"annotation.delete" to MessageAction.ANNOTATION_DELETE,

/** Action applied to a meta occupancy message. */
"meta.occupancy" to MessageAction.META_OCCUPANCY,
)

/**
* Enum representing presence events.
*/
Expand Down
27 changes: 8 additions & 19 deletions chat-android/src/main/java/com/ably/chat/Message.kt
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package com.ably.chat

import io.ably.lib.types.MessageAction

/**
* {@link Headers} type for chat messages.
*/
Expand All @@ -17,7 +19,7 @@ data class Message(
/**
* The unique identifier of the message.
*/
val timeserial: String,
val serial: String,

/**
* The clientId of the user who created the message.
Expand Down Expand Up @@ -67,22 +69,9 @@ data class Message(
* validation. When reading the headers treat them like user input.
*/
val headers: MessageHeaders,
)

/**
* (CHA-M2a)
* @return true if the timeserial of the corresponding realtime channel message comes first.
*/
fun Message.isBefore(other: Message): Boolean = Timeserial.parse(timeserial) < Timeserial.parse(other.timeserial)

/**
* (CHA-M2b)
* @return true if the timeserial of the corresponding realtime channel message comes second.
*/
fun Message.isAfter(other: Message): Boolean = Timeserial.parse(timeserial) > Timeserial.parse(other.timeserial)

/**
* (CHA-M2c)
* @return true if they have the same timeserial.
*/
fun Message.isAtTheSameTime(other: Message): Boolean = Timeserial.parse(timeserial) == Timeserial.parse(other.timeserial)
/**
* The latest action of the message. This can be used to determine if the message was created, updated, or deleted.
*/
val latestAction: MessageAction,
)
24 changes: 9 additions & 15 deletions chat-android/src/main/java/com/ably/chat/Messages.kt
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import io.ably.lib.realtime.ChannelState
import io.ably.lib.realtime.ChannelStateListener
import io.ably.lib.types.AblyException
import io.ably.lib.types.ErrorInfo
import io.ably.lib.types.MessageAction
import io.ably.lib.realtime.Channel as AblyRealtimeChannel

typealias PubSubMessageListener = AblyRealtimeChannel.MessageListener
Expand Down Expand Up @@ -200,18 +201,6 @@ internal class DefaultMessagesSubscription(

override suspend fun getPreviousMessages(start: Long?, end: Long?, limit: Int): PaginatedResult<Message> {
val fromSerial = fromSerialProvider().await()

// (CHA-M5j)
if (end != null && end > Timeserial.parse(fromSerial).timestamp) {
throw AblyException.fromErrorInfo(
ErrorInfo(
"The `end` parameter is specified and is more recent than the subscription point timeserial",
HttpStatusCode.BadRequest,
ErrorCode.BadRequest.code,
),
)
}

val queryOptions = QueryOptions(start = start, end = end, limit = limit, orderBy = NewestFirst)
return chatApi.getMessages(
roomId = roomId,
Expand Down Expand Up @@ -262,20 +251,25 @@ internal class DefaultMessages(
val pubSubMessage = it ?: throw AblyException.fromErrorInfo(
ErrorInfo("Got empty pubsub channel message", HttpStatusCode.BadRequest, ErrorCode.BadRequest.code),
)

// Ignore any action that is not message.create
if (pubSubMessage.action != MessageAction.MESSAGE_CREATE) return@PubSubMessageListener

val data = parsePubSubMessageData(pubSubMessage.data)
val chatMessage = Message(
roomId = roomId,
createdAt = pubSubMessage.timestamp,
clientId = pubSubMessage.clientId,
timeserial = pubSubMessage.extras.asJsonObject().requireString("timeserial"),
serial = pubSubMessage.serial,
text = data.text,
metadata = data.metadata,
headers = pubSubMessage.extras.asJsonObject().get("headers")?.toMap() ?: mapOf(),
latestAction = MessageAction.MESSAGE_CREATE,
)
listener.onEvent(MessageEvent(type = MessageEventType.Created, message = chatMessage))
}
// (CHA-M4d)
channel.subscribe(MessageEventType.Created.eventName, messageListener)
channel.subscribe(PubSubMessageNames.ChatMessage, messageListener)
// (CHA-M5) setting subscription point
associateWithCurrentChannelSerial(deferredChannelSerial)

Expand All @@ -284,7 +278,7 @@ internal class DefaultMessages(
roomId = roomId,
subscription = {
removeListener(listener)
channel.unsubscribe(MessageEventType.Created.eventName, messageListener)
channel.unsubscribe(PubSubMessageNames.ChatMessage, messageListener)
},
fromSerialProvider = {
listeners[listener] ?: throw AblyException.fromErrorInfo(
Expand Down
6 changes: 3 additions & 3 deletions chat-android/src/main/java/com/ably/chat/PaginatedResult.kt
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ interface PaginatedResult<T> {
fun hasNext(): Boolean
}

fun <T> AsyncHttpPaginatedResponse?.toPaginatedResult(transform: (JsonElement) -> T): PaginatedResult<T> =
fun <T> AsyncHttpPaginatedResponse?.toPaginatedResult(transform: (JsonElement) -> T?): PaginatedResult<T> =
this?.let { AsyncPaginatedResultWrapper(it, transform) } ?: EmptyPaginatedResult()

private class EmptyPaginatedResult<T> : PaginatedResult<T> {
Expand All @@ -45,9 +45,9 @@ private class EmptyPaginatedResult<T> : PaginatedResult<T> {

private class AsyncPaginatedResultWrapper<T>(
val asyncPaginatedResult: AsyncHttpPaginatedResponse,
val transform: (JsonElement) -> T,
val transform: (JsonElement) -> T?,
) : PaginatedResult<T> {
override val items: List<T> = asyncPaginatedResult.items()?.map(transform) ?: emptyList()
override val items: List<T> = asyncPaginatedResult.items()?.mapNotNull(transform) ?: emptyList()

override suspend fun next(): PaginatedResult<T> = suspendCoroutine { continuation ->
asyncPaginatedResult.next(object : AsyncHttpPaginatedResponse.Callback {
Expand Down
65 changes: 0 additions & 65 deletions chat-android/src/main/java/com/ably/chat/Timeserial.kt

This file was deleted.

13 changes: 8 additions & 5 deletions chat-android/src/test/java/com/ably/chat/ChatApiTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package com.ably.chat

import com.google.gson.JsonObject
import io.ably.lib.types.AblyException
import io.ably.lib.types.MessageAction
import io.mockk.mockk
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.test.runTest
Expand All @@ -26,7 +27,7 @@ class ChatApiTest {
listOf(
JsonObject().apply {
addProperty("foo", "bar")
addProperty("timeserial", "timeserial")
addProperty("serial", "timeserial")
addProperty("roomId", "roomId")
addProperty("clientId", "clientId")
addProperty("text", "hello")
Expand All @@ -40,13 +41,14 @@ class ChatApiTest {
assertEquals(
listOf(
Message(
timeserial = "timeserial",
serial = "timeserial",
roomId = "roomId",
clientId = "clientId",
text = "hello",
createdAt = 1_000_000L,
metadata = mapOf(),
headers = mapOf(),
latestAction = MessageAction.MESSAGE_CREATE,
),
),
messages.items,
Expand Down Expand Up @@ -83,7 +85,7 @@ class ChatApiTest {
realtime,
JsonObject().apply {
addProperty("foo", "bar")
addProperty("timeserial", "timeserial")
addProperty("serial", "timeserial")
addProperty("createdAt", 1_000_000)
},
)
Expand All @@ -92,13 +94,14 @@ class ChatApiTest {

assertEquals(
Message(
timeserial = "timeserial",
serial = "timeserial",
roomId = "roomId",
clientId = "clientId",
text = "hello",
createdAt = 1_000_000L,
headers = mapOf(),
metadata = mapOf(),
latestAction = MessageAction.MESSAGE_CREATE,
),
message,
)
Expand All @@ -108,7 +111,7 @@ class ChatApiTest {
* @nospec
*/
@Test
fun `sendMessage should throw exception if 'timeserial' field is not presented`() = runTest {
fun `sendMessage should throw exception if 'serial' field is not presented`() = runTest {
mockSendMessageApiResponse(
realtime,
JsonObject().apply {
Expand Down
Loading

0 comments on commit 9fa9cbe

Please sign in to comment.