Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -1195,9 +1195,10 @@ internal class DomainMapping(
originalText = originalText,
textHarms = textHarms.orEmpty(),
imageHarms = imageHarms.orEmpty(),
blocklistMatched = blocklistMatched,
blocklistMatched = blocklistMatched ?: blocklistsMatched?.firstOrNull(),
semanticFilterMatched = semanticFilterMatched,
platformCircumvented = platformCircumvented ?: false,
blocklistsMatched = blocklistsMatched.orEmpty(),
)

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,5 @@ internal data class ModerationEntity(
val blocklistMatched: String?,
val semanticFilterMatched: String?,
val platformCircumvented: Boolean,
val blocklistsMatched: List<String> = emptyList(),
)
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import io.getstream.chat.android.models.ModerationAction
/**
* Maps the domain model [Moderation] to the database model [ModerationEntity].
*/
@Suppress("DEPRECATION")
internal fun Moderation.toEntity() = ModerationEntity(
action = action.value,
originalText = originalText,
Expand All @@ -30,6 +31,7 @@ internal fun Moderation.toEntity() = ModerationEntity(
blocklistMatched = blocklistMatched,
semanticFilterMatched = semanticFilterMatched,
platformCircumvented = platformCircumvented,
blocklistsMatched = blocklistsMatched,
)

/**
Expand All @@ -43,4 +45,5 @@ internal fun ModerationEntity.toDomain() = Moderation(
blocklistMatched = blocklistMatched,
semanticFilterMatched = semanticFilterMatched,
platformCircumvented = platformCircumvented,
blocklistsMatched = blocklistsMatched,
)
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ internal class ModerationAdapter : JsonAdapter<Moderation>() {
var textHarms: List<String>? = null
var imageHarms: List<String>? = null
var blocklistMatched: String? = null
var blocklistsMatched: List<String>? = null
var semanticFilterMatched: String? = null
var platformCircumvented: Boolean? = null

Expand All @@ -42,6 +43,7 @@ internal class ModerationAdapter : JsonAdapter<Moderation>() {
"text_harms" -> textHarms = JsonParsingUtils.parseStringList(reader)
"image_harms" -> imageHarms = JsonParsingUtils.parseStringList(reader)
"blocklist_matched" -> blocklistMatched = JsonParsingUtils.readNullableString(reader)
"blocklists_matched" -> blocklistsMatched = JsonParsingUtils.parseStringList(reader)
"semantic_filter_matched" -> semanticFilterMatched = JsonParsingUtils.readNullableString(reader)
"platform_circumvented" -> platformCircumvented = JsonParsingUtils.readNullableBoolean(reader)
else -> reader.skipValue()
Expand All @@ -57,9 +59,10 @@ internal class ModerationAdapter : JsonAdapter<Moderation>() {
originalText = originalText,
textHarms = textHarms.orEmpty(),
imageHarms = imageHarms.orEmpty(),
blocklistMatched = blocklistMatched,
blocklistMatched = blocklistMatched ?: blocklistsMatched?.firstOrNull(),
semanticFilterMatched = semanticFilterMatched,
platformCircumvented = platformCircumvented ?: false,
blocklistsMatched = blocklistsMatched.orEmpty(),
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -925,6 +925,7 @@ internal object Mother {
blocklistMatched: String = randomString(),
semanticFilterMatched: String = randomString(),
platformCircumvented: Boolean = randomBoolean(),
blocklistsMatched: List<String> = listOf(randomString()),
): ModerationV2Response = ModerationV2Response(
action = action,
originalText = originalText,
Expand All @@ -933,6 +934,7 @@ internal object Mother {
blocklistMatched = blocklistMatched,
semanticFilterMatched = semanticFilterMatched,
platformCircumvented = platformCircumvented,
blocklistsMatched = blocklistsMatched,
)

fun randomQueryUsersRequest(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1648,10 +1648,25 @@ internal class DomainMappingTest {
blocklistMatched = moderationResponse.blocklistMatched,
semanticFilterMatched = moderationResponse.semanticFilterMatched,
platformCircumvented = moderationResponse.platformCircumvented ?: false,
blocklistsMatched = moderationResponse.blocklistsMatched.orEmpty(),
)
assertEquals(expected, moderation)
}

@Test
fun `ModerationV2Response without blocklist_matched falls back to the first blocklists_matched entry`() {
val blocklists = listOf(randomString(), randomString())
val moderationResponse = randomModerationV2Response().copy(
blocklistMatched = null,
blocklistsMatched = blocklists,
)
val sut = Fixture().get()
val moderation = with(sut) { moderationResponse.toDomain() }
@Suppress("DEPRECATION")
assertEquals(blocklists.first(), moderation.blocklistMatched)
assertEquals(blocklists, moderation.blocklistsMatched)
}

@Test
fun `SearchWarning is correctly mapped to the domain model`() {
val response = randomSearchWarningResponse()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/*
* Copyright (c) 2014-2026 Stream.io Inc. All rights reserved.
*
* Licensed under the Stream License;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://github.com/GetStream/stream-chat-android/blob/main/LICENSE
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package io.getstream.chat.android.client.internal.offline.repository.domain.message.internal

import io.getstream.chat.android.client.internal.offline.repository.database.converter.internal.ModerationConverter
import io.getstream.chat.android.randomModeration
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Test

internal class ModerationMapperTest {

private val converter = ModerationConverter()

@Test
fun `Moderation survives a round trip through the database`() {
val moderation = randomModeration(blocklistsMatched = listOf("custom_blocklist", "other_blocklist"))

val stored = converter.moderationToString(moderation.toEntity())
val result = converter.stringToModeration(stored)?.toDomain()

assertEquals(moderation, result)
}

@Test
fun `Moderation stored without blocklistsMatched reads back as an empty list`() {
val stored = """{"action":"bounce","originalText":"Some text","textHarms":[],"imageHarms":[],""" +
""""blocklistMatched":"custom_blocklist","platformCircumvented":false}"""

val result = converter.stringToModeration(stored)

assertEquals(emptyList<String>(), result?.blocklistsMatched)
assertEquals("custom_blocklist", result?.blocklistMatched)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,13 @@ internal class ModerationParsingTest {
assertEquals(ModerationTestData.expectedOptionalFieldsMissing, domain)
}

@Test
fun `Generated path - falls back to the first blocklists_matched entry`() {
val dto = parser.fromJson(ModerationTestData.jsonOnlyBlocklistsMatched, ModerationV2Response::class.java)
val domain = with(domainMapping) { dto.toDomain() }
assertEquals(ModerationTestData.expectedOnlyBlocklistsMatched, domain)
}

// endregion

// region Direct path (JSON → Moderation via ModerationAdapter)
Expand All @@ -73,6 +80,12 @@ internal class ModerationParsingTest {
assertEquals(ModerationTestData.expectedOptionalFieldsMissing, domain)
}

@Test
fun `Direct path - falls back to the first blocklists_matched entry`() {
val domain = adapter.fromJson(ModerationTestData.jsonOnlyBlocklistsMatched)
assertEquals(ModerationTestData.expectedOnlyBlocklistsMatched, domain)
}

// endregion

// region Explicit null values
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ internal object ModerationTestData {

@Language("JSON")
val jsonAllFields =
"""{"action":"bounce","original_text":"This is offensive","text_harms":["profanity","harassment"],"image_harms":["nudity"],"blocklist_matched":"custom_blocklist","semantic_filter_matched":"hate_speech","platform_circumvented":true}"""
"""{"action":"bounce","original_text":"This is offensive","text_harms":["profanity","harassment"],"image_harms":["nudity"],"blocklist_matched":"custom_blocklist","blocklists_matched":["custom_blocklist","other_blocklist"],"semantic_filter_matched":"hate_speech","platform_circumvented":true}"""

@Language("JSON")
val jsonOptionalFieldsMissing =
Expand All @@ -46,6 +46,22 @@ internal object ModerationTestData {
blocklistMatched = "custom_blocklist",
semanticFilterMatched = "hate_speech",
platformCircumvented = true,
blocklistsMatched = listOf("custom_blocklist", "other_blocklist"),
)

@Language("JSON")
val jsonOnlyBlocklistsMatched =
"""{"action":"bounce","original_text":"Some text","blocklists_matched":["custom_blocklist","other_blocklist"]}"""

val expectedOnlyBlocklistsMatched = Moderation(
action = ModerationAction.bounce,
originalText = "Some text",
textHarms = emptyList(),
imageHarms = emptyList(),
blocklistMatched = "custom_blocklist",
semanticFilterMatched = null,
platformCircumvented = false,
blocklistsMatched = listOf("custom_blocklist", "other_blocklist"),
)

val expectedOptionalFieldsMissing = Moderation(
Expand All @@ -60,7 +76,7 @@ internal object ModerationTestData {

@Language("JSON")
val jsonWithExplicitNulls =
"""{"action":"flag","original_text":"Some text","text_harms":null,"image_harms":null,"blocklist_matched":null,"semantic_filter_matched":null,"platform_circumvented":null}"""
"""{"action":"flag","original_text":"Some text","text_harms":null,"image_harms":null,"blocklist_matched":null,"blocklists_matched":null,"semantic_filter_matched":null,"platform_circumvented":null}"""

val expectedWithExplicitNulls = Moderation(
action = ModerationAction.flag,
Expand Down
9 changes: 6 additions & 3 deletions stream-chat-android-core/api/stream-chat-android-core.api
Original file line number Diff line number Diff line change
Expand Up @@ -1708,19 +1708,22 @@ public final class io/getstream/chat/android/models/MessagesState$Result : io/ge
}

public final class io/getstream/chat/android/models/Moderation {
public fun <init> (Lio/getstream/chat/android/models/ModerationAction;Ljava/lang/String;Ljava/util/List;Ljava/util/List;Ljava/lang/String;Ljava/lang/String;Z)V
public fun <init> (Lio/getstream/chat/android/models/ModerationAction;Ljava/lang/String;Ljava/util/List;Ljava/util/List;Ljava/lang/String;Ljava/lang/String;ZLjava/util/List;)V
public synthetic fun <init> (Lio/getstream/chat/android/models/ModerationAction;Ljava/lang/String;Ljava/util/List;Ljava/util/List;Ljava/lang/String;Ljava/lang/String;ZLjava/util/List;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public final fun component1 ()Lio/getstream/chat/android/models/ModerationAction;
public final fun component2 ()Ljava/lang/String;
public final fun component3 ()Ljava/util/List;
public final fun component4 ()Ljava/util/List;
public final fun component5 ()Ljava/lang/String;
public final fun component6 ()Ljava/lang/String;
public final fun component7 ()Z
public final fun copy (Lio/getstream/chat/android/models/ModerationAction;Ljava/lang/String;Ljava/util/List;Ljava/util/List;Ljava/lang/String;Ljava/lang/String;Z)Lio/getstream/chat/android/models/Moderation;
public static synthetic fun copy$default (Lio/getstream/chat/android/models/Moderation;Lio/getstream/chat/android/models/ModerationAction;Ljava/lang/String;Ljava/util/List;Ljava/util/List;Ljava/lang/String;Ljava/lang/String;ZILjava/lang/Object;)Lio/getstream/chat/android/models/Moderation;
public final fun component8 ()Ljava/util/List;
public final fun copy (Lio/getstream/chat/android/models/ModerationAction;Ljava/lang/String;Ljava/util/List;Ljava/util/List;Ljava/lang/String;Ljava/lang/String;ZLjava/util/List;)Lio/getstream/chat/android/models/Moderation;
public static synthetic fun copy$default (Lio/getstream/chat/android/models/Moderation;Lio/getstream/chat/android/models/ModerationAction;Ljava/lang/String;Ljava/util/List;Ljava/util/List;Ljava/lang/String;Ljava/lang/String;ZLjava/util/List;ILjava/lang/Object;)Lio/getstream/chat/android/models/Moderation;
public fun equals (Ljava/lang/Object;)Z
public final fun getAction ()Lio/getstream/chat/android/models/ModerationAction;
public final fun getBlocklistMatched ()Ljava/lang/String;
public final fun getBlocklistsMatched ()Ljava/util/List;
public final fun getImageHarms ()Ljava/util/List;
public final fun getOriginalText ()Ljava/lang/String;
public final fun getPlatformCircumvented ()Z
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,18 +23,21 @@
* @property originalText The original text of the message.
* @property textHarms The list of harmful text detected in the message.
* @property imageHarms The list of harmful images detected in the message.
* @property blocklistMatched The blocklist matched by the message.
* @property blocklistMatched The first blocklist matched by the message.
* @property semanticFilterMatched The semantic filter matched by the message.
* @property platformCircumvented true/false if the message triggered the platform circumvention model.
* @property blocklistsMatched The blocklists matched by the message.
*/
public data class Moderation(
val action: ModerationAction,
val originalText: String,
val textHarms: List<String>,
Comment thread
coderabbitai[bot] marked this conversation as resolved.
val imageHarms: List<String>,
@Deprecated("Use blocklistsMatched instead.")
val blocklistMatched: String?,

Check warning on line 37 in stream-chat-android-core/src/main/java/io/getstream/chat/android/models/Moderation.kt

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Do not forget to remove this deprecated code someday.

See more on https://sonarcloud.io/project/issues?id=GetStream_stream-chat-android&issues=AaD8yqtZydwYzLjGxv1q&open=AaD8yqtZydwYzLjGxv1q&pullRequest=6773
val semanticFilterMatched: String?,
val platformCircumvented: Boolean,
val blocklistsMatched: List<String> = emptyList(),
Comment thread
coderabbitai[bot] marked this conversation as resolved.
)

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1201,6 +1201,7 @@ public fun randomModeration(
blocklistMatched: String = randomString(),
semanticFilterMatched: String = randomString(),
platformCircumvented: Boolean = randomBoolean(),
blocklistsMatched: List<String> = listOf(randomString()),
): Moderation = Moderation(
action = action,
originalText = originalText,
Expand All @@ -1209,6 +1210,7 @@ public fun randomModeration(
blocklistMatched = blocklistMatched,
semanticFilterMatched = semanticFilterMatched,
platformCircumvented = platformCircumvented,
blocklistsMatched = blocklistsMatched,
)

public fun randomMessageReminder(
Expand Down
Loading