From 5220bc205fad37197b90d1cce951eb1c0e3b48ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wojciech=20Kalici=C5=84ski?= Date: Wed, 21 Aug 2024 20:20:27 +0200 Subject: [PATCH] Add custom event handling --- pubnub-chat-api/api/pubnub-chat-api.api | 23 ++------ .../commonMain/kotlin/com/pubnub/chat/Chat.kt | 4 +- .../pubnub/chat/config/ChatConfiguration.kt | 5 -- .../kotlin/com/pubnub/chat/types/Types.kt | 5 +- .../com/pubnub/chat/internal/ChatImpl.kt | 15 +++++- .../com/pubnub/chat/internal/EventImpl.kt | 14 ++++- .../chat/internal/message/MessageImpl.kt | 14 +++-- .../internal/message/ThreadMessageImpl.kt | 21 ++++---- .../com/pubnub/chat/internal/utils/json.kt | 14 ++++- .../ChatConfigurationIntegrationTest.kt | 52 ++++++++++--------- .../pubnub/integration/ChatIntegrationTest.kt | 25 +++++++++ .../kotlin/com/pubnub/kmp/utils/FakeChat.kt | 2 +- 12 files changed, 119 insertions(+), 75 deletions(-) diff --git a/pubnub-chat-api/api/pubnub-chat-api.api b/pubnub-chat-api/api/pubnub-chat-api.api index 197926e5..7c0eaec9 100644 --- a/pubnub-chat-api/api/pubnub-chat-api.api +++ b/pubnub-chat-api/api/pubnub-chat-api.api @@ -298,6 +298,7 @@ public final class com/pubnub/chat/config/LogLevel : java/lang/Enum { public final class com/pubnub/chat/config/PushNotificationsConfig { public fun (ZLjava/lang/String;Lcom/pubnub/api/enums/PNPushType;Ljava/lang/String;Lcom/pubnub/api/enums/PNPushEnvironment;)V + public synthetic fun (ZLjava/lang/String;Lcom/pubnub/api/enums/PNPushType;Ljava/lang/String;Lcom/pubnub/api/enums/PNPushEnvironment;ILkotlin/jvm/internal/DefaultConstructorMarker;)V public final fun getApnsEnvironment ()Lcom/pubnub/api/enums/PNPushEnvironment; public final fun getApnsTopic ()Ljava/lang/String; public final fun getDeviceGateway ()Lcom/pubnub/api/enums/PNPushType; @@ -439,28 +440,12 @@ public final class com/pubnub/chat/types/EventContent$Companion { } public final class com/pubnub/chat/types/EventContent$Custom : com/pubnub/chat/types/EventContent { - public static final field Companion Lcom/pubnub/chat/types/EventContent$Custom$Companion; - public fun (Ljava/lang/Object;Lcom/pubnub/chat/types/EmitEventMethod;)V - public synthetic fun (Ljava/lang/Object;Lcom/pubnub/chat/types/EmitEventMethod;ILkotlin/jvm/internal/DefaultConstructorMarker;)V - public final fun getData ()Ljava/lang/Object; + public fun (Ljava/util/Map;Lcom/pubnub/chat/types/EmitEventMethod;)V + public synthetic fun (Ljava/util/Map;Lcom/pubnub/chat/types/EmitEventMethod;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun getData ()Ljava/util/Map; public final fun getMethod ()Lcom/pubnub/chat/types/EmitEventMethod; } -public synthetic class com/pubnub/chat/types/EventContent$Custom$$serializer : kotlinx/serialization/internal/GeneratedSerializer { - public static final field INSTANCE Lcom/pubnub/chat/types/EventContent$Custom$$serializer; - public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; - public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lcom/pubnub/chat/types/EventContent$Custom; - public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; - public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; - public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lcom/pubnub/chat/types/EventContent$Custom;)V - public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V - public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer; -} - -public final class com/pubnub/chat/types/EventContent$Custom$Companion { - public final fun serializer ()Lkotlinx/serialization/KSerializer; -} - public final class com/pubnub/chat/types/EventContent$Invite : com/pubnub/chat/types/EventContent { public static final field Companion Lcom/pubnub/chat/types/EventContent$Invite$Companion; public fun (Lcom/pubnub/chat/types/ChannelType;Ljava/lang/String;)V diff --git a/pubnub-chat-api/src/commonMain/kotlin/com/pubnub/chat/Chat.kt b/pubnub-chat-api/src/commonMain/kotlin/com/pubnub/chat/Chat.kt index 483fa69d..931289b3 100644 --- a/pubnub-chat-api/src/commonMain/kotlin/com/pubnub/chat/Chat.kt +++ b/pubnub-chat-api/src/commonMain/kotlin/com/pubnub/chat/Chat.kt @@ -139,7 +139,7 @@ interface Chat { fun listenForEvents( type: KClass, channelId: String, - customMethod: EmitEventMethod? = null, + customMethod: EmitEventMethod = EmitEventMethod.PUBLISH, callback: (event: Event) -> Unit ): AutoCloseable @@ -194,7 +194,7 @@ interface Chat { inline fun Chat.listenForEvents( channelId: String, - customMethod: EmitEventMethod? = null, + customMethod: EmitEventMethod = EmitEventMethod.PUBLISH, noinline callback: (event: Event) -> Unit ): AutoCloseable { return listenForEvents(T::class, channelId, customMethod, callback) diff --git a/pubnub-chat-api/src/commonMain/kotlin/com/pubnub/chat/config/ChatConfiguration.kt b/pubnub-chat-api/src/commonMain/kotlin/com/pubnub/chat/config/ChatConfiguration.kt index 9dce1a58..68591741 100644 --- a/pubnub-chat-api/src/commonMain/kotlin/com/pubnub/chat/config/ChatConfiguration.kt +++ b/pubnub-chat-api/src/commonMain/kotlin/com/pubnub/chat/config/ChatConfiguration.kt @@ -6,7 +6,6 @@ import com.pubnub.chat.types.ChannelType import kotlin.time.Duration import kotlin.time.Duration.Companion.ZERO import kotlin.time.Duration.Companion.seconds -import kotlin.time.Duration.Companion.milliseconds interface ChatConfiguration { val logLevel: LogLevel @@ -59,7 +58,3 @@ fun RateLimitPerChannel( ChannelType.PUBLIC to public, ChannelType.UNKNOWN to unknown, ) - -fun main() { - ChatConfiguration(rateLimitPerChannel = RateLimitPerChannel(direct = 1.seconds, group = 2.seconds)) -} \ No newline at end of file diff --git a/pubnub-chat-api/src/commonMain/kotlin/com/pubnub/chat/types/Types.kt b/pubnub-chat-api/src/commonMain/kotlin/com/pubnub/chat/types/Types.kt index f82859e1..69572730 100644 --- a/pubnub-chat-api/src/commonMain/kotlin/com/pubnub/chat/types/Types.kt +++ b/pubnub-chat-api/src/commonMain/kotlin/com/pubnub/chat/types/Types.kt @@ -2,7 +2,6 @@ package com.pubnub.chat.types import com.pubnub.api.JsonElement import com.pubnub.chat.restrictions.RestrictionType -import kotlinx.serialization.Contextual import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable import kotlinx.serialization.Transient @@ -44,10 +43,8 @@ sealed class EventContent { @SerialName("invite") class Invite(val channelType: ChannelType, val channelId: String) : EventContent() - @Serializable - @SerialName("custom") class Custom( - @Contextual val data: Any, + val data: Map, @Transient val method: EmitEventMethod = EmitEventMethod.PUBLISH ) : EventContent() diff --git a/pubnub-chat-impl/src/commonMain/kotlin/com/pubnub/chat/internal/ChatImpl.kt b/pubnub-chat-impl/src/commonMain/kotlin/com/pubnub/chat/internal/ChatImpl.kt index 18f44774..14d0a50e 100644 --- a/pubnub-chat-impl/src/commonMain/kotlin/com/pubnub/chat/internal/ChatImpl.kt +++ b/pubnub-chat-impl/src/commonMain/kotlin/com/pubnub/chat/internal/ChatImpl.kt @@ -3,6 +3,9 @@ package com.pubnub.chat.internal import com.benasher44.uuid.uuid4 import com.pubnub.api.PubNub import com.pubnub.api.PubNubException +import com.pubnub.api.asMap +import com.pubnub.api.asString +import com.pubnub.api.decode import com.pubnub.api.enums.PNPushType import com.pubnub.api.models.consumer.PNBoundedPage import com.pubnub.api.models.consumer.PNPublishResult @@ -580,7 +583,7 @@ class ChatImpl( override fun listenForEvents( type: KClass, channelId: String, - customMethod: EmitEventMethod?, + customMethod: EmitEventMethod, callback: (event: Event) -> Unit ): AutoCloseable { val handler = fun(_: PubNub, pnEvent: PNEvent) { @@ -588,7 +591,15 @@ class ChatImpl( return } val message = (pnEvent as? MessageResult)?.message ?: return - val eventContent: EventContent = PNDataEncoder.decode(message) + val eventContent: EventContent = try { + PNDataEncoder.decode(message) + } catch (e: Exception) { + if (message.asMap()?.get("type")?.asString() == "custom") { + EventContent.Custom((message.decode() as Map) - "type") + } else { + throw e + } + } @Suppress("UNCHECKED_CAST") val payload = eventContent as? T ?: return diff --git a/pubnub-chat-impl/src/commonMain/kotlin/com/pubnub/chat/internal/EventImpl.kt b/pubnub-chat-impl/src/commonMain/kotlin/com/pubnub/chat/internal/EventImpl.kt index 3631c0fa..0e6efe52 100644 --- a/pubnub-chat-impl/src/commonMain/kotlin/com/pubnub/chat/internal/EventImpl.kt +++ b/pubnub-chat-impl/src/commonMain/kotlin/com/pubnub/chat/internal/EventImpl.kt @@ -1,5 +1,8 @@ package com.pubnub.chat.internal +import com.pubnub.api.asMap +import com.pubnub.api.asString +import com.pubnub.api.decode import com.pubnub.api.models.consumer.history.PNFetchMessageItem import com.pubnub.chat.Chat import com.pubnub.chat.Event @@ -19,10 +22,19 @@ class EventImpl( channelId: String, pnFetchMessageItem: PNFetchMessageItem ): Event { + val eventContent: EventContent = try { + PNDataEncoder.decode(pnFetchMessageItem.message) + } catch (e: Exception) { + if (pnFetchMessageItem.message.asMap()?.get("type")?.asString() == "custom") { + EventContent.Custom((pnFetchMessageItem.message.decode() as Map) - "type") + } else { + throw e + } + } return EventImpl( chat = chat, timetoken = pnFetchMessageItem.timetoken ?: 0, - payload = PNDataEncoder.decode(pnFetchMessageItem.message), + payload = eventContent, channelId = channelId, userId = pnFetchMessageItem.uuid ?: "unknown-user" ) diff --git a/pubnub-chat-impl/src/commonMain/kotlin/com/pubnub/chat/internal/message/MessageImpl.kt b/pubnub-chat-impl/src/commonMain/kotlin/com/pubnub/chat/internal/message/MessageImpl.kt index ad059309..5f2a2756 100644 --- a/pubnub-chat-impl/src/commonMain/kotlin/com/pubnub/chat/internal/message/MessageImpl.kt +++ b/pubnub-chat-impl/src/commonMain/kotlin/com/pubnub/chat/internal/message/MessageImpl.kt @@ -39,11 +39,14 @@ data class MessageImpl( companion object { internal fun fromDTO(chat: ChatInternal, pnMessageResult: PNMessageResult): Message { - val content = chat.config.customPayloads?.getMessageResponseBody?.invoke(pnMessageResult.message) ?: defaultGetMessageResponseBody(pnMessageResult.message) + val content = + chat.config.customPayloads?.getMessageResponseBody?.invoke(pnMessageResult.message) + ?: defaultGetMessageResponseBody(pnMessageResult.message) + ?: EventContent.UnknownMessageFormat(pnMessageResult.message) return MessageImpl( chat, pnMessageResult.timetoken!!, - content!!, // todo handle malformed content (then this is null) + content, pnMessageResult.channel, pnMessageResult.publisher!!, meta = pnMessageResult.userMetadata?.decode() as? Map, @@ -54,12 +57,15 @@ data class MessageImpl( } internal fun fromDTO(chat: ChatInternal, messageItem: PNFetchMessageItem, channelId: String): Message { - val content = chat.config.customPayloads?.getMessageResponseBody?.invoke(messageItem.message) ?: defaultGetMessageResponseBody(messageItem.message) + val content = + chat.config.customPayloads?.getMessageResponseBody?.invoke(messageItem.message) + ?: defaultGetMessageResponseBody(messageItem.message) + ?: EventContent.UnknownMessageFormat(messageItem.message) return MessageImpl( chat, messageItem.timetoken!!, - content!!, + content, channelId, messageItem.uuid!!, messageItem.actions, diff --git a/pubnub-chat-impl/src/commonMain/kotlin/com/pubnub/chat/internal/message/ThreadMessageImpl.kt b/pubnub-chat-impl/src/commonMain/kotlin/com/pubnub/chat/internal/message/ThreadMessageImpl.kt index 2506f26e..299eb5bc 100644 --- a/pubnub-chat-impl/src/commonMain/kotlin/com/pubnub/chat/internal/message/ThreadMessageImpl.kt +++ b/pubnub-chat-impl/src/commonMain/kotlin/com/pubnub/chat/internal/message/ThreadMessageImpl.kt @@ -1,6 +1,5 @@ package com.pubnub.chat.internal.message -import com.pubnub.api.asString import com.pubnub.api.decode import com.pubnub.api.models.consumer.history.PNFetchMessageItem import com.pubnub.api.models.consumer.history.PNFetchMessageItem.Action @@ -10,6 +9,7 @@ import com.pubnub.chat.ThreadMessage import com.pubnub.chat.internal.ChatImpl import com.pubnub.chat.internal.ChatInternal import com.pubnub.chat.internal.channel.ChannelImpl +import com.pubnub.chat.internal.defaultGetMessageResponseBody import com.pubnub.chat.internal.error.PubNubErrorMessage.PARENT_CHANNEL_DOES_NOT_EXISTS import com.pubnub.chat.internal.serialization.PNDataEncoder import com.pubnub.chat.internal.util.pnError @@ -53,11 +53,15 @@ data class ThreadMessageImpl( private val log = logging() internal fun fromDTO(chat: ChatImpl, pnMessageResult: PNMessageResult, parentChannelId: String): ThreadMessage { + val content = + chat.config.customPayloads?.getMessageResponseBody?.invoke(pnMessageResult.message) + ?: defaultGetMessageResponseBody(pnMessageResult.message) + ?: EventContent.UnknownMessageFormat(pnMessageResult.message) return ThreadMessageImpl( chat, parentChannelId, pnMessageResult.timetoken!!, - PNDataEncoder.decode(pnMessageResult.message) as EventContent.TextMessageContent, + content, pnMessageResult.channel, pnMessageResult.publisher!!, meta = pnMessageResult.userMetadata?.decode() as? Map, @@ -68,19 +72,16 @@ data class ThreadMessageImpl( } internal fun fromDTO(chat: ChatInternal, messageItem: PNFetchMessageItem, channelId: String, parentChannelId: String): ThreadMessage { - val eventContent = try { - messageItem.message.asString()?.let { text -> - EventContent.TextMessageContent(text, null) - } ?: PNDataEncoder.decode(messageItem.message) - } catch (e: Exception) { - EventContent.UnknownMessageFormat(messageItem.message) - } + val content = + chat.config.customPayloads?.getMessageResponseBody?.invoke(messageItem.message) + ?: defaultGetMessageResponseBody(messageItem.message) + ?: EventContent.UnknownMessageFormat(messageItem.message) return ThreadMessageImpl( chat = chat, parentChannelId = parentChannelId, timetoken = messageItem.timetoken!!, - content = eventContent, + content = content, channelId = channelId, userId = messageItem.uuid!!, actions = messageItem.actions, diff --git a/pubnub-chat-impl/src/commonMain/kotlin/com/pubnub/chat/internal/utils/json.kt b/pubnub-chat-impl/src/commonMain/kotlin/com/pubnub/chat/internal/utils/json.kt index d7706d5e..0d7ab220 100644 --- a/pubnub-chat-impl/src/commonMain/kotlin/com/pubnub/chat/internal/utils/json.kt +++ b/pubnub-chat-impl/src/commonMain/kotlin/com/pubnub/chat/internal/utils/json.kt @@ -1,6 +1,7 @@ import com.pubnub.chat.internal.defaultGetMessagePublishBody import com.pubnub.chat.internal.serialization.PNDataEncoder import com.pubnub.chat.types.EventContent +import kotlinx.serialization.InternalSerializationApi internal fun Any?.tryLong(): Long? { return when (this) { @@ -41,10 +42,19 @@ internal fun EventContent.TextMessageContent.encodeForSending( return finalMessage } +@OptIn(InternalSerializationApi::class) internal fun EventContent.encodeForSending( mergeMessageWith: Map? = null, -): Map { - var finalMessage = PNDataEncoder.encode(this) as Map +): Map { + var finalMessage = if (this is EventContent.Custom) { + buildMap { + putAll(data) + put("type", "custom") + } + } else { + PNDataEncoder.encode(this) as Map + } + if (mergeMessageWith != null) { finalMessage = buildMap { putAll(finalMessage) diff --git a/pubnub-chat-impl/src/commonTest/kotlin/com/pubnub/integration/ChatConfigurationIntegrationTest.kt b/pubnub-chat-impl/src/commonTest/kotlin/com/pubnub/integration/ChatConfigurationIntegrationTest.kt index ca127870..bedb2620 100644 --- a/pubnub-chat-impl/src/commonTest/kotlin/com/pubnub/integration/ChatConfigurationIntegrationTest.kt +++ b/pubnub-chat-impl/src/commonTest/kotlin/com/pubnub/integration/ChatConfigurationIntegrationTest.kt @@ -21,35 +21,37 @@ import kotlin.test.assertFalse class ChatConfigurationIntegrationTest : BaseChatIntegrationTest() { @Test fun custom_payloads_send_receive_msgs() = runTest { - val chat = ChatImpl(ChatConfiguration( - customPayloads = CustomPayloads( - getMessagePublishBody = { content, channelId -> - mapOf( + val chat = ChatImpl( + ChatConfiguration( + customPayloads = CustomPayloads( + getMessagePublishBody = { content, channelId -> + mapOf( "custom" to mapOf( "payload" to mapOf( - "text" to content.text - ) + "text" to content.text ) - , - "files" to content.files, + ), + "files" to content.files, // "type" to "text" - ) - }, - getMessageResponseBody = { json: JsonElement -> - EventContent.TextMessageContent( - json.asMap()?.get("custom")?.asMap()?.get("payload")?.asMap()?.get("text")?.asString()!!, - json.asList()?.map { - File( - it.asMap()?.get("name")?.asString()!!, - it.asMap()?.get("id")?.asString()!!, - it.asMap()?.get("url")?.asString()!!, - it.asMap()?.get("type")?.asString(), + ) + }, + getMessageResponseBody = { json: JsonElement -> + EventContent.TextMessageContent( + json.asMap()?.get("custom")?.asMap()?.get("payload")?.asMap()?.get("text")?.asString()!!, + json.asList()?.map { + File( + it.asMap()?.get("name")?.asString()!!, + it.asMap()?.get("id")?.asString()!!, + it.asMap()?.get("url")?.asString()!!, + it.asMap()?.get("type")?.asString(), ) - } - ) - } - ) - ), pubnub).initialize().await() + } + ) + } + ) + ), + pubnub + ).initialize().await() val channel = chat.createChannel(randomString()).await() val messageText = randomString() val message = CompletableDeferred() @@ -67,4 +69,4 @@ class ChatConfigurationIntegrationTest : BaseChatIntegrationTest() { unsubscribe?.close() } } -} \ No newline at end of file +} diff --git a/pubnub-chat-impl/src/commonTest/kotlin/com/pubnub/integration/ChatIntegrationTest.kt b/pubnub-chat-impl/src/commonTest/kotlin/com/pubnub/integration/ChatIntegrationTest.kt index 8f299f73..cc89b50f 100644 --- a/pubnub-chat-impl/src/commonTest/kotlin/com/pubnub/integration/ChatIntegrationTest.kt +++ b/pubnub-chat-impl/src/commonTest/kotlin/com/pubnub/integration/ChatIntegrationTest.kt @@ -20,6 +20,7 @@ import com.pubnub.chat.membership.MembershipsResponse import com.pubnub.chat.message.GetUnreadMessagesCounts import com.pubnub.chat.message.MarkAllMessageAsReadResponse import com.pubnub.chat.types.ChannelMentionData +import com.pubnub.chat.types.EmitEventMethod import com.pubnub.chat.types.EventContent import com.pubnub.chat.types.GetCurrentUserMentionsResult import com.pubnub.chat.types.GetEventsHistoryResult @@ -31,6 +32,7 @@ import com.pubnub.kmp.CustomObject import com.pubnub.kmp.createCustomObject import com.pubnub.test.await import com.pubnub.test.randomString +import com.pubnub.test.test import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.test.runTest import tryLong @@ -470,6 +472,29 @@ class ChatIntegrationTest : BaseChatIntegrationTest() { chat.pubNub.deleteMessages(listOf(channelId01, userId)) } + @Test + fun emitEvent_with_custom() = runTest { + val channel = chat.createChannel(randomString()).await() + val event = CompletableDeferred>() + var tt: Long = 0 + pubnub.test(backgroundScope, checkAllEvents = false) { + var unsubscribe: AutoCloseable? = null + pubnub.awaitSubscribe { + unsubscribe = chat.listenForEvents(channel.id) { + event.complete(it) + } + } + tt = chat.emitEvent(channel.id, EventContent.Custom(mapOf("abc" to "def"), EmitEventMethod.PUBLISH)).await().timetoken + assertEquals(mapOf("abc" to "def"), event.await().payload.data) + assertFalse(channel.getMembers().await().members.any { it.user.id == chat.currentUser.id }) + unsubscribe?.close() + } + delayInMillis(1000) + val eventFromHistory = chat.getEventsHistory(channel.id, tt + 1, tt).await().events.first() + require(eventFromHistory.payload is EventContent.Custom) + assertEquals(mapOf("abc" to "def"), (eventFromHistory.payload as EventContent.Custom).data) + } + private suspend fun assertPushChannels(expectedNumberOfChannels: Int) { val pushChannels = chat.getPushChannels().await() assertEquals(expectedNumberOfChannels, pushChannels.size) diff --git a/pubnub-chat-impl/src/commonTest/kotlin/com/pubnub/kmp/utils/FakeChat.kt b/pubnub-chat-impl/src/commonTest/kotlin/com/pubnub/kmp/utils/FakeChat.kt index 5a1aecf8..acf3fab4 100644 --- a/pubnub-chat-impl/src/commonTest/kotlin/com/pubnub/kmp/utils/FakeChat.kt +++ b/pubnub-chat-impl/src/commonTest/kotlin/com/pubnub/kmp/utils/FakeChat.kt @@ -203,7 +203,7 @@ abstract class FakeChat(override val config: ChatConfiguration, override val pub override fun listenForEvents( type: KClass, channelId: String, - customMethod: EmitEventMethod?, + customMethod: EmitEventMethod, callback: (event: Event) -> Unit, ): AutoCloseable { TODO("Not yet implemented")