Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add function for getting the moderated message from a report #178

Draft
wants to merge 1 commit into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions 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,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<Message | null>
}

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

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,37 @@ 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)
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<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 @@ -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
Expand Down Expand Up @@ -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<EventContent.Report>

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)
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
Loading