When mapping invalid notification event, only drop that one (#5137)

Previously, this meant the code processing the whole notification batch result failed and other notifications would be lost too.
This commit is contained in:
Jorge Martin Espinosa
2025-08-08 11:56:15 +02:00
committed by GitHub
parent b1a48ec863
commit 16b9440048
5 changed files with 77 additions and 34 deletions

View File

@@ -8,6 +8,7 @@
package io.element.android.libraries.matrix.impl.notification
import io.element.android.libraries.core.bool.orFalse
import io.element.android.libraries.core.extensions.runCatchingExceptions
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.SessionId
@@ -30,31 +31,33 @@ class NotificationMapper(
eventId: EventId,
roomId: RoomId,
notificationItem: NotificationItem
): NotificationData {
return notificationItem.use { item ->
val isDm = isDm(
isDirect = item.roomInfo.isDirect,
activeMembersCount = item.roomInfo.joinedMembersCount.toInt(),
)
NotificationData(
sessionId = sessionId,
eventId = eventId,
// FIXME once the `NotificationItem` in the SDK returns the thread id
threadId = null,
roomId = roomId,
senderAvatarUrl = item.senderInfo.avatarUrl,
senderDisplayName = item.senderInfo.displayName,
senderIsNameAmbiguous = item.senderInfo.isNameAmbiguous,
roomAvatarUrl = item.roomInfo.avatarUrl ?: item.senderInfo.avatarUrl.takeIf { isDm },
roomDisplayName = item.roomInfo.displayName,
isDirect = item.roomInfo.isDirect,
isDm = isDm,
isEncrypted = item.roomInfo.isEncrypted.orFalse(),
isNoisy = item.isNoisy.orFalse(),
timestamp = item.timestamp() ?: clock.epochMillis(),
content = item.event.use { notificationContentMapper.map(it) },
hasMention = item.hasMention.orFalse(),
)
): Result<NotificationData> {
return runCatchingExceptions {
notificationItem.use { item ->
val isDm = isDm(
isDirect = item.roomInfo.isDirect,
activeMembersCount = item.roomInfo.joinedMembersCount.toInt(),
)
NotificationData(
sessionId = sessionId,
eventId = eventId,
// FIXME once the `NotificationItem` in the SDK returns the thread id
threadId = null,
roomId = roomId,
senderAvatarUrl = item.senderInfo.avatarUrl,
senderDisplayName = item.senderInfo.displayName,
senderIsNameAmbiguous = item.senderInfo.isNameAmbiguous,
roomAvatarUrl = item.roomInfo.avatarUrl ?: item.senderInfo.avatarUrl.takeIf { isDm },
roomDisplayName = item.roomInfo.displayName,
isDirect = item.roomInfo.isDirect,
isDm = isDm,
isEncrypted = item.roomInfo.isEncrypted.orFalse(),
isNoisy = item.isNoisy.orFalse(),
timestamp = item.timestamp() ?: clock.epochMillis(),
content = item.event.use { notificationContentMapper.map(it) }.getOrThrow(),
hasMention = item.hasMention.orFalse(),
)
}
}
}
}
@@ -62,11 +65,13 @@ class NotificationMapper(
class NotificationContentMapper {
private val timelineEventToNotificationContentMapper = TimelineEventToNotificationContentMapper()
fun map(notificationEvent: NotificationEvent): NotificationContent =
fun map(notificationEvent: NotificationEvent): Result<NotificationContent> =
when (notificationEvent) {
is NotificationEvent.Timeline -> timelineEventToNotificationContentMapper.map(notificationEvent.event)
is NotificationEvent.Invite -> NotificationContent.Invite(
senderId = UserId(notificationEvent.sender),
is NotificationEvent.Invite -> Result.success(
NotificationContent.Invite(
senderId = UserId(notificationEvent.sender),
)
)
}
}

View File

@@ -53,7 +53,9 @@ class RustNotificationService(
is BatchNotificationResult.Ok -> {
when (val status = result.status) {
is NotificationStatus.Event -> {
put(eventId, Result.success(notificationMapper.map(sessionId, eventId, roomId, status.item)))
val result = notificationMapper.map(sessionId, eventId, roomId, status.item)
result.onFailure { Timber.e(it, "Could not map notification event $eventId") }
put(eventId, result)
}
is NotificationStatus.EventNotFound -> {
Timber.e("Could not retrieve event for notification with $eventId - event not found")

View File

@@ -7,6 +7,7 @@
package io.element.android.libraries.matrix.impl.notification
import io.element.android.libraries.core.extensions.runCatchingExceptions
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.notification.CallNotifyType
@@ -21,10 +22,12 @@ import org.matrix.rustcomponents.sdk.TimelineEventType
import org.matrix.rustcomponents.sdk.use
class TimelineEventToNotificationContentMapper {
fun map(timelineEvent: TimelineEvent): NotificationContent {
return timelineEvent.use {
timelineEvent.eventType().use { eventType ->
eventType.toContent(senderId = UserId(timelineEvent.senderId()))
fun map(timelineEvent: TimelineEvent): Result<NotificationContent> {
return runCatchingExceptions {
timelineEvent.use {
timelineEvent.eventType().use { eventType ->
eventType.toContent(senderId = UserId(timelineEvent.senderId()))
}
}
}
}

View File

@@ -14,7 +14,7 @@ import org.matrix.rustcomponents.sdk.NoPointer
import org.matrix.rustcomponents.sdk.TimelineEvent
import org.matrix.rustcomponents.sdk.TimelineEventType
class FakeFfiTimelineEvent(
open class FakeFfiTimelineEvent(
val timestamp: ULong = A_FAKE_TIMESTAMP.toULong(),
val timelineEventType: TimelineEventType = aRustTimelineEventTypeMessageLike(),
val senderId: String = A_USER_ID_2.value,

View File

@@ -12,8 +12,12 @@ import io.element.android.libraries.matrix.api.exception.NotificationResolverExc
import io.element.android.libraries.matrix.api.notification.NotificationContent
import io.element.android.libraries.matrix.api.timeline.item.event.TextMessageType
import io.element.android.libraries.matrix.impl.fixtures.factories.aRustBatchNotificationResult
import io.element.android.libraries.matrix.impl.fixtures.factories.aRustNotificationEventTimeline
import io.element.android.libraries.matrix.impl.fixtures.factories.aRustNotificationItem
import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeFfiNotificationClient
import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeFfiTimelineEvent
import io.element.android.libraries.matrix.test.AN_EVENT_ID
import io.element.android.libraries.matrix.test.AN_EVENT_ID_2
import io.element.android.libraries.matrix.test.A_MESSAGE
import io.element.android.libraries.matrix.test.A_ROOM_ID
import io.element.android.libraries.matrix.test.A_SESSION_ID
@@ -26,6 +30,8 @@ import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runTest
import org.junit.Test
import org.matrix.rustcomponents.sdk.NotificationClient
import org.matrix.rustcomponents.sdk.NotificationStatus
import org.matrix.rustcomponents.sdk.TimelineEventType
class RustNotificationServiceTest {
@Test
@@ -49,6 +55,33 @@ class RustNotificationServiceTest {
)
}
@Test
fun `test mapping invalid item only drops that item`() = runTest {
val error = IllegalStateException("This event type is not supported")
val faultyEvent = object : FakeFfiTimelineEvent() {
override fun eventType(): TimelineEventType {
throw error
}
}
val notificationClient = FakeFfiNotificationClient(
notificationItemResult = mapOf(
AN_EVENT_ID.value to aRustBatchNotificationResult(
notificationStatus = NotificationStatus.Event(aRustNotificationItem(aRustNotificationEventTimeline(faultyEvent)))
),
AN_EVENT_ID_2.value to aRustBatchNotificationResult()
),
)
val sut = createRustNotificationService(
notificationClient = notificationClient,
)
val result = sut.getNotifications(mapOf(A_ROOM_ID to listOf(AN_EVENT_ID, AN_EVENT_ID_2))).getOrThrow()
val exception = result[AN_EVENT_ID]!!.exceptionOrNull()
assertThat(exception).isEqualTo(error)
val successfulResult = result[AN_EVENT_ID_2]
assertThat(successfulResult?.isSuccess).isTrue()
}
@Test
fun `test unable to resolve event`() = runTest {
val notificationClient = FakeFfiNotificationClient(