From 7c664ba4d50f703c690de6305606701b749fbe1a Mon Sep 17 00:00:00 2001 From: benkuly Date: Mon, 17 Feb 2025 12:22:46 +0100 Subject: [PATCH] Consider room forget in room deletion. --- CHANGELOG.md | 2 + .../client/integrationtests/RoomsIT.kt | 88 +++++++++++++++++++ .../jvmTest/resources/data/homeserver.yaml | 1 + .../folivo/trixnity/client/MatrixClient.kt | 35 ++------ .../client/MatrixClientConfiguration.kt | 4 +- .../trixnity/client/room/RoomListHandler.kt | 10 +-- .../client/room/RoomListHandlerTest.kt | 39 ++++++-- .../trixnity/core/ClientEventEmitter.kt | 4 +- 8 files changed, 134 insertions(+), 49 deletions(-) create mode 100644 trixnity-client/integration-tests/src/jvmTest/kotlin/net/folivo/trixnity/client/integrationtests/RoomsIT.kt diff --git a/CHANGELOG.md b/CHANGELOG.md index 704470261..244f9c448 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- Consider room forget in room deletion. + ### Changed ### Deprecated diff --git a/trixnity-client/integration-tests/src/jvmTest/kotlin/net/folivo/trixnity/client/integrationtests/RoomsIT.kt b/trixnity-client/integration-tests/src/jvmTest/kotlin/net/folivo/trixnity/client/integrationtests/RoomsIT.kt new file mode 100644 index 000000000..f10807b01 --- /dev/null +++ b/trixnity-client/integration-tests/src/jvmTest/kotlin/net/folivo/trixnity/client/integrationtests/RoomsIT.kt @@ -0,0 +1,88 @@ +package net.folivo.trixnity.client.integrationtests + +import io.kotest.matchers.shouldBe +import io.ktor.http.* +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.async +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.withTimeout +import net.folivo.trixnity.client.store.membership +import net.folivo.trixnity.client.store.repository.exposed.createExposedRepositoriesModule +import net.folivo.trixnity.client.user +import net.folivo.trixnity.core.ClientEventEmitter.Priority +import net.folivo.trixnity.core.model.events.m.room.Membership.JOIN +import net.folivo.trixnity.core.model.events.m.room.Membership.LEAVE +import net.folivo.trixnity.core.subscribeAsFlow +import org.testcontainers.junit.jupiter.Container +import org.testcontainers.junit.jupiter.Testcontainers +import kotlin.test.AfterTest +import kotlin.test.BeforeTest +import kotlin.test.Test + +@Testcontainers +class RoomsIT { + + private lateinit var client1: StartedClient + private lateinit var client2: StartedClient + + @Container + val synapseDocker = synapseDocker() + + @BeforeTest + fun beforeEach(): Unit = runBlocking { + val baseUrl = URLBuilder( + protocol = URLProtocol.HTTP, + host = synapseDocker.host, + port = synapseDocker.firstMappedPort + ).build() + + client1 = + registerAndStartClient("client1", "user1", baseUrl, createExposedRepositoriesModule(newDatabase())) + client2 = startClient("client2", "user1", baseUrl, createExposedRepositoriesModule(newDatabase())) + } + + @AfterTest + fun afterEach() { + client1.client.close() + client2.client.close() + } + + @Test + fun shouldIncludeLeaveRooms(): Unit = runBlocking(Dispatchers.Default) { + withTimeout(30_000) { + val userId = client1.client.userId + + val room1 = client1.client.api.room.createRoom().getOrThrow() + client1.client.user.getById(room1, userId).first { it?.membership == JOIN } + client2.client.user.getById(room1, userId).first { it?.membership == JOIN } + + client1.client.api.room.leaveRoom(room1) + client1.client.user.getById(room1, userId).first { it?.membership == LEAVE } + client2.client.user.getById(room1, userId).first { it?.membership == LEAVE } + + val sync1 = + async { client1.client.api.sync.subscribeAsFlow(priority = Priority.LAST).first().syncResponse } + val sync2 = + async { client1.client.api.sync.subscribeAsFlow(priority = Priority.LAST).first().syncResponse } + client2.client.api.room.createRoom().getOrThrow() // trigger sync + println(sync1.await().room?.leave) + println(sync2.await().room?.leave) + + client1.client.user.getById(room1, userId).first()?.membership shouldBe LEAVE + client2.client.user.getById(room1, userId).first()?.membership shouldBe LEAVE + + val sync3 = + async { client1.client.api.sync.subscribeAsFlow(priority = Priority.LAST).first().syncResponse } + val sync4 = + async { client1.client.api.sync.subscribeAsFlow(priority = Priority.LAST).first().syncResponse } + client1.client.api.room.forgetRoom(room1) + client2.client.api.room.createRoom().getOrThrow() // trigger sync + println("after forget") + println(sync3.await().room?.leave) // TODO expect an empty list, but was null + println(sync4.await().room?.leave) // TODO expect an empty list, but was null + client1.client.user.getById(room1, userId).first { it == null } + client2.client.user.getById(room1, userId).first { it == null } + } + } +} \ No newline at end of file diff --git a/trixnity-client/integration-tests/src/jvmTest/resources/data/homeserver.yaml b/trixnity-client/integration-tests/src/jvmTest/resources/data/homeserver.yaml index 8f3503628..5de501658 100755 --- a/trixnity-client/integration-tests/src/jvmTest/resources/data/homeserver.yaml +++ b/trixnity-client/integration-tests/src/jvmTest/resources/data/homeserver.yaml @@ -19,6 +19,7 @@ signing_key_path: "/data/localhost.signing.key" trusted_key_servers: - server_name: "matrix.org" report_stats: false +forget_rooms_on_leave: false suppress_key_server_warning: true enable_registration: true enable_registration_without_verification: true diff --git a/trixnity-client/src/commonMain/kotlin/net/folivo/trixnity/client/MatrixClient.kt b/trixnity-client/src/commonMain/kotlin/net/folivo/trixnity/client/MatrixClient.kt index 653962d44..ce04ca586 100644 --- a/trixnity-client/src/commonMain/kotlin/net/folivo/trixnity/client/MatrixClient.kt +++ b/trixnity-client/src/commonMain/kotlin/net/folivo/trixnity/client/MatrixClient.kt @@ -2,41 +2,16 @@ package net.folivo.trixnity.client import io.github.oshai.kotlinlogging.KotlinLogging import io.ktor.http.* -import kotlinx.coroutines.CoroutineExceptionHandler -import kotlinx.coroutines.CoroutineName -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.cancel -import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.* +import kotlinx.coroutines.flow.* import kotlinx.coroutines.flow.SharingStarted.Companion.Eagerly -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.collectLatest -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.flow.flowOf -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.stateIn -import kotlinx.coroutines.launch import net.folivo.trixnity.client.MatrixClient.* import net.folivo.trixnity.client.MatrixClient.LoginState.* import net.folivo.trixnity.client.media.MediaStore -import net.folivo.trixnity.client.store.Account -import net.folivo.trixnity.client.store.AccountStore -import net.folivo.trixnity.client.store.KeyStore -import net.folivo.trixnity.client.store.KeyVerificationState -import net.folivo.trixnity.client.store.MediaCacheMappingStore -import net.folivo.trixnity.client.store.OlmCryptoStore -import net.folivo.trixnity.client.store.RootStore -import net.folivo.trixnity.client.store.ServerData -import net.folivo.trixnity.client.store.ServerDataStore +import net.folivo.trixnity.client.store.* import net.folivo.trixnity.client.utils.RetryLoopFlowState.RUN import net.folivo.trixnity.client.utils.retryWhen -import net.folivo.trixnity.clientserverapi.client.ClassicMatrixAuthProvider -import net.folivo.trixnity.clientserverapi.client.LogoutInfo -import net.folivo.trixnity.clientserverapi.client.MatrixAuthProvider -import net.folivo.trixnity.clientserverapi.client.MatrixClientServerApiClient -import net.folivo.trixnity.clientserverapi.client.SyncState -import net.folivo.trixnity.clientserverapi.client.classic -import net.folivo.trixnity.clientserverapi.client.classicInMemory +import net.folivo.trixnity.clientserverapi.client.* import net.folivo.trixnity.clientserverapi.model.authentication.IdentifierType import net.folivo.trixnity.clientserverapi.model.authentication.LoginType import net.folivo.trixnity.clientserverapi.model.sync.Sync @@ -648,6 +623,7 @@ class MatrixClientImpl internal constructor( userId, config.syncFilter.copy( room = (config.syncFilter.room ?: Filters.RoomFilter()).copy( state = Filters.RoomFilter.StateFilter(lazyLoadMembers = true), + includeLeave = true, ) ) ).getOrThrow().also { log.debug { "set new filter for sync: $it" } } @@ -664,6 +640,7 @@ class MatrixClientImpl internal constructor( userId, config.syncFilter.copy( room = (config.syncFilter.room ?: Filters.RoomFilter()).copy( state = Filters.RoomFilter.StateFilter(lazyLoadMembers = true), + includeLeave = true, ), ) ).getOrThrow().also { log.debug { "set new background filter for sync: $it" } } diff --git a/trixnity-client/src/commonMain/kotlin/net/folivo/trixnity/client/MatrixClientConfiguration.kt b/trixnity-client/src/commonMain/kotlin/net/folivo/trixnity/client/MatrixClientConfiguration.kt index 71e735af9..12ee05ec9 100644 --- a/trixnity-client/src/commonMain/kotlin/net/folivo/trixnity/client/MatrixClientConfiguration.kt +++ b/trixnity-client/src/commonMain/kotlin/net/folivo/trixnity/client/MatrixClientConfiguration.kt @@ -37,9 +37,9 @@ data class MatrixClientConfiguration( var autoJoinUpgradedRooms: Boolean = true, /** - * Delete a room, when it's membership is [Membership.LEAVE]. + * Delete a room, when it's membership is [Membership.LEAVE] and the user called `/forget` or the homeserver has enabled auto-forget. */ - var deleteRoomsOnLeave: Boolean = true, + var deleteRoomsOnForget: Boolean = true, /** * Set the delay, after which a sent outbox message is deleted. The delay is checked each time a sync is received. diff --git a/trixnity-client/src/commonMain/kotlin/net/folivo/trixnity/client/room/RoomListHandler.kt b/trixnity-client/src/commonMain/kotlin/net/folivo/trixnity/client/room/RoomListHandler.kt index 81778a12d..d3f129435 100644 --- a/trixnity-client/src/commonMain/kotlin/net/folivo/trixnity/client/room/RoomListHandler.kt +++ b/trixnity-client/src/commonMain/kotlin/net/folivo/trixnity/client/room/RoomListHandler.kt @@ -338,18 +338,14 @@ class RoomListHandler( internal suspend fun deleteLeftRooms(syncEvents: SyncEvents) { val syncLeaveRooms = syncEvents.syncResponse.room?.leave?.keys - if (syncLeaveRooms != null && config.deleteRoomsOnLeave) { + if (syncLeaveRooms != null && config.deleteRoomsOnForget) { val existingLeaveRooms = roomStore.getAll().first() .filter { it.value.first()?.membership == Membership.LEAVE } .keys - if ((existingLeaveRooms - syncLeaveRooms).isNotEmpty()) { - log.warn { "there were LEAVE rooms which should have already been deleted (existingLeaveRooms=$existingLeaveRooms syncLeaveRooms=$syncLeaveRooms)" } - } - - val forgetRooms = existingLeaveRooms + syncLeaveRooms - log.trace { "existingLeaveRooms=$existingLeaveRooms syncLeaveRooms=$syncLeaveRooms" } + + val forgetRooms = existingLeaveRooms - syncLeaveRooms if (forgetRooms.isNotEmpty()) { log.debug { "forget rooms: $forgetRooms" } tm.transaction { diff --git a/trixnity-client/src/commonTest/kotlin/net/folivo/trixnity/client/room/RoomListHandlerTest.kt b/trixnity-client/src/commonTest/kotlin/net/folivo/trixnity/client/room/RoomListHandlerTest.kt index 24071d4f0..f5e7f07c6 100644 --- a/trixnity-client/src/commonTest/kotlin/net/folivo/trixnity/client/room/RoomListHandlerTest.kt +++ b/trixnity-client/src/commonTest/kotlin/net/folivo/trixnity/client/room/RoomListHandlerTest.kt @@ -2,8 +2,8 @@ package net.folivo.trixnity.client.room import io.kotest.core.spec.style.ShouldSpec import io.kotest.core.spec.style.scopes.ShouldSpecContainerScope +import io.kotest.matchers.collections.shouldBeEmpty import io.kotest.matchers.shouldBe -import io.kotest.matchers.shouldNotBe import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.cancel @@ -14,7 +14,6 @@ import net.folivo.trixnity.client.store.* import net.folivo.trixnity.clientserverapi.client.SyncEvents import net.folivo.trixnity.clientserverapi.model.sync.Sync import net.folivo.trixnity.clientserverapi.model.sync.Sync.Response.Rooms.JoinedRoom -import net.folivo.trixnity.clientserverapi.model.sync.Sync.Response.Rooms.LeftRoom import net.folivo.trixnity.core.model.EventId import net.folivo.trixnity.core.model.RoomAliasId import net.folivo.trixnity.core.model.RoomId @@ -800,23 +799,45 @@ class RoomListHandlerTest : ShouldSpec({ } } context(RoomListHandler::deleteLeftRooms.name) { - should("forget rooms on leave when activated") { + should("forget rooms on forget when activated") { + roomStore.update(roomId) { simpleRoom.copy(membership = Membership.LEAVE) } + + roomStore.getAll().first { it.size == 1 } + cut.deleteLeftRooms( SyncEvents( Sync.Response( nextBatch = "", room = Sync.Response.Rooms( - leave = mapOf(roomId to LeftRoom()) + leave = mapOf() ) - ), - emptyList() + ), emptyList() ) ) forgetRooms shouldBe listOf(roomId) } + should("not forget rooms when still part of sync") { + config.deleteRoomsOnForget = false + roomStore.update(roomId) { simpleRoom.copy(membership = Membership.LEAVE) } + + roomStore.getAll().first { it.size == 1 } + + cut.deleteLeftRooms( + SyncEvents( + Sync.Response( + nextBatch = "", + room = Sync.Response.Rooms( + leave = mapOf(roomId to Sync.Response.Rooms.LeftRoom()) + ) + ), emptyList() + ) + ) + + forgetRooms.shouldBeEmpty() + } should("not forget rooms on leave when disabled") { - config.deleteRoomsOnLeave = false + config.deleteRoomsOnForget = false roomStore.update(roomId) { simpleRoom.copy(membership = Membership.LEAVE) } roomStore.getAll().first { it.size == 1 } @@ -826,13 +847,13 @@ class RoomListHandlerTest : ShouldSpec({ Sync.Response( nextBatch = "", room = Sync.Response.Rooms( - leave = mapOf(roomId to LeftRoom()) + leave = mapOf() ) ), emptyList() ) ) - roomStore.get(roomId).first() shouldNotBe null + forgetRooms.shouldBeEmpty() } } }) \ No newline at end of file diff --git a/trixnity-core/src/commonMain/kotlin/net/folivo/trixnity/core/ClientEventEmitter.kt b/trixnity-core/src/commonMain/kotlin/net/folivo/trixnity/core/ClientEventEmitter.kt index 70c4a112d..9df105f6f 100644 --- a/trixnity-core/src/commonMain/kotlin/net/folivo/trixnity/core/ClientEventEmitter.kt +++ b/trixnity-core/src/commonMain/kotlin/net/folivo/trixnity/core/ClientEventEmitter.kt @@ -294,13 +294,13 @@ fun >> ClientEventEmitter.subscribeAsFlow(priority: I awaitClose { unsubscribe() } } -/** +/**^ * Subscribe with a flow. * * If you want, that exceptions are passed to the sync loop (so sync is cancelled on an error), * you should use [subscribeContent] and unsubscribe. */ -fun ClientEventEmitter<*>.subscribeChangeAsFlow(priority: Int = Priority.DEFAULT): Flow = callbackFlow { +fun ClientEventEmitter<*>.subscribeSyncEventsAsFlow(priority: Int = Priority.DEFAULT): Flow = callbackFlow { val unsubscribe = subscribe(priority) { send(Unit) } awaitClose { unsubscribe() } } -- GitLab