Skip to content

Commit

Permalink
Add function for getting the moderated message from a report
Browse files Browse the repository at this point in the history
  • Loading branch information
wkal-pubnub committed Mar 3, 2025
1 parent 427df06 commit 8244e29
Show file tree
Hide file tree
Showing 10 changed files with 128 additions and 4 deletions.
41 changes: 40 additions & 1 deletion js-chat/tests/message.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import { generateUUID } from "pubnub"
import {
Channel,
Chat,
Event,
INTERNAL_ADMIN_CHANNEL,
INTERNAL_MODERATION_PREFIX,
Message,
MessageDraft,
CryptoUtils,
Expand All @@ -19,8 +22,12 @@ 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<Message | null>
}

describe("Send message test", () => {
jest.retryTimes(3)
// jest.retryTimes(3)

let chat: Chat
let channel: Channel
Expand Down Expand Up @@ -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"

Expand Down
5 changes: 3 additions & 2 deletions pubnub-chat-api/api/pubnub-chat-api.api
Original file line number Diff line number Diff line change
Expand Up @@ -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 <init> (Ljava/lang/String;Ljava/lang/String;Ljava/lang/Long;Ljava/lang/String;Ljava/lang/String;)V
public synthetic fun <init> (Ljava/lang/String;Ljava/lang/String;Ljava/lang/Long;Ljava/lang/String;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public fun <init> (Ljava/lang/String;Ljava/lang/String;Ljava/lang/Long;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V
public synthetic fun <init> (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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -1078,6 +1080,31 @@ class ChatImpl(
pubNub.destroy()
}

override fun getMessageFromReport(reportEvent: Event<EventContent.Report>, lookupBefore: Duration, lookupAfter: Duration): PNFuture<Message?> {
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)

findMessageBetween(channel, maxTimetoken, end = minTimetoken) { it.meta?.get(METADATA_AUTO_MODERATION_ID) == report.autoModerationId }
} ?: null.asFuture()
}

internal fun findMessageBetween(channel: Channel, start: Long, end: Long, countPerRequest: Int = 100, match: (Message) -> Boolean): PNFuture<Message?> {
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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -63,4 +67,6 @@ interface ChatInternal : Chat {
* @return [PNFuture] containing set of [User]
*/
fun getUserSuggestions(text: String, limit: Int = 10): PNFuture<List<User>>

fun getMessageFromReport(reportEvent: Event<EventContent.Report>, lookupBefore: Duration = 3.seconds, lookupAfter: Duration = 5.seconds): PNFuture<Message?>
}
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Original file line number Diff line number Diff line change
Expand Up @@ -674,6 +674,25 @@ class ChatIntegrationTest : BaseChatIntegrationTest() {
}
}

@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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -61,6 +62,14 @@ abstract class FakeChat(override val config: ChatConfiguration, override val pub
TODO("Not yet implemented")
}

override fun getMessageFromReport(
reportEvent: Event<EventContent.Report>,
lookupBefore: Duration,
lookupAfter: Duration
): PNFuture<Message?> {
TODO("Not yet implemented")
}

override fun removeThreadChannel(
chat: Chat,
message: Message,
Expand Down
19 changes: 19 additions & 0 deletions pubnub-chat-impl/src/jsMain/kotlin/ChatJs.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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")
Expand Down Expand Up @@ -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<MessageJs?> {
val event = EventImpl(
chat,
eventJs.timetoken.toLong(),
PNDataEncoder.decode<EventContent.Report>(createJsonElement(eventJs.payload.unsafeCast<JsMap<Any>>())),
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<ChatJs> {
Expand Down
1 change: 1 addition & 0 deletions src/jsMain/resources/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,7 @@ type ReportEventPayload = {
reportedMessageTimetoken?: string;
reportedMessageChannelId?: string;
reportedUserId?: string;
autoModerationId?: string;
};
type ReceiptEventPayload = {
messageTimetoken: string;
Expand Down

0 comments on commit 8244e29

Please sign in to comment.