diff --git a/js-chat/tests/message.test.ts b/js-chat/tests/message.test.ts index 0a507364..99644559 100644 --- a/js-chat/tests/message.test.ts +++ b/js-chat/tests/message.test.ts @@ -1,7 +1,10 @@ +import { generateUUID } from "pubnub" import { Channel, Chat, + Event, INTERNAL_ADMIN_CHANNEL, + INTERNAL_MODERATION_PREFIX, Message, MessageDraft, CryptoUtils, @@ -19,6 +22,10 @@ import { import { jest } from "@jest/globals" import * as fs from "fs" +declare class ChatInternal extends Chat { + getMessageFromReport(eventJs: Event<"report">, lookupBeforeMillis?: number, lookupAfterMillis?: number): Promise +} + describe("Send message test", () => { jest.retryTimes(3) @@ -485,7 +492,39 @@ describe("Send message test", () => { expect(reportMessage?.payload.reportedUserId).toBe(reportedMessage.userId) }) + test.only("should find a message from auto moderation report", async () => { + const messageText = "Test message to be reported" + const modId = generateUUID() + const reportChannel = INTERNAL_MODERATION_PREFIX + channel.id + + const reportPayload = { + "text": messageText, + "reason": "auto moderated", + "reportedMessageChannelId": channel.id, + "autoModerationId": modId + }; + + await chat.emitEvent({ + "channel": reportChannel, + "type": "report", + "payload": reportPayload + }) + await sleep(150) + await channel.sendText(messageText, {meta : {"pn_mod_id" : modId}}) + await sleep(150) // history calls have around 130ms of cache time + + const history = await channel.getHistory({ count: 1 }) + const reportedMessage = history.messages[0] + const reportEvents = await chat.getEventsHistory({channel: reportChannel, count: 1 }) + const reportEvent = reportEvents.events[0] + + const message = await (chat as ChatInternal).getMessageFromReport(reportEvent) + + expect(message?.timetoken).toBe(reportedMessage.timetoken) + }) + test.skip("should report a message (deprecated)", async () => { + const messageText = "Test message to be reported" const reportReason = "Inappropriate content" diff --git a/pubnub-chat-api/api/pubnub-chat-api.api b/pubnub-chat-api/api/pubnub-chat-api.api index 5cde03d6..eb06d641 100644 --- a/pubnub-chat-api/api/pubnub-chat-api.api +++ b/pubnub-chat-api/api/pubnub-chat-api.api @@ -663,8 +663,9 @@ public final class com/pubnub/chat/types/EventContent$Receipt$Companion { public final class com/pubnub/chat/types/EventContent$Report : com/pubnub/chat/types/EventContent { public static final field Companion Lcom/pubnub/chat/types/EventContent$Report$Companion; - public fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/Long;Ljava/lang/String;Ljava/lang/String;)V - public synthetic fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/Long;Ljava/lang/String;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/Long;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V + public synthetic fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/Long;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun getAutoModerationId ()Ljava/lang/String; public final fun getReason ()Ljava/lang/String; public final fun getReportedMessageChannelId ()Ljava/lang/String; public final fun getReportedMessageTimetoken ()Ljava/lang/Long; 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 e6ee22cb..2c5f28cd 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 @@ -64,7 +64,8 @@ abstract class EventContent( @Serializable(with = LongAsStringSerializer::class) val reportedMessageTimetoken: Long? = null, val reportedMessageChannelId: String? = null, - val reportedUserId: String?, + val reportedUserId: String? = null, + val autoModerationId: String? = null ) : 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 8010c288..f8110059 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 @@ -33,6 +33,7 @@ import com.pubnub.api.models.consumer.push.PNPushListProvisionsResult import com.pubnub.api.models.consumer.push.PNPushRemoveChannelResult import com.pubnub.api.utils.Clock import com.pubnub.api.utils.Instant +import com.pubnub.api.utils.TimetokenUtil import com.pubnub.api.v2.callbacks.Result import com.pubnub.chat.Channel import com.pubnub.chat.Chat @@ -115,6 +116,7 @@ import com.pubnub.kmp.then import com.pubnub.kmp.thenAsync import encodeForSending import kotlin.reflect.KClass +import kotlin.time.Duration import kotlin.time.Duration.Companion.seconds class ChatImpl( @@ -1078,6 +1080,37 @@ class ChatImpl( pubNub.destroy() } + override fun getMessageFromReport(reportEvent: Event, lookupBefore: Duration, lookupAfter: Duration): PNFuture { + val report = reportEvent.payload + val channel = ChannelImpl(this, id = requireNotNull(report.reportedMessageChannelId)) + return report.reportedMessageTimetoken?.let { messageTimetoken -> + channel.getMessage(messageTimetoken) + } ?: report.autoModerationId?.let { autoModerationId -> + val reportTimetoken = reportEvent.timetoken + val maxTimetoken = TimetokenUtil.instantToTimetoken(TimetokenUtil.timetokenToInstant(reportTimetoken) + lookupAfter) + val minTimetoken = TimetokenUtil.instantToTimetoken(TimetokenUtil.timetokenToInstant(reportTimetoken) - lookupBefore) + val predicate = { message: Message -> message.meta?.get(METADATA_AUTO_MODERATION_ID) == report.autoModerationId } + // let's try to optimize by first getting messages right after the know timetoken + channel.getHistory(endTimetoken = reportTimetoken, count = 50).thenAsync { historyResponse -> + val result = historyResponse.messages.firstOrNull(predicate) + result?.asFuture() + // if that fails, let's check the time range + ?: findMessageBetween(channel, maxTimetoken, end = minTimetoken, match = predicate) + } + } ?: null.asFuture() + } + + internal fun findMessageBetween(channel: Channel, start: Long, end: Long, countPerRequest: Int = 100, match: (Message) -> Boolean): PNFuture { + return channel.getHistory(startTimetoken = start, endTimetoken = end, count = countPerRequest).thenAsync { historyResponse -> + val result = historyResponse.messages.firstOrNull(match) + return@thenAsync result?.asFuture() ?: if (historyResponse.messages.isEmpty()) { + null.asFuture() + } else { + findMessageBetween(channel, historyResponse.messages.minOf { it.timetoken }, end, countPerRequest, match) + } + } + } + private fun getTimetokenFromHistoryMessage( channelId: String, pnFetchMessagesResult: PNFetchMessagesResult diff --git a/pubnub-chat-impl/src/commonMain/kotlin/com/pubnub/chat/internal/ChatInternal.kt b/pubnub-chat-impl/src/commonMain/kotlin/com/pubnub/chat/internal/ChatInternal.kt index 630a685d..b4e452d8 100644 --- a/pubnub-chat-impl/src/commonMain/kotlin/com/pubnub/chat/internal/ChatInternal.kt +++ b/pubnub-chat-impl/src/commonMain/kotlin/com/pubnub/chat/internal/ChatInternal.kt @@ -5,13 +5,17 @@ import com.pubnub.api.models.consumer.message_actions.PNMessageAction import com.pubnub.api.models.consumer.message_actions.PNRemoveMessageActionResult import com.pubnub.chat.Channel import com.pubnub.chat.Chat +import com.pubnub.chat.Event import com.pubnub.chat.Message import com.pubnub.chat.ThreadChannel import com.pubnub.chat.User import com.pubnub.chat.internal.timer.TimerManager import com.pubnub.chat.types.ChannelType +import com.pubnub.chat.types.EventContent import com.pubnub.kmp.CustomObject import com.pubnub.kmp.PNFuture +import kotlin.time.Duration +import kotlin.time.Duration.Companion.seconds interface ChatInternal : Chat { val editMessageActionName: String @@ -63,4 +67,6 @@ interface ChatInternal : Chat { * @return [PNFuture] containing set of [User] */ fun getUserSuggestions(text: String, limit: Int = 10): PNFuture> + + fun getMessageFromReport(reportEvent: Event, lookupBefore: Duration = 3.seconds, lookupAfter: Duration = 5.seconds): PNFuture } diff --git a/pubnub-chat-impl/src/commonMain/kotlin/com/pubnub/chat/internal/Constants.kt b/pubnub-chat-impl/src/commonMain/kotlin/com/pubnub/chat/internal/Constants.kt index cdcdef86..151eef5a 100644 --- a/pubnub-chat-impl/src/commonMain/kotlin/com/pubnub/chat/internal/Constants.kt +++ b/pubnub-chat-impl/src/commonMain/kotlin/com/pubnub/chat/internal/Constants.kt @@ -55,3 +55,5 @@ internal const val RESTRICTION_REASON = "reason" internal const val TYPE_PUBNUB_PRIVATE = "pn.prv" internal const val PREFIX_PUBNUB_PRIVATE = "PN_PRV." internal const val SUFFIX_MUTE_1 = "mute1" + +internal const val METADATA_AUTO_MODERATION_ID = "pn_mod_id" 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 7b53f3c5..65b5cfb5 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 @@ -16,10 +16,13 @@ import com.pubnub.chat.User import com.pubnub.chat.config.ChatConfiguration import com.pubnub.chat.config.PushNotificationsConfig import com.pubnub.chat.internal.ChatImpl +import com.pubnub.chat.internal.ChatInternal +import com.pubnub.chat.internal.INTERNAL_MODERATION_PREFIX import com.pubnub.chat.internal.INTERNAL_USER_MODERATION_CHANNEL_PREFIX import com.pubnub.chat.internal.SUFFIX_MUTE_1 import com.pubnub.chat.internal.UserImpl import com.pubnub.chat.internal.error.PubNubErrorMessage +import com.pubnub.chat.internal.generateRandomUuid import com.pubnub.chat.internal.utils.cyrb53a import com.pubnub.chat.listenForEvents import com.pubnub.chat.membership.MembershipsResponse @@ -674,6 +677,54 @@ class ChatIntegrationTest : BaseChatIntegrationTest() { } } + @Test + fun getMessageFromReport() = runTest { + val messageText = "Test message to be reported" + val modId = generateRandomUuid() + val reportChannel = INTERNAL_MODERATION_PREFIX + channel01.id + + val report = EventContent.Report( + text = messageText, + reason = "auto moderated", + reportedMessageTimetoken = null, + reportedMessageChannelId = channel01.id, + reportedUserId = null, + autoModerationId = modId + ) + chat.emitEvent(reportChannel, report).await() + channel01.sendText(messageText, meta = mapOf("pn_mod_id" to modId)).await() + delayForHistory() + val history = channel01.getHistory(count = 1).await() + val reportedMessage = history.messages[0] + val reportEvents = chat.getEventsHistory(reportChannel, count = 1).await() + + @Suppress("UNCHECKED_CAST") + val reportEvent = reportEvents.events[0] as Event + + val message = (chat as ChatInternal).getMessageFromReport(reportEvent).await() + + assertEquals(reportedMessage.timetoken, message?.timetoken) + } + + @Test + fun findMessageBetween() = runTest { + val tts = (0..20).map { + channel01.sendText("$it", ttl = 1).await() + } + val message = (chat as ChatImpl).findMessageBetween( + channel01, + tts.maxOf { + it.timetoken + }, + end = tts.minOf { it.timetoken }, + countPerRequest = 3 + ) { + it.text == "11" + }.await() + assertNotNull(message) + assertEquals("11", message.text) + } + private suspend fun assertPushChannels(chat: Chat, 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 c4298020..0831b49c 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 @@ -35,6 +35,7 @@ import com.pubnub.chat.user.GetUsersResponse import com.pubnub.kmp.CustomObject import com.pubnub.kmp.PNFuture import kotlin.reflect.KClass +import kotlin.time.Duration abstract class FakeChat(override val config: ChatConfiguration, override val pubNub: PubNub) : ChatInternal { override val timerManager: TimerManager = createTimerManager() @@ -61,6 +62,14 @@ abstract class FakeChat(override val config: ChatConfiguration, override val pub TODO("Not yet implemented") } + override fun getMessageFromReport( + reportEvent: Event, + lookupBefore: Duration, + lookupAfter: Duration + ): PNFuture { + TODO("Not yet implemented") + } + override fun removeThreadChannel( chat: Chat, message: Message, diff --git a/pubnub-chat-impl/src/jsMain/kotlin/ChatJs.kt b/pubnub-chat-impl/src/jsMain/kotlin/ChatJs.kt index 74787b19..566a5e15 100644 --- a/pubnub-chat-impl/src/jsMain/kotlin/ChatJs.kt +++ b/pubnub-chat-impl/src/jsMain/kotlin/ChatJs.kt @@ -4,6 +4,7 @@ import com.pubnub.api.PubNubImpl import com.pubnub.api.createJsonElement import com.pubnub.chat.internal.ChatImpl import com.pubnub.chat.internal.ChatInternal +import com.pubnub.chat.internal.EventImpl import com.pubnub.chat.internal.PUBNUB_CHAT_VERSION import com.pubnub.chat.internal.TYPE_OF_MESSAGE_IS_CUSTOM import com.pubnub.chat.internal.serialization.PNDataEncoder @@ -22,6 +23,7 @@ import com.pubnub.kmp.toMap import kotlin.js.Json import kotlin.js.Promise import kotlin.js.json +import kotlin.time.Duration.Companion.milliseconds @JsExport @JsName("Chat") @@ -398,6 +400,23 @@ class ChatJs internal constructor(val chat: ChatInternal, val config: ChatConfig }.asPromise() } + fun getMessageFromReport(eventJs: EventJs, lookupBeforeMillis: Int = 3000, lookupAfterMillis: Int = 5000): Promise { + val event = EventImpl( + chat, + eventJs.timetoken.toLong(), + PNDataEncoder.decode(createJsonElement(eventJs.payload.unsafeCast>())), + eventJs.channelId, + eventJs.userId + ) + return chat.getMessageFromReport( + event, + lookupBeforeMillis.milliseconds, + lookupAfterMillis.milliseconds, + ).then { + it?.asJs(this) + }.asPromise() + } + companion object { @JsStatic fun init(config: ChatConstructor): Promise { diff --git a/src/jsMain/resources/index.d.ts b/src/jsMain/resources/index.d.ts index 7d7ba6f3..db98214d 100644 --- a/src/jsMain/resources/index.d.ts +++ b/src/jsMain/resources/index.d.ts @@ -176,6 +176,7 @@ type ReportEventPayload = { reportedMessageTimetoken?: string; reportedMessageChannelId?: string; reportedUserId?: string; + autoModerationId?: string; }; type ReceiptEventPayload = { messageTimetoken: string;