From 158e4b9042312f030eb997ec41cd658204d22302 Mon Sep 17 00:00:00 2001 From: Manuel Stahl Date: Mon, 20 Oct 2025 14:31:13 +0200 Subject: [PATCH 01/11] Add support for MSC4140: Cancellable delayed events This is required for MSC4143: MatrixRTC. --- .../client/DelayedEventsClient.kt | 60 +++++++ .../client/MatrixClientServerApiClient.kt | 9 +- .../clientserverapi/client/RoomApiClient.kt | 52 +++++++ .../client/DelayedEventsClientTest.kt | 147 ++++++++++++++++++ .../client/RoomsApiClientTest.kt | 70 ++++++++- .../delayed_events/CancelDelayedEvent.kt | 27 ++++ .../model/delayed_events/DelayedEvents.kt | 92 +++++++++++ .../model/delayed_events/GetDelayedEvents.kt | 35 +++++ .../delayed_events/RestartDelayedEvent.kt | 27 ++++ .../model/delayed_events/SendDelayedEvent.kt | 27 ++++ .../model/rooms/SendDelayedEventResponse.kt | 13 ++ .../model/rooms/SendDelayedMessageEvent.kt | 40 +++++ .../model/rooms/SendDelayedStateEvent.kt | 39 +++++ .../folivo/trixnity/core/MSCAnnotations.kt | 16 ++ .../net/folivo/trixnity/core/model/DelayId.kt | 33 ++++ .../trixnity/core/util/MatrixIdRegex.kt | 13 +- 16 files changed, 697 insertions(+), 3 deletions(-) create mode 100644 trixnity-clientserverapi/trixnity-clientserverapi-client/src/commonMain/kotlin/net/folivo/trixnity/clientserverapi/client/DelayedEventsClient.kt create mode 100644 trixnity-clientserverapi/trixnity-clientserverapi-client/src/commonTest/kotlin/net/folivo/trixnity/clientserverapi/client/DelayedEventsClientTest.kt create mode 100644 trixnity-clientserverapi/trixnity-clientserverapi-model/src/commonMain/kotlin/net/folivo/trixnity/clientserverapi/model/delayed_events/CancelDelayedEvent.kt create mode 100644 trixnity-clientserverapi/trixnity-clientserverapi-model/src/commonMain/kotlin/net/folivo/trixnity/clientserverapi/model/delayed_events/DelayedEvents.kt create mode 100644 trixnity-clientserverapi/trixnity-clientserverapi-model/src/commonMain/kotlin/net/folivo/trixnity/clientserverapi/model/delayed_events/GetDelayedEvents.kt create mode 100644 trixnity-clientserverapi/trixnity-clientserverapi-model/src/commonMain/kotlin/net/folivo/trixnity/clientserverapi/model/delayed_events/RestartDelayedEvent.kt create mode 100644 trixnity-clientserverapi/trixnity-clientserverapi-model/src/commonMain/kotlin/net/folivo/trixnity/clientserverapi/model/delayed_events/SendDelayedEvent.kt create mode 100644 trixnity-clientserverapi/trixnity-clientserverapi-model/src/commonMain/kotlin/net/folivo/trixnity/clientserverapi/model/rooms/SendDelayedEventResponse.kt create mode 100644 trixnity-clientserverapi/trixnity-clientserverapi-model/src/commonMain/kotlin/net/folivo/trixnity/clientserverapi/model/rooms/SendDelayedMessageEvent.kt create mode 100644 trixnity-clientserverapi/trixnity-clientserverapi-model/src/commonMain/kotlin/net/folivo/trixnity/clientserverapi/model/rooms/SendDelayedStateEvent.kt create mode 100644 trixnity-core/src/commonMain/kotlin/net/folivo/trixnity/core/model/DelayId.kt diff --git a/trixnity-clientserverapi/trixnity-clientserverapi-client/src/commonMain/kotlin/net/folivo/trixnity/clientserverapi/client/DelayedEventsClient.kt b/trixnity-clientserverapi/trixnity-clientserverapi-client/src/commonMain/kotlin/net/folivo/trixnity/clientserverapi/client/DelayedEventsClient.kt new file mode 100644 index 000000000..4b55ec758 --- /dev/null +++ b/trixnity-clientserverapi/trixnity-clientserverapi-client/src/commonMain/kotlin/net/folivo/trixnity/clientserverapi/client/DelayedEventsClient.kt @@ -0,0 +1,60 @@ +package net.folivo.trixnity.clientserverapi.client + +import net.folivo.trixnity.clientserverapi.model.delayed_events.CancelDelayedEvent +import net.folivo.trixnity.clientserverapi.model.delayed_events.DelayedEventStatus +import net.folivo.trixnity.clientserverapi.model.delayed_events.DelayedEvents +import net.folivo.trixnity.clientserverapi.model.delayed_events.GetDelayedEvents +import net.folivo.trixnity.clientserverapi.model.delayed_events.RestartDelayedEvent +import net.folivo.trixnity.clientserverapi.model.delayed_events.SendDelayedEvent +import net.folivo.trixnity.core.MSC4140 +import net.folivo.trixnity.core.model.DelayId + +@MSC4140 +interface DelayedEventsClient { + + /** + * @see [GetDelayedEvents] + */ + suspend fun getDelayedEvents( + status: DelayedEventStatus? = null, + delayIds: List? = null, + from: String? = null, + ): Result + + /** + * @see [SendDelayedEvent] + */ + suspend fun sendDelayedEvent(delayId: DelayId): Result + + /** + * @see [CancelDelayedEvent] + */ + suspend fun cancelDelayedEvent(delayId: DelayId): Result + + /** + * @see [RestartDelayedEvent] + */ + suspend fun restartDelayedEvent(delayId: DelayId): Result +} + +@MSC4140 +class DelayedEventsClientImpl( + private val baseClient: MatrixClientServerApiBaseClient, +) : DelayedEventsClient { + + override suspend fun getDelayedEvents( + status: DelayedEventStatus?, + delayIds: List?, + from: String?, + ): Result = + baseClient.request(GetDelayedEvents(status, delayIds, from)) + + override suspend fun sendDelayedEvent(delayId: DelayId): Result = + baseClient.request(SendDelayedEvent(delayId), SendDelayedEvent.Request()) + + override suspend fun cancelDelayedEvent(delayId: DelayId): Result = + baseClient.request(CancelDelayedEvent(delayId), CancelDelayedEvent.Request()) + + override suspend fun restartDelayedEvent(delayId: DelayId): Result = + baseClient.request(RestartDelayedEvent(delayId), RestartDelayedEvent.Request()) +} diff --git a/trixnity-clientserverapi/trixnity-clientserverapi-client/src/commonMain/kotlin/net/folivo/trixnity/clientserverapi/client/MatrixClientServerApiClient.kt b/trixnity-clientserverapi/trixnity-clientserverapi-client/src/commonMain/kotlin/net/folivo/trixnity/clientserverapi/client/MatrixClientServerApiClient.kt index f584c1b61..4d09e7c12 100644 --- a/trixnity-clientserverapi/trixnity-clientserverapi-client/src/commonMain/kotlin/net/folivo/trixnity/clientserverapi/client/MatrixClientServerApiClient.kt +++ b/trixnity-clientserverapi/trixnity-clientserverapi-client/src/commonMain/kotlin/net/folivo/trixnity/clientserverapi/client/MatrixClientServerApiClient.kt @@ -6,6 +6,7 @@ import io.ktor.client.engine.* import io.ktor.http.* import kotlinx.coroutines.* import kotlinx.serialization.json.Json +import net.folivo.trixnity.core.MSC4140 import net.folivo.trixnity.core.serialization.createMatrixEventJson import net.folivo.trixnity.core.serialization.events.DefaultEventContentSerializerMappings import net.folivo.trixnity.core.serialization.events.EventContentSerializerMappings @@ -28,6 +29,9 @@ interface MatrixClientServerApiClient : AutoCloseable { val device: DeviceApiClient val push: PushApiClient + @OptIn(MSC4140::class) + val delayedEvents: DelayedEventsClient + val eventContentSerializerMappings: EventContentSerializerMappings val json: Json @@ -102,6 +106,9 @@ class MatrixClientServerApiClientImpl( override val device = DeviceApiClientImpl(baseClient) override val push = PushApiClientImpl(baseClient) + @OptIn(MSC4140::class) + override val delayedEvents = DelayedEventsClientImpl(baseClient) + override fun close() { coroutineScope.cancel() baseClient.close() @@ -112,4 +119,4 @@ class MatrixClientServerApiClientImpl( close() job.join() } -} \ No newline at end of file +} diff --git a/trixnity-clientserverapi/trixnity-clientserverapi-client/src/commonMain/kotlin/net/folivo/trixnity/clientserverapi/client/RoomApiClient.kt b/trixnity-clientserverapi/trixnity-clientserverapi-client/src/commonMain/kotlin/net/folivo/trixnity/clientserverapi/client/RoomApiClient.kt index 7b4039f5a..bbc0722b7 100644 --- a/trixnity-clientserverapi/trixnity-clientserverapi-client/src/commonMain/kotlin/net/folivo/trixnity/clientserverapi/client/RoomApiClient.kt +++ b/trixnity-clientserverapi/trixnity-clientserverapi-client/src/commonMain/kotlin/net/folivo/trixnity/clientserverapi/client/RoomApiClient.kt @@ -1,6 +1,8 @@ package net.folivo.trixnity.clientserverapi.client import net.folivo.trixnity.clientserverapi.model.rooms.* +import net.folivo.trixnity.core.MSC4140 +import net.folivo.trixnity.core.model.DelayId import net.folivo.trixnity.core.model.EventId import net.folivo.trixnity.core.model.RoomAliasId import net.folivo.trixnity.core.model.RoomId @@ -146,6 +148,18 @@ interface RoomApiClient { asUserId: UserId? = null ): Result + /** + * @see [SendDelayedStateEvent] + */ + @MSC4140 + suspend fun sendDelayedStateEvent( + roomId: RoomId, + eventContent: StateEventContent, + delayMs: Long, // TODO: must be > 0, new type? + stateKey: String = "", + asUserId: UserId? = null + ): Result + /** * @see [SendMessageEvent] */ @@ -156,6 +170,18 @@ interface RoomApiClient { asUserId: UserId? = null ): Result + /** + * @see [SendDelayedMessageEvent] + */ + @MSC4140 + suspend fun sendDelayedMessageEvent( + roomId: RoomId, + eventContent: MessageEventContent, + txnId: String = Random.nextString(22), + delayMs: Long, // TODO: must be > 0, new type? + asUserId: UserId? = null + ): Result + /** * @see [RedactEvent] */ @@ -642,6 +668,19 @@ class RoomApiClientImpl( .mapCatching { it.eventId } } + @MSC4140 + override suspend fun sendDelayedStateEvent( + roomId: RoomId, + eventContent: StateEventContent, + delayMs: Long, + stateKey: String, + asUserId: UserId? + ): Result { + val eventType = contentMappings.state.contentType(eventContent) + return baseClient.request(SendDelayedStateEvent(roomId, eventType, delayMs, stateKey, asUserId), eventContent) + .mapCatching { it.delayId } + } + override suspend fun sendMessageEvent( roomId: RoomId, eventContent: MessageEventContent, @@ -653,6 +692,19 @@ class RoomApiClientImpl( .mapCatching { it.eventId } } + @MSC4140 + override suspend fun sendDelayedMessageEvent( + roomId: RoomId, + eventContent: MessageEventContent, + txnId: String, + delayMs: Long, + asUserId: UserId? + ): Result { + val eventType = contentMappings.message.contentType(eventContent) + return baseClient.request(SendDelayedMessageEvent(roomId, eventType, txnId, delayMs, asUserId), eventContent) + .mapCatching { it.delayId } + } + override suspend fun redactEvent( roomId: RoomId, eventId: EventId, diff --git a/trixnity-clientserverapi/trixnity-clientserverapi-client/src/commonTest/kotlin/net/folivo/trixnity/clientserverapi/client/DelayedEventsClientTest.kt b/trixnity-clientserverapi/trixnity-clientserverapi-client/src/commonTest/kotlin/net/folivo/trixnity/clientserverapi/client/DelayedEventsClientTest.kt new file mode 100644 index 000000000..fecf12db6 --- /dev/null +++ b/trixnity-clientserverapi/trixnity-clientserverapi-client/src/commonTest/kotlin/net/folivo/trixnity/clientserverapi/client/DelayedEventsClientTest.kt @@ -0,0 +1,147 @@ +package net.folivo.trixnity.clientserverapi.client + +import io.kotest.matchers.shouldBe +import io.ktor.client.engine.mock.respond +import io.ktor.client.engine.mock.toByteArray +import io.ktor.http.ContentType +import io.ktor.http.HttpHeaders +import io.ktor.http.HttpMethod +import io.ktor.http.HttpStatusCode +import io.ktor.http.Url +import io.ktor.http.fullPath +import io.ktor.http.headersOf +import kotlinx.coroutines.test.runTest +import net.folivo.trixnity.core.MSC4140 +import net.folivo.trixnity.core.model.DelayId +import net.folivo.trixnity.test.utils.TrixnityBaseTest +import net.folivo.trixnity.testutils.scopedMockEngine +import kotlin.test.Test +import kotlin.test.assertEquals + +@OptIn(MSC4140::class) +class DelayedEventsClientTest : TrixnityBaseTest() { + + @Test + fun shouldGetDelayedEvents() = runTest { + // FIXME: "content" cannot be deserialized + val response = """ + { + "scheduled": [ + { + "delay_id": "d1", + "room_id": "!room:server", + "type": "m.room.message", + "delay": 1000, + "running_since": 123456789, + "content": { + "body": "This is a delayed message.", + "msgtype": "m.text" + } + } + ], + "finalised": [ + { + "delayed_event": { + "delay_id": "d2", + "room_id": "!room:server", + "type": "m.call", + "state_key": "m.call#ROOM", + "delay": 1000, + "running_since": 123456789, + "content": { + "name": "This is another delayed message." + } + }, + "outcome": "send", + "reason": "delay", + "event_id": "123", + "origin_server_ts": 123456789 + } + ], + "next_batch": "foo1234" + } + """.trimIndent() + val matrixRestClient = MatrixClientServerApiClientImpl( + baseUrl = Url("https://matrix.host"), + httpClientEngine = scopedMockEngine { + addHandler { request -> + assertEquals( + "/_matrix/client/unstable/org.matrix.msc4140/delayed_events", + request.url.fullPath) + assertEquals(HttpMethod.Get, request.method) + respond( + response, + HttpStatusCode.OK, + headersOf(HttpHeaders.ContentType, ContentType.Application.Json.toString()) + ) + } + }) + val result = matrixRestClient.delayedEvents.getDelayedEvents().getOrThrow() + result.scheduled.first().delayId shouldBe DelayId("d1") + result.finalised.first().delayedEvent.delayId shouldBe DelayId("d2") + } + + @Test + fun shouldSendDelayedEvent() = runTest { + val matrixRestClient = MatrixClientServerApiClientImpl( + baseUrl = Url("https://matrix.host"), + httpClientEngine = scopedMockEngine { + addHandler { request -> + assertEquals( + "/_matrix/client/unstable/org.matrix.msc4140/delayed_events/d1", + request.url.fullPath + ) + assertEquals(HttpMethod.Post, request.method) + assertEquals("{\"action\":\"send\"}", request.body.toByteArray().decodeToString()) + respond( + "{}", + HttpStatusCode.OK, + headersOf(HttpHeaders.ContentType, ContentType.Application.Json.toString()) + ) + } + }) + matrixRestClient.delayedEvents.sendDelayedEvent(DelayId("d1")).getOrThrow() + } + + @Test + fun shouldCancelDelayedEvent() = runTest { + val matrixRestClient = MatrixClientServerApiClientImpl( + baseUrl = Url("https://matrix.host"), + httpClientEngine = scopedMockEngine { + addHandler { request -> + assertEquals( + "/_matrix/client/unstable/org.matrix.msc4140/delayed_events/d1", + request.url.fullPath) + assertEquals(HttpMethod.Post, request.method) + assertEquals("{\"action\":\"cancel\"}", request.body.toByteArray().decodeToString()) + respond( + "{}", + HttpStatusCode.OK, + headersOf(HttpHeaders.ContentType, ContentType.Application.Json.toString()) + ) + } + }) + matrixRestClient.delayedEvents.cancelDelayedEvent(DelayId("d1")).getOrThrow() + } + + @Test + fun shouldRestartDelayedEvent() = runTest { + val matrixRestClient = MatrixClientServerApiClientImpl( + baseUrl = Url("https://matrix.host"), + httpClientEngine = scopedMockEngine { + addHandler { request -> + assertEquals( + "/_matrix/client/unstable/org.matrix.msc4140/delayed_events/d1", + request.url.fullPath) + assertEquals(HttpMethod.Post, request.method) + assertEquals("{\"action\":\"restart\"}", request.body.toByteArray().decodeToString()) + respond( + "{}", + HttpStatusCode.OK, + headersOf(HttpHeaders.ContentType, ContentType.Application.Json.toString()) + ) + } + }) + matrixRestClient.delayedEvents.restartDelayedEvent(DelayId("d1")).getOrThrow() + } +} diff --git a/trixnity-clientserverapi/trixnity-clientserverapi-client/src/commonTest/kotlin/net/folivo/trixnity/clientserverapi/client/RoomsApiClientTest.kt b/trixnity-clientserverapi/trixnity-clientserverapi-client/src/commonTest/kotlin/net/folivo/trixnity/clientserverapi/client/RoomsApiClientTest.kt index 445ec42da..a7f28b17f 100644 --- a/trixnity-clientserverapi/trixnity-clientserverapi-client/src/commonTest/kotlin/net/folivo/trixnity/clientserverapi/client/RoomsApiClientTest.kt +++ b/trixnity-clientserverapi/trixnity-clientserverapi-client/src/commonTest/kotlin/net/folivo/trixnity/clientserverapi/client/RoomsApiClientTest.kt @@ -8,6 +8,8 @@ import kotlinx.coroutines.test.runTest import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.builtins.ListSerializer import net.folivo.trixnity.clientserverapi.model.rooms.* +import net.folivo.trixnity.core.MSC4140 +import net.folivo.trixnity.core.model.DelayId import net.folivo.trixnity.core.model.EventId import net.folivo.trixnity.core.model.RoomAliasId import net.folivo.trixnity.core.model.RoomId @@ -596,6 +598,38 @@ class RoomsApiClientTest : TrixnityBaseTest() { } } + @OptIn(MSC4140::class) + @Test + fun shouldSendDelayedStateEvent() = runTest { + val response = SendDelayedEventResponse(DelayId("delay")) + val matrixRestClient = MatrixClientServerApiClientImpl( + baseUrl = Url("https://matrix.host"), + httpClientEngine = scopedMockEngine { + addHandler { request -> + assertEquals( + "/_matrix/client/v3/rooms/!room:server/state/m.room.name/someStateKey?delay=123456", + request.url.fullPath + ) + assertEquals(HttpMethod.Put, request.method) + assertEquals("""{"name":"name"}""", request.body.toByteArray().decodeToString()) + respond( + json.encodeToString(response), + HttpStatusCode.OK, + headersOf(HttpHeaders.ContentType, Application.Json.toString()) + ) + } + }) + val eventContent = NameEventContent("name") + + val result = matrixRestClient.room.sendDelayedStateEvent( + roomId = RoomId("!room:server"), + eventContent = eventContent, + stateKey = "someStateKey", + delayMs = 123456, + ).getOrThrow() + assertEquals(DelayId("delay"), result) + } + @Test fun shouldSendRoomEvent() = runTest { val response = SendEventResponse(EventId("event")) @@ -654,6 +688,40 @@ class RoomsApiClientTest : TrixnityBaseTest() { } } + @OptIn(MSC4140::class) + @Test + fun shouldSendDelayedRoomEvent() = runTest { + val response = SendDelayedEventResponse(DelayId("delay")) + val matrixRestClient = MatrixClientServerApiClientImpl( + baseUrl = Url("https://matrix.host"), + httpClientEngine = scopedMockEngine { + addHandler { request -> + assertEquals( + "/_matrix/client/v3/rooms/!room:server/send/m.room.message/someTxnId?delay=123456", + request.url.fullPath + ) + assertEquals(HttpMethod.Put, request.method) + assertEquals( + """{"body":"someBody","msgtype":"m.text"}""", + request.body.toByteArray().decodeToString() + ) + respond( + json.encodeToString(response), + HttpStatusCode.OK, + headersOf(HttpHeaders.ContentType, Application.Json.toString()) + ) + } + }) + val eventContent = RoomMessageEventContent.TextBased.Text("someBody") + val result = matrixRestClient.room.sendDelayedMessageEvent( + roomId = RoomId("!room:server"), + eventContent = eventContent, + txnId = "someTxnId", + delayMs = 123456, + ).getOrThrow() + assertEquals(DelayId("delay"), result) + } + @Test fun shouldSendRedactEvent() = runTest { val response = SendEventResponse(EventId("event")) @@ -2069,4 +2137,4 @@ class RoomsApiClientTest : TrixnityBaseTest() { originTimestamp = 1432735824653, ) } -} \ No newline at end of file +} diff --git a/trixnity-clientserverapi/trixnity-clientserverapi-model/src/commonMain/kotlin/net/folivo/trixnity/clientserverapi/model/delayed_events/CancelDelayedEvent.kt b/trixnity-clientserverapi/trixnity-clientserverapi-model/src/commonMain/kotlin/net/folivo/trixnity/clientserverapi/model/delayed_events/CancelDelayedEvent.kt new file mode 100644 index 000000000..7f1a388bc --- /dev/null +++ b/trixnity-clientserverapi/trixnity-clientserverapi-model/src/commonMain/kotlin/net/folivo/trixnity/clientserverapi/model/delayed_events/CancelDelayedEvent.kt @@ -0,0 +1,27 @@ +package net.folivo.trixnity.clientserverapi.model.delayed_events + +import io.ktor.resources.Resource +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import net.folivo.trixnity.core.HttpMethod +import net.folivo.trixnity.core.HttpMethodType.POST +import net.folivo.trixnity.core.MSC4140 +import net.folivo.trixnity.core.MatrixEndpoint +import net.folivo.trixnity.core.model.DelayId + +/** + * @see matrix spec + */ +@MSC4140 +@Serializable +//@Resource("/_matrix/client/v1/delayed_events/{delayId}") +@Resource("/_matrix/client/unstable/org.matrix.msc4140/delayed_events/{delayId}") +@HttpMethod(POST) +data class CancelDelayedEvent( + @SerialName("delayId") val delayId: DelayId, +) : MatrixEndpoint { + @Serializable + data class Request( + @SerialName("action") val action: String = "cancel", + ) +} diff --git a/trixnity-clientserverapi/trixnity-clientserverapi-model/src/commonMain/kotlin/net/folivo/trixnity/clientserverapi/model/delayed_events/DelayedEvents.kt b/trixnity-clientserverapi/trixnity-clientserverapi-model/src/commonMain/kotlin/net/folivo/trixnity/clientserverapi/model/delayed_events/DelayedEvents.kt new file mode 100644 index 000000000..9404c5299 --- /dev/null +++ b/trixnity-clientserverapi/trixnity-clientserverapi-model/src/commonMain/kotlin/net/folivo/trixnity/clientserverapi/model/delayed_events/DelayedEvents.kt @@ -0,0 +1,92 @@ +package net.folivo.trixnity.clientserverapi.model.delayed_events + +import kotlinx.serialization.Contextual +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import net.folivo.trixnity.core.ErrorResponse +import net.folivo.trixnity.core.MSC4140 +import net.folivo.trixnity.core.model.DelayId +import net.folivo.trixnity.core.model.EventId +import net.folivo.trixnity.core.model.RoomId +import net.folivo.trixnity.core.model.events.EventContent + +@MSC4140 +@Serializable +enum class DelayedEventStatus { + @SerialName("scheduled") + SCHEDULED, + + @SerialName("finalised") + FINALIZED, +} + +/** + * @see MSC4140 + * @param delayId Required. The ID of the delayed event. + * @param roomId Required. The room ID of the delayed event. + * @param type Required. The event type of the delayed event. + * @param stateKey Optional. The state key of the delayed event if it is a state event. + * @param delayMs Required. The delay in milliseconds before the event is to be sent. + * @param runningSince Required. The timestamp (as Unix time in milliseconds) when the delayed event was scheduled or last restarted. + * @param content Required. The content of the delayed event. This is the body of the original PUT request, not a preview of the full event after sending. + */ +@MSC4140 +@Serializable +data class ScheduledDelayedEvent( + @SerialName("delay_id") val delayId: DelayId, + @SerialName("room_id") val roomId: RoomId, + @SerialName("type") val type: String, + @SerialName("state_key") val stateKey: String? = null, + @SerialName("delay") val delayMs: Long, + @SerialName("running_since") val runningSince: Long, + @SerialName("content") val content: @Contextual EventContent, +) + +/** + * @see MSC4140 + * @param delayedEvent Required. Describes the original delayed event in the same format as the items in the scheduled array. + * @param outcome Whether the delayed event was sent, or was cancelled by an error or the management endpoint with an action of "cancel". + * @param reason What caused the delayed event to become finalised. "error" means the delayed event failed to be sent due to an error; "action" means it was sent or cancelled by the management endpoint; and "delay" means it was sent automatically on its scheduled delivery time. + * @param error Optional. A Matrix error (as defined by Standard error response) to explain why this event failed to be sent. + * @param eventId Optional. The event_id this event got in case it was sent. + * @param originServerTs Required. The timestamp of when the event was finalised. + */ +@MSC4140 +@Serializable +data class FinalizedDelayedEvent( + @SerialName("delayed_event") val delayedEvent: ScheduledDelayedEvent, + @SerialName("outcome") val outcome: Outcome, + @SerialName("reason") val reason: Reason, + @SerialName("error") val error: ErrorResponse? = null, + @SerialName("event_id") val eventId: EventId? = null, + @SerialName("origin_server_ts") val originServerTs: Long, +) { + @Serializable + enum class Outcome { + @SerialName("send") + SEND, + + @SerialName("cancel") + CANCEL, + } + + @Serializable + enum class Reason { + @SerialName("error") + ERROR, + + @SerialName("action") + ACTION, + + @SerialName("delay") + DELAY, + } +} + +@MSC4140 +@Serializable +data class DelayedEvents( + @SerialName("scheduled") val scheduled: List, + @SerialName("finalised") val finalised: List, + @SerialName("next_batch") val nextBatch: String? = null, +) diff --git a/trixnity-clientserverapi/trixnity-clientserverapi-model/src/commonMain/kotlin/net/folivo/trixnity/clientserverapi/model/delayed_events/GetDelayedEvents.kt b/trixnity-clientserverapi/trixnity-clientserverapi-model/src/commonMain/kotlin/net/folivo/trixnity/clientserverapi/model/delayed_events/GetDelayedEvents.kt new file mode 100644 index 000000000..efe42f192 --- /dev/null +++ b/trixnity-clientserverapi/trixnity-clientserverapi-model/src/commonMain/kotlin/net/folivo/trixnity/clientserverapi/model/delayed_events/GetDelayedEvents.kt @@ -0,0 +1,35 @@ +package net.folivo.trixnity.clientserverapi.model.delayed_events + +import io.ktor.resources.Resource +import kotlinx.serialization.KSerializer +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import kotlinx.serialization.json.Json +import net.folivo.trixnity.core.HttpMethod +import net.folivo.trixnity.core.HttpMethodType.GET +import net.folivo.trixnity.core.MSC4140 +import net.folivo.trixnity.core.MatrixEndpoint +import net.folivo.trixnity.core.model.DelayId +import net.folivo.trixnity.core.serialization.events.EventContentSerializerMappings + +/** + * @see matrix spec + */ +@MSC4140 +@Serializable +//@Resource("/_matrix/client/v1/delayed_events") +@Resource("/_matrix/client/unstable/org.matrix.msc4140/delayed_events") +@HttpMethod(GET) +data class GetDelayedEvents( + @SerialName("status") val status: DelayedEventStatus?, + @SerialName("delay_id") val delayIds: List?, + @SerialName("from") val from: String?, +) : MatrixEndpoint { + override fun responseSerializerBuilder( + mappings: EventContentSerializerMappings, + json: Json, + value: DelayedEvents? + ): KSerializer { + return requireNotNull(json.serializersModule.getContextual(DelayedEvents::class)) + } +} diff --git a/trixnity-clientserverapi/trixnity-clientserverapi-model/src/commonMain/kotlin/net/folivo/trixnity/clientserverapi/model/delayed_events/RestartDelayedEvent.kt b/trixnity-clientserverapi/trixnity-clientserverapi-model/src/commonMain/kotlin/net/folivo/trixnity/clientserverapi/model/delayed_events/RestartDelayedEvent.kt new file mode 100644 index 000000000..21a04ce9d --- /dev/null +++ b/trixnity-clientserverapi/trixnity-clientserverapi-model/src/commonMain/kotlin/net/folivo/trixnity/clientserverapi/model/delayed_events/RestartDelayedEvent.kt @@ -0,0 +1,27 @@ +package net.folivo.trixnity.clientserverapi.model.delayed_events + +import io.ktor.resources.Resource +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import net.folivo.trixnity.core.HttpMethod +import net.folivo.trixnity.core.HttpMethodType.POST +import net.folivo.trixnity.core.MSC4140 +import net.folivo.trixnity.core.MatrixEndpoint +import net.folivo.trixnity.core.model.DelayId + +/** + * @see matrix spec + */ +@MSC4140 +@Serializable +//@Resource("/_matrix/client/v1/delayed_events/{delayId}") +@Resource("/_matrix/client/unstable/org.matrix.msc4140/delayed_events/{delayId}") +@HttpMethod(POST) +data class RestartDelayedEvent( + @SerialName("delayId") val delayId: DelayId, +) : MatrixEndpoint { + @Serializable + data class Request( + @SerialName("action") val action: String = "restart", + ) +} diff --git a/trixnity-clientserverapi/trixnity-clientserverapi-model/src/commonMain/kotlin/net/folivo/trixnity/clientserverapi/model/delayed_events/SendDelayedEvent.kt b/trixnity-clientserverapi/trixnity-clientserverapi-model/src/commonMain/kotlin/net/folivo/trixnity/clientserverapi/model/delayed_events/SendDelayedEvent.kt new file mode 100644 index 000000000..343f0a35e --- /dev/null +++ b/trixnity-clientserverapi/trixnity-clientserverapi-model/src/commonMain/kotlin/net/folivo/trixnity/clientserverapi/model/delayed_events/SendDelayedEvent.kt @@ -0,0 +1,27 @@ +package net.folivo.trixnity.clientserverapi.model.delayed_events + +import io.ktor.resources.Resource +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import net.folivo.trixnity.core.HttpMethod +import net.folivo.trixnity.core.HttpMethodType.POST +import net.folivo.trixnity.core.MSC4140 +import net.folivo.trixnity.core.MatrixEndpoint +import net.folivo.trixnity.core.model.DelayId + +/** + * @see matrix spec + */ +@MSC4140 +@Serializable +//@Resource("/_matrix/client/v1/delayed_events/{delayId}") +@Resource("/_matrix/client/unstable/org.matrix.msc4140/delayed_events/{delayId}") +@HttpMethod(POST) +data class SendDelayedEvent( + @SerialName("delayId") val delayId: DelayId, +) : MatrixEndpoint { + @Serializable + data class Request( + @SerialName("action") val action: String = "send", + ) +} diff --git a/trixnity-clientserverapi/trixnity-clientserverapi-model/src/commonMain/kotlin/net/folivo/trixnity/clientserverapi/model/rooms/SendDelayedEventResponse.kt b/trixnity-clientserverapi/trixnity-clientserverapi-model/src/commonMain/kotlin/net/folivo/trixnity/clientserverapi/model/rooms/SendDelayedEventResponse.kt new file mode 100644 index 000000000..e9ae75df5 --- /dev/null +++ b/trixnity-clientserverapi/trixnity-clientserverapi-model/src/commonMain/kotlin/net/folivo/trixnity/clientserverapi/model/rooms/SendDelayedEventResponse.kt @@ -0,0 +1,13 @@ +package net.folivo.trixnity.clientserverapi.model.rooms + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import net.folivo.trixnity.core.model.EventId +import net.folivo.trixnity.core.MSC4140 +import net.folivo.trixnity.core.model.DelayId + +@MSC4140 +@Serializable +data class SendDelayedEventResponse( + @SerialName("delay_id") val delayId: DelayId, +) diff --git a/trixnity-clientserverapi/trixnity-clientserverapi-model/src/commonMain/kotlin/net/folivo/trixnity/clientserverapi/model/rooms/SendDelayedMessageEvent.kt b/trixnity-clientserverapi/trixnity-clientserverapi-model/src/commonMain/kotlin/net/folivo/trixnity/clientserverapi/model/rooms/SendDelayedMessageEvent.kt new file mode 100644 index 000000000..99a300554 --- /dev/null +++ b/trixnity-clientserverapi/trixnity-clientserverapi-model/src/commonMain/kotlin/net/folivo/trixnity/clientserverapi/model/rooms/SendDelayedMessageEvent.kt @@ -0,0 +1,40 @@ +package net.folivo.trixnity.clientserverapi.model.rooms + +import io.ktor.resources.* +import kotlinx.serialization.KSerializer +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import kotlinx.serialization.json.Json +import net.folivo.trixnity.core.HttpMethod +import net.folivo.trixnity.core.HttpMethodType.PUT +import net.folivo.trixnity.core.MSC4140 +import net.folivo.trixnity.core.MatrixEndpoint +import net.folivo.trixnity.core.model.RoomId +import net.folivo.trixnity.core.model.UserId +import net.folivo.trixnity.core.model.events.MessageEventContent +import net.folivo.trixnity.core.serialization.events.EventContentSerializerMappings +import net.folivo.trixnity.core.serialization.events.contentSerializer + +/** + * @see matrix spec + */ +@MSC4140 +@Serializable +@Resource("/_matrix/client/v3/rooms/{roomId}/send/{type}/{txnId}") +@HttpMethod(PUT) +data class SendDelayedMessageEvent( + @SerialName("roomId") val roomId: RoomId, + @SerialName("type") val type: String, + @SerialName("txnId") val txnId: String, + @SerialName("delay") val delayMs: Long, + @SerialName("user_id") val asUserId: UserId? = null, + @SerialName("ts") val ts: Long? = null, +) : MatrixEndpoint { + override fun requestSerializerBuilder( + mappings: EventContentSerializerMappings, + json: Json, + value: MessageEventContent? + ): KSerializer { + return mappings.message.contentSerializer(type, value) + } +} diff --git a/trixnity-clientserverapi/trixnity-clientserverapi-model/src/commonMain/kotlin/net/folivo/trixnity/clientserverapi/model/rooms/SendDelayedStateEvent.kt b/trixnity-clientserverapi/trixnity-clientserverapi-model/src/commonMain/kotlin/net/folivo/trixnity/clientserverapi/model/rooms/SendDelayedStateEvent.kt new file mode 100644 index 000000000..83cc98a98 --- /dev/null +++ b/trixnity-clientserverapi/trixnity-clientserverapi-model/src/commonMain/kotlin/net/folivo/trixnity/clientserverapi/model/rooms/SendDelayedStateEvent.kt @@ -0,0 +1,39 @@ +package net.folivo.trixnity.clientserverapi.model.rooms + +import io.ktor.resources.* +import kotlinx.serialization.KSerializer +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import kotlinx.serialization.json.Json +import net.folivo.trixnity.core.HttpMethod +import net.folivo.trixnity.core.HttpMethodType.PUT +import net.folivo.trixnity.core.MSC4140 +import net.folivo.trixnity.core.MatrixEndpoint +import net.folivo.trixnity.core.model.RoomId +import net.folivo.trixnity.core.model.UserId +import net.folivo.trixnity.core.model.events.StateEventContent +import net.folivo.trixnity.core.serialization.events.EventContentSerializerMappings +import net.folivo.trixnity.core.serialization.events.contentSerializer + +/** + * @see matrix spec + */ +@MSC4140 +@Serializable +@Resource("/_matrix/client/v3/rooms/{roomId}/state/{type}/{stateKey?}") +@HttpMethod(PUT) +data class SendDelayedStateEvent( + @SerialName("roomId") val roomId: RoomId, + @SerialName("type") val type: String, + @SerialName("delay") val delayMs: Long, + @SerialName("stateKey") val stateKey: String = "", + @SerialName("user_id") val asUserId: UserId? = null, + @SerialName("ts") val ts: Long? = null, +) : MatrixEndpoint { + override fun requestSerializerBuilder( + mappings: EventContentSerializerMappings, + json: Json, + value: StateEventContent? + ): KSerializer = + mappings.state.contentSerializer(type, value) +} diff --git a/trixnity-core/src/commonMain/kotlin/net/folivo/trixnity/core/MSCAnnotations.kt b/trixnity-core/src/commonMain/kotlin/net/folivo/trixnity/core/MSCAnnotations.kt index 7187d63bf..fffcedc5d 100644 --- a/trixnity-core/src/commonMain/kotlin/net/folivo/trixnity/core/MSCAnnotations.kt +++ b/trixnity-core/src/commonMain/kotlin/net/folivo/trixnity/core/MSCAnnotations.kt @@ -1,5 +1,21 @@ package net.folivo.trixnity.core +/** + * @see MSC4140 + */ +@RequiresOptIn(message = "This API is experimental. It could change in the future without notice.") +@Retention(AnnotationRetention.BINARY) +@Target( + AnnotationTarget.CLASS, + AnnotationTarget.ANNOTATION_CLASS, + AnnotationTarget.PROPERTY, + AnnotationTarget.FIELD, + AnnotationTarget.CONSTRUCTOR, + AnnotationTarget.FUNCTION, + AnnotationTarget.TYPEALIAS +) +annotation class MSC4140 + /** * @see MSC3814 */ diff --git a/trixnity-core/src/commonMain/kotlin/net/folivo/trixnity/core/model/DelayId.kt b/trixnity-core/src/commonMain/kotlin/net/folivo/trixnity/core/model/DelayId.kt new file mode 100644 index 000000000..433210aba --- /dev/null +++ b/trixnity-core/src/commonMain/kotlin/net/folivo/trixnity/core/model/DelayId.kt @@ -0,0 +1,33 @@ +package net.folivo.trixnity.core.model + +import kotlinx.serialization.KSerializer +import kotlinx.serialization.Serializable +import kotlinx.serialization.descriptors.PrimitiveKind +import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder +import net.folivo.trixnity.core.MSC4140 +import net.folivo.trixnity.core.util.MatrixIdRegex + +@MSC4140 +@Serializable(with = DelayIdSerializer::class) +data class DelayId(val full: String) { + companion object { + fun isValid(id: String): Boolean = id.length <= 255 && id.matches(MatrixIdRegex.delayId) + } + + val isValid by lazy { isValid(full) } + + override fun toString() = full +} + +@OptIn(MSC4140::class) +object DelayIdSerializer : KSerializer { + override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("DelayIdSerializer", PrimitiveKind.STRING) + override fun deserialize(decoder: Decoder): DelayId = DelayId(decoder.decodeString()) + + override fun serialize(encoder: Encoder, value: DelayId) { + encoder.encodeString(value.full) + } +} diff --git a/trixnity-core/src/commonMain/kotlin/net/folivo/trixnity/core/util/MatrixIdRegex.kt b/trixnity-core/src/commonMain/kotlin/net/folivo/trixnity/core/util/MatrixIdRegex.kt index cdbea6b07..c0b412bc6 100644 --- a/trixnity-core/src/commonMain/kotlin/net/folivo/trixnity/core/util/MatrixIdRegex.kt +++ b/trixnity-core/src/commonMain/kotlin/net/folivo/trixnity/core/util/MatrixIdRegex.kt @@ -1,5 +1,7 @@ package net.folivo.trixnity.core.util +import net.folivo.trixnity.core.MSC4140 + internal object MatrixIdRegex { /** * @see [https://spec.matrix.org/unstable/appendices/#server-name] @@ -64,6 +66,15 @@ internal object MatrixIdRegex { private const val REASONABLE_EVENT_ID_PATTERN = "\\$$OPAQUE_ID_PATTERN(?::$DOMAIN_PATTERN)?" val reasonableEventId = REASONABLE_EVENT_ID_PATTERN.toRegex() + /** + * The delay_id is an opaque identifier generated by the server. + * + * @see [https://github.com/matrix-org/matrix-spec-proposals/blob/toger5/expiring-events-keep-alive/proposals/4140-delayed-events-futures.md#scheduling-a-delayed-event] + */ + @MSC4140 + // language=Regexp + val delayId = OPAQUE_ID_PATTERN.toRegex() + /** * This matches user ids and room aliases on best-effort basis, * it is NOT intended to match all valid ids. @@ -71,4 +82,4 @@ internal object MatrixIdRegex { // language=Regexp private const val AUTOLINK_ID_PATTERN = "[@#][a-zA-Z0-9${Patterns.UCS_CHAR}\\-.=_/+]+:$DOMAIN_PATTERN" val autolinkId = AUTOLINK_ID_PATTERN.toRegex() -} \ No newline at end of file +} -- GitLab From 487b2c1f45b2cbcd0aace5f93b843c4fce5b297a Mon Sep 17 00:00:00 2001 From: Manuel Stahl Date: Mon, 20 Oct 2025 15:32:03 +0200 Subject: [PATCH 02/11] MSC4140: Implement server API --- .../clientserverapi/server/RoomsApiHandler.kt | 15 ++++- .../clientserverapi/server/roomsApiRoutes.kt | 6 +- .../clientserverapi/server/RoomsRoutesTest.kt | 66 +++++++++++++++++++ 3 files changed, 85 insertions(+), 2 deletions(-) diff --git a/trixnity-clientserverapi/trixnity-clientserverapi-server/src/commonMain/kotlin/net/folivo/trixnity/clientserverapi/server/RoomsApiHandler.kt b/trixnity-clientserverapi/trixnity-clientserverapi-server/src/commonMain/kotlin/net/folivo/trixnity/clientserverapi/server/RoomsApiHandler.kt index b72306eb0..0892c8168 100644 --- a/trixnity-clientserverapi/trixnity-clientserverapi-server/src/commonMain/kotlin/net/folivo/trixnity/clientserverapi/server/RoomsApiHandler.kt +++ b/trixnity-clientserverapi/trixnity-clientserverapi-server/src/commonMain/kotlin/net/folivo/trixnity/clientserverapi/server/RoomsApiHandler.kt @@ -2,6 +2,7 @@ package net.folivo.trixnity.clientserverapi.server import net.folivo.trixnity.api.server.MatrixEndpointContext import net.folivo.trixnity.clientserverapi.model.rooms.* +import net.folivo.trixnity.core.MSC4140 import net.folivo.trixnity.core.model.events.ClientEvent.RoomEvent import net.folivo.trixnity.core.model.events.ClientEvent.RoomEvent.StateEvent import net.folivo.trixnity.core.model.events.MessageEventContent @@ -65,11 +66,23 @@ interface RoomsApiHandler { */ suspend fun sendStateEvent(context: MatrixEndpointContext): SendEventResponse + /** + * @see [SendDelayedStateEvent] + */ + @MSC4140 + suspend fun sendDelayedStateEvent(context: MatrixEndpointContext): SendDelayedEventResponse + /** * @see [SendMessageEvent] */ suspend fun sendMessageEvent(context: MatrixEndpointContext): SendEventResponse + /** + * @see [SendDelayedMessageEvent] + */ + @MSC4140 + suspend fun sendDelayedMessageEvent(context: MatrixEndpointContext): SendDelayedEventResponse + /** * @see [RedactEvent] */ @@ -234,4 +247,4 @@ interface RoomsApiHandler { * @see [TimestampToEvent] */ suspend fun timestampToEvent(context: MatrixEndpointContext): TimestampToEvent.Response -} \ No newline at end of file +} diff --git a/trixnity-clientserverapi/trixnity-clientserverapi-server/src/commonMain/kotlin/net/folivo/trixnity/clientserverapi/server/roomsApiRoutes.kt b/trixnity-clientserverapi/trixnity-clientserverapi-server/src/commonMain/kotlin/net/folivo/trixnity/clientserverapi/server/roomsApiRoutes.kt index 0b8cdb112..2f484f781 100644 --- a/trixnity-clientserverapi/trixnity-clientserverapi-server/src/commonMain/kotlin/net/folivo/trixnity/clientserverapi/server/roomsApiRoutes.kt +++ b/trixnity-clientserverapi/trixnity-clientserverapi-server/src/commonMain/kotlin/net/folivo/trixnity/clientserverapi/server/roomsApiRoutes.kt @@ -3,8 +3,10 @@ package net.folivo.trixnity.clientserverapi.server import io.ktor.server.routing.* import kotlinx.serialization.json.Json import net.folivo.trixnity.api.server.matrixEndpoint +import net.folivo.trixnity.core.MSC4140 import net.folivo.trixnity.core.serialization.events.EventContentSerializerMappings +@OptIn(MSC4140::class) internal fun Route.roomsApiRoutes( handler: RoomsApiHandler, json: Json, @@ -21,7 +23,9 @@ internal fun Route.roomsApiRoutes( matrixEndpoint(json, contentMappings, handler::getRelationsByRelationTypeAndEventType) matrixEndpoint(json, contentMappings, handler::getThreads) matrixEndpoint(json, contentMappings, handler::sendStateEvent) + matrixEndpoint(json, contentMappings, handler::sendDelayedStateEvent) matrixEndpoint(json, contentMappings, handler::sendMessageEvent) + matrixEndpoint(json, contentMappings, handler::sendDelayedMessageEvent) matrixEndpoint(json, contentMappings, handler::redactEvent) matrixEndpoint(json, contentMappings, handler::createRoom) matrixEndpoint(json, contentMappings, handler::setRoomAlias) @@ -55,4 +59,4 @@ internal fun Route.roomsApiRoutes( matrixEndpoint(json, contentMappings, handler::upgradeRoom) matrixEndpoint(json, contentMappings, handler::getHierarchy) matrixEndpoint(json, contentMappings, handler::timestampToEvent) -} \ No newline at end of file +} diff --git a/trixnity-clientserverapi/trixnity-clientserverapi-server/src/commonTest/kotlin/net/folivo/trixnity/clientserverapi/server/RoomsRoutesTest.kt b/trixnity-clientserverapi/trixnity-clientserverapi-server/src/commonTest/kotlin/net/folivo/trixnity/clientserverapi/server/RoomsRoutesTest.kt index dee4ebaed..34fac9188 100644 --- a/trixnity-clientserverapi/trixnity-clientserverapi-server/src/commonTest/kotlin/net/folivo/trixnity/clientserverapi/server/RoomsRoutesTest.kt +++ b/trixnity-clientserverapi/trixnity-clientserverapi-server/src/commonTest/kotlin/net/folivo/trixnity/clientserverapi/server/RoomsRoutesTest.kt @@ -14,6 +14,8 @@ import kotlinx.serialization.json.JsonObject import kotlinx.serialization.json.JsonPrimitive import net.folivo.trixnity.api.server.matrixApiServer import net.folivo.trixnity.clientserverapi.model.rooms.* +import net.folivo.trixnity.core.MSC4140 +import net.folivo.trixnity.core.model.DelayId import net.folivo.trixnity.core.model.EventId import net.folivo.trixnity.core.model.RoomAliasId import net.folivo.trixnity.core.model.RoomId @@ -693,6 +695,38 @@ class RoomsRoutesTest : TrixnityBaseTest() { } } + @Test + @OptIn(MSC4140::class) + fun shouldSendDelayedStateEvent() = testApplication { + initCut() + everySuspend { handlerMock.sendDelayedStateEvent(any()) } + .returns(SendDelayedEventResponse(DelayId("delay123"))) + val response = + client.put("/_matrix/client/v3/rooms/!room:server/state/m.room.name/?delay=1000") { + bearerAuth("token") + contentType(ContentType.Application.Json) + setBody("""{"name":"name"}""") + } + assertSoftly(response) { + this.status shouldBe HttpStatusCode.OK + this.contentType() shouldBe ContentType.Application.Json.withCharset(Charsets.UTF_8) + this.body() shouldBe """ + { + "delay_id":"delay123" + } + """.trimToFlatJson() + } + verifySuspend { + handlerMock.sendDelayedStateEvent(assert { + it.endpoint.delayMs shouldBe 1000 + it.endpoint.roomId shouldBe RoomId("!room:server") + it.endpoint.stateKey shouldBe "" + it.endpoint.type shouldBe "m.room.name" + it.requestBody shouldBe NameEventContent("name") + }) + } + } + @Test fun shouldSendMessageEvent() = testApplication { initCut() @@ -756,6 +790,38 @@ class RoomsRoutesTest : TrixnityBaseTest() { } } + @Test + @OptIn(MSC4140::class) + fun shouldSendDelayedMessageEvent() = testApplication { + initCut() + everySuspend { handlerMock.sendDelayedMessageEvent(any()) } + .returns(SendDelayedEventResponse(DelayId("delay123"))) + val response = + client.put("/_matrix/client/v3/rooms/!room:server/send/m.room.message/someTxnId?delay=1000") { + bearerAuth("token") + contentType(ContentType.Application.Json) + setBody("""{"body":"someBody","msgtype":"m.text"}""") + } + assertSoftly(response) { + this.status shouldBe HttpStatusCode.OK + this.contentType() shouldBe ContentType.Application.Json.withCharset(Charsets.UTF_8) + this.body() shouldBe """ + { + "delay_id":"delay123" + } + """.trimToFlatJson() + } + verifySuspend { + handlerMock.sendDelayedMessageEvent(assert { + it.endpoint.delayMs shouldBe 1000 + it.endpoint.roomId shouldBe RoomId("!room:server") + it.endpoint.txnId shouldBe "someTxnId" + it.endpoint.type shouldBe "m.room.message" + it.requestBody shouldBe RoomMessageEventContent.TextBased.Text("someBody") + }) + } + } + @Test fun shouldSendRedactEvent() = testApplication { initCut() -- GitLab From ef376a42ebea22a81d85a86d19f061dd69aaf7cd Mon Sep 17 00:00:00 2001 From: Manuel Stahl Date: Mon, 20 Oct 2025 16:58:06 +0200 Subject: [PATCH 03/11] MSC4140: Use polymorphic classes for serialization --- .../client/DelayedEventsClient.kt | 5 +- .../client/DelayedEventsClientTest.kt | 90 ++++++++++--------- .../model/delayed_events/DelayedEvents.kt | 60 ++++++++----- .../model/delayed_events/GetDelayedEvents.kt | 20 ++--- 4 files changed, 99 insertions(+), 76 deletions(-) diff --git a/trixnity-clientserverapi/trixnity-clientserverapi-client/src/commonMain/kotlin/net/folivo/trixnity/clientserverapi/client/DelayedEventsClient.kt b/trixnity-clientserverapi/trixnity-clientserverapi-client/src/commonMain/kotlin/net/folivo/trixnity/clientserverapi/client/DelayedEventsClient.kt index 4b55ec758..510bf7eed 100644 --- a/trixnity-clientserverapi/trixnity-clientserverapi-client/src/commonMain/kotlin/net/folivo/trixnity/clientserverapi/client/DelayedEventsClient.kt +++ b/trixnity-clientserverapi/trixnity-clientserverapi-client/src/commonMain/kotlin/net/folivo/trixnity/clientserverapi/client/DelayedEventsClient.kt @@ -2,7 +2,6 @@ package net.folivo.trixnity.clientserverapi.client import net.folivo.trixnity.clientserverapi.model.delayed_events.CancelDelayedEvent import net.folivo.trixnity.clientserverapi.model.delayed_events.DelayedEventStatus -import net.folivo.trixnity.clientserverapi.model.delayed_events.DelayedEvents import net.folivo.trixnity.clientserverapi.model.delayed_events.GetDelayedEvents import net.folivo.trixnity.clientserverapi.model.delayed_events.RestartDelayedEvent import net.folivo.trixnity.clientserverapi.model.delayed_events.SendDelayedEvent @@ -19,7 +18,7 @@ interface DelayedEventsClient { status: DelayedEventStatus? = null, delayIds: List? = null, from: String? = null, - ): Result + ): Result /** * @see [SendDelayedEvent] @@ -46,7 +45,7 @@ class DelayedEventsClientImpl( status: DelayedEventStatus?, delayIds: List?, from: String?, - ): Result = + ): Result = baseClient.request(GetDelayedEvents(status, delayIds, from)) override suspend fun sendDelayedEvent(delayId: DelayId): Result = diff --git a/trixnity-clientserverapi/trixnity-clientserverapi-client/src/commonTest/kotlin/net/folivo/trixnity/clientserverapi/client/DelayedEventsClientTest.kt b/trixnity-clientserverapi/trixnity-clientserverapi-client/src/commonTest/kotlin/net/folivo/trixnity/clientserverapi/client/DelayedEventsClientTest.kt index fecf12db6..0c7436257 100644 --- a/trixnity-clientserverapi/trixnity-clientserverapi-client/src/commonTest/kotlin/net/folivo/trixnity/clientserverapi/client/DelayedEventsClientTest.kt +++ b/trixnity-clientserverapi/trixnity-clientserverapi-client/src/commonTest/kotlin/net/folivo/trixnity/clientserverapi/client/DelayedEventsClientTest.kt @@ -11,8 +11,17 @@ import io.ktor.http.Url import io.ktor.http.fullPath import io.ktor.http.headersOf import kotlinx.coroutines.test.runTest +import net.folivo.trixnity.clientserverapi.model.delayed_events.FinalizedDelayedEvent +import net.folivo.trixnity.clientserverapi.model.delayed_events.GetDelayedEvents +import net.folivo.trixnity.clientserverapi.model.delayed_events.ScheduledDelayedEvent.ScheduledDelayedMessageEvent +import net.folivo.trixnity.clientserverapi.model.delayed_events.ScheduledDelayedEvent.ScheduledDelayedStateEvent import net.folivo.trixnity.core.MSC4140 import net.folivo.trixnity.core.model.DelayId +import net.folivo.trixnity.core.model.EventId +import net.folivo.trixnity.core.model.RoomId +import net.folivo.trixnity.core.model.events.m.room.NameEventContent +import net.folivo.trixnity.core.model.events.m.room.RoomMessageEventContent +import net.folivo.trixnity.core.serialization.createMatrixEventJson import net.folivo.trixnity.test.utils.TrixnityBaseTest import net.folivo.trixnity.testutils.scopedMockEngine import kotlin.test.Test @@ -21,46 +30,45 @@ import kotlin.test.assertEquals @OptIn(MSC4140::class) class DelayedEventsClientTest : TrixnityBaseTest() { + private val json = createMatrixEventJson() + @Test fun shouldGetDelayedEvents() = runTest { - // FIXME: "content" cannot be deserialized - val response = """ - { - "scheduled": [ - { - "delay_id": "d1", - "room_id": "!room:server", - "type": "m.room.message", - "delay": 1000, - "running_since": 123456789, - "content": { - "body": "This is a delayed message.", - "msgtype": "m.text" - } - } - ], - "finalised": [ - { - "delayed_event": { - "delay_id": "d2", - "room_id": "!room:server", - "type": "m.call", - "state_key": "m.call#ROOM", - "delay": 1000, - "running_since": 123456789, - "content": { - "name": "This is another delayed message." - } - }, - "outcome": "send", - "reason": "delay", - "event_id": "123", - "origin_server_ts": 123456789 - } - ], - "next_batch": "foo1234" - } - """.trimIndent() + val response = GetDelayedEvents.Response( + scheduled = listOf( + ScheduledDelayedMessageEvent( + delayId = DelayId("d1"), + roomId = RoomId("room:server"), + type = "m.room.message", + delayMs = 1000, + runningSince = 123456789, + content = RoomMessageEventContent.Location( + body = "Somewhere", + geoUri = "geo:0,0?q=Somewhere" + ) + ) + ), + finalised = listOf( + FinalizedDelayedEvent( + delayedEvent = ScheduledDelayedStateEvent( + delayId = DelayId("d2"), + roomId = RoomId("room:server"), + type = "m.room.name", + stateKey = "m.room.name#ROOM", + delayMs = 1000, + runningSince = 123456789, + content = NameEventContent( + name = "This is another delayed message." + ), + ), + outcome = FinalizedDelayedEvent.Outcome.SEND, + reason = FinalizedDelayedEvent.Reason.DELAY, + eventId = EventId("$123"), + originServerTs = 123456789, + ) + ), + nextBatch = "foo1234", + ) val matrixRestClient = MatrixClientServerApiClientImpl( baseUrl = Url("https://matrix.host"), httpClientEngine = scopedMockEngine { @@ -70,15 +78,15 @@ class DelayedEventsClientTest : TrixnityBaseTest() { request.url.fullPath) assertEquals(HttpMethod.Get, request.method) respond( - response, + json.encodeToString(response), HttpStatusCode.OK, headersOf(HttpHeaders.ContentType, ContentType.Application.Json.toString()) ) } }) val result = matrixRestClient.delayedEvents.getDelayedEvents().getOrThrow() - result.scheduled.first().delayId shouldBe DelayId("d1") - result.finalised.first().delayedEvent.delayId shouldBe DelayId("d2") + result.scheduled!!.first().delayId shouldBe DelayId("d1") + result.finalised!!.first().delayedEvent.delayId shouldBe DelayId("d2") } @Test diff --git a/trixnity-clientserverapi/trixnity-clientserverapi-model/src/commonMain/kotlin/net/folivo/trixnity/clientserverapi/model/delayed_events/DelayedEvents.kt b/trixnity-clientserverapi/trixnity-clientserverapi-model/src/commonMain/kotlin/net/folivo/trixnity/clientserverapi/model/delayed_events/DelayedEvents.kt index 9404c5299..89df9b6e9 100644 --- a/trixnity-clientserverapi/trixnity-clientserverapi-model/src/commonMain/kotlin/net/folivo/trixnity/clientserverapi/model/delayed_events/DelayedEvents.kt +++ b/trixnity-clientserverapi/trixnity-clientserverapi-model/src/commonMain/kotlin/net/folivo/trixnity/clientserverapi/model/delayed_events/DelayedEvents.kt @@ -1,6 +1,5 @@ package net.folivo.trixnity.clientserverapi.model.delayed_events -import kotlinx.serialization.Contextual import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable import net.folivo.trixnity.core.ErrorResponse @@ -8,7 +7,9 @@ import net.folivo.trixnity.core.MSC4140 import net.folivo.trixnity.core.model.DelayId import net.folivo.trixnity.core.model.EventId import net.folivo.trixnity.core.model.RoomId -import net.folivo.trixnity.core.model.events.EventContent +import net.folivo.trixnity.core.model.events.MessageEventContent +import net.folivo.trixnity.core.model.events.RoomEventContent +import net.folivo.trixnity.core.model.events.StateEventContent @MSC4140 @Serializable @@ -21,6 +22,8 @@ enum class DelayedEventStatus { } /** + * Matrix scheduled delayed event. Either a delayed state event or a delayed message event. + * * @see MSC4140 * @param delayId Required. The ID of the delayed event. * @param roomId Required. The room ID of the delayed event. @@ -32,15 +35,38 @@ enum class DelayedEventStatus { */ @MSC4140 @Serializable -data class ScheduledDelayedEvent( - @SerialName("delay_id") val delayId: DelayId, - @SerialName("room_id") val roomId: RoomId, - @SerialName("type") val type: String, - @SerialName("state_key") val stateKey: String? = null, - @SerialName("delay") val delayMs: Long, - @SerialName("running_since") val runningSince: Long, - @SerialName("content") val content: @Contextual EventContent, -) +sealed interface ScheduledDelayedEvent { + val delayId: DelayId + val roomId: RoomId + val type: String + val stateKey: String? + val delayMs: Long + val runningSince: Long + val content: C + + @Serializable + data class ScheduledDelayedStateEvent( + @SerialName("delay_id") override val delayId: DelayId, + @SerialName("room_id") override val roomId: RoomId, + @SerialName("type") override val type: String, + @SerialName("state_key") override val stateKey: String? = null, + @SerialName("delay") override val delayMs: Long, + @SerialName("running_since") override val runningSince: Long, + @SerialName("content") override val content: C, + ): ScheduledDelayedEvent + + + @Serializable + data class ScheduledDelayedMessageEvent( + @SerialName("delay_id") override val delayId: DelayId, + @SerialName("room_id") override val roomId: RoomId, + @SerialName("type") override val type: String, + @SerialName("state_key") override val stateKey: String? = null, + @SerialName("delay") override val delayMs: Long, + @SerialName("running_since") override val runningSince: Long, + @SerialName("content") override val content: C, + ): ScheduledDelayedEvent +} /** * @see MSC4140 @@ -53,8 +79,8 @@ data class ScheduledDelayedEvent( */ @MSC4140 @Serializable -data class FinalizedDelayedEvent( - @SerialName("delayed_event") val delayedEvent: ScheduledDelayedEvent, +data class FinalizedDelayedEvent>( + @SerialName("delayed_event") val delayedEvent: E, @SerialName("outcome") val outcome: Outcome, @SerialName("reason") val reason: Reason, @SerialName("error") val error: ErrorResponse? = null, @@ -82,11 +108,3 @@ data class FinalizedDelayedEvent( DELAY, } } - -@MSC4140 -@Serializable -data class DelayedEvents( - @SerialName("scheduled") val scheduled: List, - @SerialName("finalised") val finalised: List, - @SerialName("next_batch") val nextBatch: String? = null, -) diff --git a/trixnity-clientserverapi/trixnity-clientserverapi-model/src/commonMain/kotlin/net/folivo/trixnity/clientserverapi/model/delayed_events/GetDelayedEvents.kt b/trixnity-clientserverapi/trixnity-clientserverapi-model/src/commonMain/kotlin/net/folivo/trixnity/clientserverapi/model/delayed_events/GetDelayedEvents.kt index efe42f192..4679c8d79 100644 --- a/trixnity-clientserverapi/trixnity-clientserverapi-model/src/commonMain/kotlin/net/folivo/trixnity/clientserverapi/model/delayed_events/GetDelayedEvents.kt +++ b/trixnity-clientserverapi/trixnity-clientserverapi-model/src/commonMain/kotlin/net/folivo/trixnity/clientserverapi/model/delayed_events/GetDelayedEvents.kt @@ -1,16 +1,14 @@ package net.folivo.trixnity.clientserverapi.model.delayed_events import io.ktor.resources.Resource -import kotlinx.serialization.KSerializer +import kotlinx.serialization.Contextual import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable -import kotlinx.serialization.json.Json import net.folivo.trixnity.core.HttpMethod import net.folivo.trixnity.core.HttpMethodType.GET import net.folivo.trixnity.core.MSC4140 import net.folivo.trixnity.core.MatrixEndpoint import net.folivo.trixnity.core.model.DelayId -import net.folivo.trixnity.core.serialization.events.EventContentSerializerMappings /** * @see matrix spec @@ -24,12 +22,12 @@ data class GetDelayedEvents( @SerialName("status") val status: DelayedEventStatus?, @SerialName("delay_id") val delayIds: List?, @SerialName("from") val from: String?, -) : MatrixEndpoint { - override fun responseSerializerBuilder( - mappings: EventContentSerializerMappings, - json: Json, - value: DelayedEvents? - ): KSerializer { - return requireNotNull(json.serializersModule.getContextual(DelayedEvents::class)) - } +) : MatrixEndpoint { + + @Serializable + data class Response( + @SerialName("scheduled") val scheduled: List<@Contextual ScheduledDelayedEvent<*>>? = null, + @SerialName("finalised") val finalised: List<@Contextual FinalizedDelayedEvent>>? = null, + @SerialName("next_batch") val nextBatch: String? = null, + ) } -- GitLab From 445c55bfa459936665a1195c4e29b96ff5c21250 Mon Sep 17 00:00:00 2001 From: Manuel Stahl Date: Mon, 20 Oct 2025 17:00:14 +0200 Subject: [PATCH 04/11] MSC4140: Rename package delayed_events to delayedevents --- .../clientserverapi/client/DelayedEventsClient.kt | 10 +++++----- .../clientserverapi/client/DelayedEventsClientTest.kt | 8 ++++---- .../CancelDelayedEvent.kt | 2 +- .../{delayed_events => delayedevents}/DelayedEvents.kt | 2 +- .../GetDelayedEvents.kt | 2 +- .../RestartDelayedEvent.kt | 2 +- .../SendDelayedEvent.kt | 2 +- 7 files changed, 14 insertions(+), 14 deletions(-) rename trixnity-clientserverapi/trixnity-clientserverapi-model/src/commonMain/kotlin/net/folivo/trixnity/clientserverapi/model/{delayed_events => delayedevents}/CancelDelayedEvent.kt (93%) rename trixnity-clientserverapi/trixnity-clientserverapi-model/src/commonMain/kotlin/net/folivo/trixnity/clientserverapi/model/{delayed_events => delayedevents}/DelayedEvents.kt (98%) rename trixnity-clientserverapi/trixnity-clientserverapi-model/src/commonMain/kotlin/net/folivo/trixnity/clientserverapi/model/{delayed_events => delayedevents}/GetDelayedEvents.kt (95%) rename trixnity-clientserverapi/trixnity-clientserverapi-model/src/commonMain/kotlin/net/folivo/trixnity/clientserverapi/model/{delayed_events => delayedevents}/RestartDelayedEvent.kt (93%) rename trixnity-clientserverapi/trixnity-clientserverapi-model/src/commonMain/kotlin/net/folivo/trixnity/clientserverapi/model/{delayed_events => delayedevents}/SendDelayedEvent.kt (93%) diff --git a/trixnity-clientserverapi/trixnity-clientserverapi-client/src/commonMain/kotlin/net/folivo/trixnity/clientserverapi/client/DelayedEventsClient.kt b/trixnity-clientserverapi/trixnity-clientserverapi-client/src/commonMain/kotlin/net/folivo/trixnity/clientserverapi/client/DelayedEventsClient.kt index 510bf7eed..3ed08f998 100644 --- a/trixnity-clientserverapi/trixnity-clientserverapi-client/src/commonMain/kotlin/net/folivo/trixnity/clientserverapi/client/DelayedEventsClient.kt +++ b/trixnity-clientserverapi/trixnity-clientserverapi-client/src/commonMain/kotlin/net/folivo/trixnity/clientserverapi/client/DelayedEventsClient.kt @@ -1,10 +1,10 @@ package net.folivo.trixnity.clientserverapi.client -import net.folivo.trixnity.clientserverapi.model.delayed_events.CancelDelayedEvent -import net.folivo.trixnity.clientserverapi.model.delayed_events.DelayedEventStatus -import net.folivo.trixnity.clientserverapi.model.delayed_events.GetDelayedEvents -import net.folivo.trixnity.clientserverapi.model.delayed_events.RestartDelayedEvent -import net.folivo.trixnity.clientserverapi.model.delayed_events.SendDelayedEvent +import net.folivo.trixnity.clientserverapi.model.delayedevents.CancelDelayedEvent +import net.folivo.trixnity.clientserverapi.model.delayedevents.DelayedEventStatus +import net.folivo.trixnity.clientserverapi.model.delayedevents.GetDelayedEvents +import net.folivo.trixnity.clientserverapi.model.delayedevents.RestartDelayedEvent +import net.folivo.trixnity.clientserverapi.model.delayedevents.SendDelayedEvent import net.folivo.trixnity.core.MSC4140 import net.folivo.trixnity.core.model.DelayId diff --git a/trixnity-clientserverapi/trixnity-clientserverapi-client/src/commonTest/kotlin/net/folivo/trixnity/clientserverapi/client/DelayedEventsClientTest.kt b/trixnity-clientserverapi/trixnity-clientserverapi-client/src/commonTest/kotlin/net/folivo/trixnity/clientserverapi/client/DelayedEventsClientTest.kt index 0c7436257..2eb95f862 100644 --- a/trixnity-clientserverapi/trixnity-clientserverapi-client/src/commonTest/kotlin/net/folivo/trixnity/clientserverapi/client/DelayedEventsClientTest.kt +++ b/trixnity-clientserverapi/trixnity-clientserverapi-client/src/commonTest/kotlin/net/folivo/trixnity/clientserverapi/client/DelayedEventsClientTest.kt @@ -11,10 +11,10 @@ import io.ktor.http.Url import io.ktor.http.fullPath import io.ktor.http.headersOf import kotlinx.coroutines.test.runTest -import net.folivo.trixnity.clientserverapi.model.delayed_events.FinalizedDelayedEvent -import net.folivo.trixnity.clientserverapi.model.delayed_events.GetDelayedEvents -import net.folivo.trixnity.clientserverapi.model.delayed_events.ScheduledDelayedEvent.ScheduledDelayedMessageEvent -import net.folivo.trixnity.clientserverapi.model.delayed_events.ScheduledDelayedEvent.ScheduledDelayedStateEvent +import net.folivo.trixnity.clientserverapi.model.delayedevents.FinalizedDelayedEvent +import net.folivo.trixnity.clientserverapi.model.delayedevents.GetDelayedEvents +import net.folivo.trixnity.clientserverapi.model.delayedevents.ScheduledDelayedEvent.ScheduledDelayedMessageEvent +import net.folivo.trixnity.clientserverapi.model.delayedevents.ScheduledDelayedEvent.ScheduledDelayedStateEvent import net.folivo.trixnity.core.MSC4140 import net.folivo.trixnity.core.model.DelayId import net.folivo.trixnity.core.model.EventId diff --git a/trixnity-clientserverapi/trixnity-clientserverapi-model/src/commonMain/kotlin/net/folivo/trixnity/clientserverapi/model/delayed_events/CancelDelayedEvent.kt b/trixnity-clientserverapi/trixnity-clientserverapi-model/src/commonMain/kotlin/net/folivo/trixnity/clientserverapi/model/delayedevents/CancelDelayedEvent.kt similarity index 93% rename from trixnity-clientserverapi/trixnity-clientserverapi-model/src/commonMain/kotlin/net/folivo/trixnity/clientserverapi/model/delayed_events/CancelDelayedEvent.kt rename to trixnity-clientserverapi/trixnity-clientserverapi-model/src/commonMain/kotlin/net/folivo/trixnity/clientserverapi/model/delayedevents/CancelDelayedEvent.kt index 7f1a388bc..e7037686b 100644 --- a/trixnity-clientserverapi/trixnity-clientserverapi-model/src/commonMain/kotlin/net/folivo/trixnity/clientserverapi/model/delayed_events/CancelDelayedEvent.kt +++ b/trixnity-clientserverapi/trixnity-clientserverapi-model/src/commonMain/kotlin/net/folivo/trixnity/clientserverapi/model/delayedevents/CancelDelayedEvent.kt @@ -1,4 +1,4 @@ -package net.folivo.trixnity.clientserverapi.model.delayed_events +package net.folivo.trixnity.clientserverapi.model.delayedevents import io.ktor.resources.Resource import kotlinx.serialization.SerialName diff --git a/trixnity-clientserverapi/trixnity-clientserverapi-model/src/commonMain/kotlin/net/folivo/trixnity/clientserverapi/model/delayed_events/DelayedEvents.kt b/trixnity-clientserverapi/trixnity-clientserverapi-model/src/commonMain/kotlin/net/folivo/trixnity/clientserverapi/model/delayedevents/DelayedEvents.kt similarity index 98% rename from trixnity-clientserverapi/trixnity-clientserverapi-model/src/commonMain/kotlin/net/folivo/trixnity/clientserverapi/model/delayed_events/DelayedEvents.kt rename to trixnity-clientserverapi/trixnity-clientserverapi-model/src/commonMain/kotlin/net/folivo/trixnity/clientserverapi/model/delayedevents/DelayedEvents.kt index 89df9b6e9..d8662ffe5 100644 --- a/trixnity-clientserverapi/trixnity-clientserverapi-model/src/commonMain/kotlin/net/folivo/trixnity/clientserverapi/model/delayed_events/DelayedEvents.kt +++ b/trixnity-clientserverapi/trixnity-clientserverapi-model/src/commonMain/kotlin/net/folivo/trixnity/clientserverapi/model/delayedevents/DelayedEvents.kt @@ -1,4 +1,4 @@ -package net.folivo.trixnity.clientserverapi.model.delayed_events +package net.folivo.trixnity.clientserverapi.model.delayedevents import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable diff --git a/trixnity-clientserverapi/trixnity-clientserverapi-model/src/commonMain/kotlin/net/folivo/trixnity/clientserverapi/model/delayed_events/GetDelayedEvents.kt b/trixnity-clientserverapi/trixnity-clientserverapi-model/src/commonMain/kotlin/net/folivo/trixnity/clientserverapi/model/delayedevents/GetDelayedEvents.kt similarity index 95% rename from trixnity-clientserverapi/trixnity-clientserverapi-model/src/commonMain/kotlin/net/folivo/trixnity/clientserverapi/model/delayed_events/GetDelayedEvents.kt rename to trixnity-clientserverapi/trixnity-clientserverapi-model/src/commonMain/kotlin/net/folivo/trixnity/clientserverapi/model/delayedevents/GetDelayedEvents.kt index 4679c8d79..d1d208e5b 100644 --- a/trixnity-clientserverapi/trixnity-clientserverapi-model/src/commonMain/kotlin/net/folivo/trixnity/clientserverapi/model/delayed_events/GetDelayedEvents.kt +++ b/trixnity-clientserverapi/trixnity-clientserverapi-model/src/commonMain/kotlin/net/folivo/trixnity/clientserverapi/model/delayedevents/GetDelayedEvents.kt @@ -1,4 +1,4 @@ -package net.folivo.trixnity.clientserverapi.model.delayed_events +package net.folivo.trixnity.clientserverapi.model.delayedevents import io.ktor.resources.Resource import kotlinx.serialization.Contextual diff --git a/trixnity-clientserverapi/trixnity-clientserverapi-model/src/commonMain/kotlin/net/folivo/trixnity/clientserverapi/model/delayed_events/RestartDelayedEvent.kt b/trixnity-clientserverapi/trixnity-clientserverapi-model/src/commonMain/kotlin/net/folivo/trixnity/clientserverapi/model/delayedevents/RestartDelayedEvent.kt similarity index 93% rename from trixnity-clientserverapi/trixnity-clientserverapi-model/src/commonMain/kotlin/net/folivo/trixnity/clientserverapi/model/delayed_events/RestartDelayedEvent.kt rename to trixnity-clientserverapi/trixnity-clientserverapi-model/src/commonMain/kotlin/net/folivo/trixnity/clientserverapi/model/delayedevents/RestartDelayedEvent.kt index 21a04ce9d..580772076 100644 --- a/trixnity-clientserverapi/trixnity-clientserverapi-model/src/commonMain/kotlin/net/folivo/trixnity/clientserverapi/model/delayed_events/RestartDelayedEvent.kt +++ b/trixnity-clientserverapi/trixnity-clientserverapi-model/src/commonMain/kotlin/net/folivo/trixnity/clientserverapi/model/delayedevents/RestartDelayedEvent.kt @@ -1,4 +1,4 @@ -package net.folivo.trixnity.clientserverapi.model.delayed_events +package net.folivo.trixnity.clientserverapi.model.delayedevents import io.ktor.resources.Resource import kotlinx.serialization.SerialName diff --git a/trixnity-clientserverapi/trixnity-clientserverapi-model/src/commonMain/kotlin/net/folivo/trixnity/clientserverapi/model/delayed_events/SendDelayedEvent.kt b/trixnity-clientserverapi/trixnity-clientserverapi-model/src/commonMain/kotlin/net/folivo/trixnity/clientserverapi/model/delayedevents/SendDelayedEvent.kt similarity index 93% rename from trixnity-clientserverapi/trixnity-clientserverapi-model/src/commonMain/kotlin/net/folivo/trixnity/clientserverapi/model/delayed_events/SendDelayedEvent.kt rename to trixnity-clientserverapi/trixnity-clientserverapi-model/src/commonMain/kotlin/net/folivo/trixnity/clientserverapi/model/delayedevents/SendDelayedEvent.kt index 343f0a35e..ed844207a 100644 --- a/trixnity-clientserverapi/trixnity-clientserverapi-model/src/commonMain/kotlin/net/folivo/trixnity/clientserverapi/model/delayed_events/SendDelayedEvent.kt +++ b/trixnity-clientserverapi/trixnity-clientserverapi-model/src/commonMain/kotlin/net/folivo/trixnity/clientserverapi/model/delayedevents/SendDelayedEvent.kt @@ -1,4 +1,4 @@ -package net.folivo.trixnity.clientserverapi.model.delayed_events +package net.folivo.trixnity.clientserverapi.model.delayedevents import io.ktor.resources.Resource import kotlinx.serialization.SerialName -- GitLab From 2c7bfb41a2cafbf9bb26208bcf2b452689869b84 Mon Sep 17 00:00:00 2001 From: Manuel Stahl Date: Mon, 20 Oct 2025 19:54:35 +0200 Subject: [PATCH 05/11] MSC4140: Add missing serializers --- .../client/DelayedEventsClient.kt | 2 +- .../client/DelayedEventsClientTest.kt | 10 ++--- .../model/delayedevents/GetDelayedEvents.kt | 5 ++- .../core/model/events/DelayedEvent.kt | 16 +++----- .../events/DelayedEventSerializer.kt | 41 +++++++++++++++++++ .../events/DelayedMessageEventSerializer.kt | 18 ++++++++ .../events/DelayedStateEventSerializer.kt | 18 ++++++++ .../events/createEventSerializersModule.kt | 9 +++- 8 files changed, 101 insertions(+), 18 deletions(-) rename trixnity-clientserverapi/trixnity-clientserverapi-model/src/commonMain/kotlin/net/folivo/trixnity/clientserverapi/model/delayedevents/DelayedEvents.kt => trixnity-core/src/commonMain/kotlin/net/folivo/trixnity/core/model/events/DelayedEvent.kt (87%) create mode 100644 trixnity-core/src/commonMain/kotlin/net/folivo/trixnity/core/serialization/events/DelayedEventSerializer.kt create mode 100644 trixnity-core/src/commonMain/kotlin/net/folivo/trixnity/core/serialization/events/DelayedMessageEventSerializer.kt create mode 100644 trixnity-core/src/commonMain/kotlin/net/folivo/trixnity/core/serialization/events/DelayedStateEventSerializer.kt diff --git a/trixnity-clientserverapi/trixnity-clientserverapi-client/src/commonMain/kotlin/net/folivo/trixnity/clientserverapi/client/DelayedEventsClient.kt b/trixnity-clientserverapi/trixnity-clientserverapi-client/src/commonMain/kotlin/net/folivo/trixnity/clientserverapi/client/DelayedEventsClient.kt index 3ed08f998..fde8cc97d 100644 --- a/trixnity-clientserverapi/trixnity-clientserverapi-client/src/commonMain/kotlin/net/folivo/trixnity/clientserverapi/client/DelayedEventsClient.kt +++ b/trixnity-clientserverapi/trixnity-clientserverapi-client/src/commonMain/kotlin/net/folivo/trixnity/clientserverapi/client/DelayedEventsClient.kt @@ -1,7 +1,7 @@ package net.folivo.trixnity.clientserverapi.client import net.folivo.trixnity.clientserverapi.model.delayedevents.CancelDelayedEvent -import net.folivo.trixnity.clientserverapi.model.delayedevents.DelayedEventStatus +import net.folivo.trixnity.core.model.events.DelayedEventStatus import net.folivo.trixnity.clientserverapi.model.delayedevents.GetDelayedEvents import net.folivo.trixnity.clientserverapi.model.delayedevents.RestartDelayedEvent import net.folivo.trixnity.clientserverapi.model.delayedevents.SendDelayedEvent diff --git a/trixnity-clientserverapi/trixnity-clientserverapi-client/src/commonTest/kotlin/net/folivo/trixnity/clientserverapi/client/DelayedEventsClientTest.kt b/trixnity-clientserverapi/trixnity-clientserverapi-client/src/commonTest/kotlin/net/folivo/trixnity/clientserverapi/client/DelayedEventsClientTest.kt index 2eb95f862..2c9242048 100644 --- a/trixnity-clientserverapi/trixnity-clientserverapi-client/src/commonTest/kotlin/net/folivo/trixnity/clientserverapi/client/DelayedEventsClientTest.kt +++ b/trixnity-clientserverapi/trixnity-clientserverapi-client/src/commonTest/kotlin/net/folivo/trixnity/clientserverapi/client/DelayedEventsClientTest.kt @@ -11,10 +11,10 @@ import io.ktor.http.Url import io.ktor.http.fullPath import io.ktor.http.headersOf import kotlinx.coroutines.test.runTest -import net.folivo.trixnity.clientserverapi.model.delayedevents.FinalizedDelayedEvent +import net.folivo.trixnity.core.model.events.FinalizedDelayedEvent import net.folivo.trixnity.clientserverapi.model.delayedevents.GetDelayedEvents -import net.folivo.trixnity.clientserverapi.model.delayedevents.ScheduledDelayedEvent.ScheduledDelayedMessageEvent -import net.folivo.trixnity.clientserverapi.model.delayedevents.ScheduledDelayedEvent.ScheduledDelayedStateEvent +import net.folivo.trixnity.core.model.events.ScheduledDelayedEvent.ScheduledDelayedMessageEvent +import net.folivo.trixnity.core.model.events.ScheduledDelayedEvent.ScheduledDelayedStateEvent import net.folivo.trixnity.core.MSC4140 import net.folivo.trixnity.core.model.DelayId import net.folivo.trixnity.core.model.EventId @@ -50,11 +50,11 @@ class DelayedEventsClientTest : TrixnityBaseTest() { ), finalised = listOf( FinalizedDelayedEvent( - delayedEvent = ScheduledDelayedStateEvent( + delayedEvent = ScheduledDelayedStateEvent( delayId = DelayId("d2"), roomId = RoomId("room:server"), type = "m.room.name", - stateKey = "m.room.name#ROOM", + stateKey = "", delayMs = 1000, runningSince = 123456789, content = NameEventContent( diff --git a/trixnity-clientserverapi/trixnity-clientserverapi-model/src/commonMain/kotlin/net/folivo/trixnity/clientserverapi/model/delayedevents/GetDelayedEvents.kt b/trixnity-clientserverapi/trixnity-clientserverapi-model/src/commonMain/kotlin/net/folivo/trixnity/clientserverapi/model/delayedevents/GetDelayedEvents.kt index d1d208e5b..df70f430a 100644 --- a/trixnity-clientserverapi/trixnity-clientserverapi-model/src/commonMain/kotlin/net/folivo/trixnity/clientserverapi/model/delayedevents/GetDelayedEvents.kt +++ b/trixnity-clientserverapi/trixnity-clientserverapi-model/src/commonMain/kotlin/net/folivo/trixnity/clientserverapi/model/delayedevents/GetDelayedEvents.kt @@ -9,6 +9,9 @@ import net.folivo.trixnity.core.HttpMethodType.GET import net.folivo.trixnity.core.MSC4140 import net.folivo.trixnity.core.MatrixEndpoint import net.folivo.trixnity.core.model.DelayId +import net.folivo.trixnity.core.model.events.DelayedEventStatus +import net.folivo.trixnity.core.model.events.FinalizedDelayedEvent +import net.folivo.trixnity.core.model.events.ScheduledDelayedEvent /** * @see matrix spec @@ -27,7 +30,7 @@ data class GetDelayedEvents( @Serializable data class Response( @SerialName("scheduled") val scheduled: List<@Contextual ScheduledDelayedEvent<*>>? = null, - @SerialName("finalised") val finalised: List<@Contextual FinalizedDelayedEvent>>? = null, + @SerialName("finalised") val finalised: List>>? = null, @SerialName("next_batch") val nextBatch: String? = null, ) } diff --git a/trixnity-clientserverapi/trixnity-clientserverapi-model/src/commonMain/kotlin/net/folivo/trixnity/clientserverapi/model/delayedevents/DelayedEvents.kt b/trixnity-core/src/commonMain/kotlin/net/folivo/trixnity/core/model/events/DelayedEvent.kt similarity index 87% rename from trixnity-clientserverapi/trixnity-clientserverapi-model/src/commonMain/kotlin/net/folivo/trixnity/clientserverapi/model/delayedevents/DelayedEvents.kt rename to trixnity-core/src/commonMain/kotlin/net/folivo/trixnity/core/model/events/DelayedEvent.kt index d8662ffe5..5c2bb5557 100644 --- a/trixnity-clientserverapi/trixnity-clientserverapi-model/src/commonMain/kotlin/net/folivo/trixnity/clientserverapi/model/delayedevents/DelayedEvents.kt +++ b/trixnity-core/src/commonMain/kotlin/net/folivo/trixnity/core/model/events/DelayedEvent.kt @@ -1,4 +1,4 @@ -package net.folivo.trixnity.clientserverapi.model.delayedevents +package net.folivo.trixnity.core.model.events import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable @@ -7,9 +7,6 @@ import net.folivo.trixnity.core.MSC4140 import net.folivo.trixnity.core.model.DelayId import net.folivo.trixnity.core.model.EventId import net.folivo.trixnity.core.model.RoomId -import net.folivo.trixnity.core.model.events.MessageEventContent -import net.folivo.trixnity.core.model.events.RoomEventContent -import net.folivo.trixnity.core.model.events.StateEventContent @MSC4140 @Serializable @@ -28,28 +25,28 @@ enum class DelayedEventStatus { * @param delayId Required. The ID of the delayed event. * @param roomId Required. The room ID of the delayed event. * @param type Required. The event type of the delayed event. - * @param stateKey Optional. The state key of the delayed event if it is a state event. * @param delayMs Required. The delay in milliseconds before the event is to be sent. * @param runningSince Required. The timestamp (as Unix time in milliseconds) when the delayed event was scheduled or last restarted. * @param content Required. The content of the delayed event. This is the body of the original PUT request, not a preview of the full event after sending. */ @MSC4140 @Serializable -sealed interface ScheduledDelayedEvent { +sealed interface ScheduledDelayedEvent: Event { val delayId: DelayId val roomId: RoomId val type: String - val stateKey: String? val delayMs: Long val runningSince: Long - val content: C + /** + * @param stateKey The state key of the delayed event if it is a state event. + */ @Serializable data class ScheduledDelayedStateEvent( @SerialName("delay_id") override val delayId: DelayId, @SerialName("room_id") override val roomId: RoomId, @SerialName("type") override val type: String, - @SerialName("state_key") override val stateKey: String? = null, + @SerialName("state_key") val stateKey: String, @SerialName("delay") override val delayMs: Long, @SerialName("running_since") override val runningSince: Long, @SerialName("content") override val content: C, @@ -61,7 +58,6 @@ sealed interface ScheduledDelayedEvent { @SerialName("delay_id") override val delayId: DelayId, @SerialName("room_id") override val roomId: RoomId, @SerialName("type") override val type: String, - @SerialName("state_key") override val stateKey: String? = null, @SerialName("delay") override val delayMs: Long, @SerialName("running_since") override val runningSince: Long, @SerialName("content") override val content: C, diff --git a/trixnity-core/src/commonMain/kotlin/net/folivo/trixnity/core/serialization/events/DelayedEventSerializer.kt b/trixnity-core/src/commonMain/kotlin/net/folivo/trixnity/core/serialization/events/DelayedEventSerializer.kt new file mode 100644 index 000000000..c9515aaf0 --- /dev/null +++ b/trixnity-core/src/commonMain/kotlin/net/folivo/trixnity/core/serialization/events/DelayedEventSerializer.kt @@ -0,0 +1,41 @@ +package net.folivo.trixnity.core.serialization.events + +import kotlinx.serialization.KSerializer +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.descriptors.buildClassSerialDescriptor +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder +import kotlinx.serialization.json.JsonDecoder +import kotlinx.serialization.json.JsonEncoder +import kotlinx.serialization.json.jsonObject +import net.folivo.trixnity.core.MSC4140 +import net.folivo.trixnity.core.model.events.ScheduledDelayedEvent +import net.folivo.trixnity.core.model.events.ScheduledDelayedEvent.ScheduledDelayedMessageEvent +import net.folivo.trixnity.core.model.events.ScheduledDelayedEvent.ScheduledDelayedStateEvent +import net.folivo.trixnity.core.serialization.canonicalJson + +@OptIn(MSC4140::class) +class DelayedEventSerializer( + private val messageEventSerializer: KSerializer>, + private val stateEventSerializer: KSerializer>, +) : KSerializer> { + override val descriptor: SerialDescriptor = + buildClassSerialDescriptor("DelayedEventSerializer") + + override fun deserialize(decoder: Decoder): ScheduledDelayedEvent<*> { + require(decoder is JsonDecoder) + val jsonObj = decoder.decodeJsonElement().jsonObject + val hasStateKey = "state_key" in jsonObj + val serializer = if (hasStateKey) stateEventSerializer else messageEventSerializer + return decoder.json.decodeFromJsonElement(serializer, jsonObj) + } + + override fun serialize(encoder: Encoder, value: ScheduledDelayedEvent<*>) { + require(encoder is JsonEncoder) + val jsonElement = when (value) { + is ScheduledDelayedMessageEvent -> encoder.json.encodeToJsonElement(messageEventSerializer, value) + is ScheduledDelayedStateEvent -> encoder.json.encodeToJsonElement(stateEventSerializer, value) + } + encoder.encodeJsonElement(canonicalJson(jsonElement)) + } +} diff --git a/trixnity-core/src/commonMain/kotlin/net/folivo/trixnity/core/serialization/events/DelayedMessageEventSerializer.kt b/trixnity-core/src/commonMain/kotlin/net/folivo/trixnity/core/serialization/events/DelayedMessageEventSerializer.kt new file mode 100644 index 000000000..9e560614b --- /dev/null +++ b/trixnity-core/src/commonMain/kotlin/net/folivo/trixnity/core/serialization/events/DelayedMessageEventSerializer.kt @@ -0,0 +1,18 @@ +package net.folivo.trixnity.core.serialization.events + +import net.folivo.trixnity.core.MSC4140 +import net.folivo.trixnity.core.model.events.MessageEventContent +import net.folivo.trixnity.core.model.events.ScheduledDelayedEvent.ScheduledDelayedMessageEvent + +@MSC4140 +class DelayedMessageEventSerializer( + messageEventContentSerializers: Set>, +) : BaseEventSerializer>( + "ScheduledDelayedMessageEvent", + RoomEventContentToEventSerializerMappings( + baseMapping = messageEventContentSerializers, + eventDeserializer = { ScheduledDelayedMessageEvent.serializer(it.serializer) }, + unknownEventSerializer = { ScheduledDelayedMessageEvent.serializer(UnknownEventContentSerializer(it)) }, + redactedEventSerializer = { ScheduledDelayedMessageEvent.serializer(RedactedEventContentSerializer(it)) }, + ) +) diff --git a/trixnity-core/src/commonMain/kotlin/net/folivo/trixnity/core/serialization/events/DelayedStateEventSerializer.kt b/trixnity-core/src/commonMain/kotlin/net/folivo/trixnity/core/serialization/events/DelayedStateEventSerializer.kt new file mode 100644 index 000000000..96d71ed5c --- /dev/null +++ b/trixnity-core/src/commonMain/kotlin/net/folivo/trixnity/core/serialization/events/DelayedStateEventSerializer.kt @@ -0,0 +1,18 @@ +package net.folivo.trixnity.core.serialization.events + +import net.folivo.trixnity.core.MSC4140 +import net.folivo.trixnity.core.model.events.ScheduledDelayedEvent.ScheduledDelayedStateEvent +import net.folivo.trixnity.core.model.events.StateEventContent + +@MSC4140 +class DelayedStateEventSerializer( + stateEventContentSerializers: Set>, +) : BaseEventSerializer>( + "ScheduledDelayedStateEvent", + RoomEventContentToEventSerializerMappings( + baseMapping = stateEventContentSerializers, + eventDeserializer = { ScheduledDelayedStateEvent.serializer(it.serializer) }, + unknownEventSerializer = { ScheduledDelayedStateEvent.serializer(UnknownEventContentSerializer(it)) }, + redactedEventSerializer = { ScheduledDelayedStateEvent.serializer(RedactedEventContentSerializer(it)) }, + ) +) diff --git a/trixnity-core/src/commonMain/kotlin/net/folivo/trixnity/core/serialization/events/createEventSerializersModule.kt b/trixnity-core/src/commonMain/kotlin/net/folivo/trixnity/core/serialization/events/createEventSerializersModule.kt index 1293611a4..eca30bca0 100644 --- a/trixnity-core/src/commonMain/kotlin/net/folivo/trixnity/core/serialization/events/createEventSerializersModule.kt +++ b/trixnity-core/src/commonMain/kotlin/net/folivo/trixnity/core/serialization/events/createEventSerializersModule.kt @@ -2,8 +2,10 @@ package net.folivo.trixnity.core.serialization.events import kotlinx.serialization.modules.SerializersModule import kotlinx.serialization.modules.contextual +import net.folivo.trixnity.core.MSC4140 import net.folivo.trixnity.core.model.events.EventContent +@OptIn(MSC4140::class) fun createEventSerializersModule( mappings: EventContentSerializerMappings, ): SerializersModule { @@ -26,6 +28,10 @@ fun createEventSerializersModule( val globalAccountDataEventSerializer = GlobalAccountDataEventSerializer(mappings.globalAccountData) val roomAccountDataEventSerializer = RoomAccountDataEventSerializer(mappings.roomAccountData) val eventTypeSerializer = EventTypeSerializer(mappings) + val delayedMessageEventSerializer = DelayedMessageEventSerializer(mappings.message) + val delayedStateEventSerializer = DelayedStateEventSerializer(mappings.state) + val delayedEventSerializer = DelayedEventSerializer(delayedMessageEventSerializer, delayedStateEventSerializer) + return SerializersModule { contextual(contextualMessageEventContentSerializer) contextual(contextualStateEventContentSerializer) @@ -42,5 +48,6 @@ fun createEventSerializersModule( contextual(globalAccountDataEventSerializer) contextual(roomAccountDataEventSerializer) contextual(eventTypeSerializer) + contextual(delayedEventSerializer) } -} \ No newline at end of file +} -- GitLab From 3802d34d7b5157e6634acc48322d6e24e5f58d26 Mon Sep 17 00:00:00 2001 From: Manuel Stahl Date: Tue, 21 Oct 2025 16:42:51 +0200 Subject: [PATCH 06/11] MSC4104: Rename DelayId to OpaqueId --- .../client/DelayedEventsClient.kt | 18 +++++++++--------- .../clientserverapi/client/RoomApiClient.kt | 10 +++++----- .../client/DelayedEventsClientTest.kt | 16 ++++++++-------- .../client/RoomsApiClientTest.kt | 10 +++++----- .../model/delayedevents/CancelDelayedEvent.kt | 4 ++-- .../model/delayedevents/GetDelayedEvents.kt | 4 ++-- .../model/delayedevents/RestartDelayedEvent.kt | 4 ++-- .../model/delayedevents/SendDelayedEvent.kt | 4 ++-- .../model/rooms/SendDelayedEventResponse.kt | 5 ++--- .../clientserverapi/server/RoomsRoutesTest.kt | 6 +++--- .../core/model/{DelayId.kt => OpaqueId.kt} | 16 +++++++--------- .../trixnity/core/model/events/DelayedEvent.kt | 8 ++++---- .../folivo/trixnity/core/util/MatrixIdRegex.kt | 10 +--------- 13 files changed, 52 insertions(+), 63 deletions(-) rename trixnity-core/src/commonMain/kotlin/net/folivo/trixnity/core/model/{DelayId.kt => OpaqueId.kt} (65%) diff --git a/trixnity-clientserverapi/trixnity-clientserverapi-client/src/commonMain/kotlin/net/folivo/trixnity/clientserverapi/client/DelayedEventsClient.kt b/trixnity-clientserverapi/trixnity-clientserverapi-client/src/commonMain/kotlin/net/folivo/trixnity/clientserverapi/client/DelayedEventsClient.kt index fde8cc97d..cb45c6578 100644 --- a/trixnity-clientserverapi/trixnity-clientserverapi-client/src/commonMain/kotlin/net/folivo/trixnity/clientserverapi/client/DelayedEventsClient.kt +++ b/trixnity-clientserverapi/trixnity-clientserverapi-client/src/commonMain/kotlin/net/folivo/trixnity/clientserverapi/client/DelayedEventsClient.kt @@ -6,7 +6,7 @@ import net.folivo.trixnity.clientserverapi.model.delayedevents.GetDelayedEvents import net.folivo.trixnity.clientserverapi.model.delayedevents.RestartDelayedEvent import net.folivo.trixnity.clientserverapi.model.delayedevents.SendDelayedEvent import net.folivo.trixnity.core.MSC4140 -import net.folivo.trixnity.core.model.DelayId +import net.folivo.trixnity.core.model.OpaqueId @MSC4140 interface DelayedEventsClient { @@ -16,24 +16,24 @@ interface DelayedEventsClient { */ suspend fun getDelayedEvents( status: DelayedEventStatus? = null, - delayIds: List? = null, + delayIds: List? = null, from: String? = null, ): Result /** * @see [SendDelayedEvent] */ - suspend fun sendDelayedEvent(delayId: DelayId): Result + suspend fun sendDelayedEvent(delayId: OpaqueId): Result /** * @see [CancelDelayedEvent] */ - suspend fun cancelDelayedEvent(delayId: DelayId): Result + suspend fun cancelDelayedEvent(delayId: OpaqueId): Result /** * @see [RestartDelayedEvent] */ - suspend fun restartDelayedEvent(delayId: DelayId): Result + suspend fun restartDelayedEvent(delayId: OpaqueId): Result } @MSC4140 @@ -43,17 +43,17 @@ class DelayedEventsClientImpl( override suspend fun getDelayedEvents( status: DelayedEventStatus?, - delayIds: List?, + delayIds: List?, from: String?, ): Result = baseClient.request(GetDelayedEvents(status, delayIds, from)) - override suspend fun sendDelayedEvent(delayId: DelayId): Result = + override suspend fun sendDelayedEvent(delayId: OpaqueId): Result = baseClient.request(SendDelayedEvent(delayId), SendDelayedEvent.Request()) - override suspend fun cancelDelayedEvent(delayId: DelayId): Result = + override suspend fun cancelDelayedEvent(delayId: OpaqueId): Result = baseClient.request(CancelDelayedEvent(delayId), CancelDelayedEvent.Request()) - override suspend fun restartDelayedEvent(delayId: DelayId): Result = + override suspend fun restartDelayedEvent(delayId: OpaqueId): Result = baseClient.request(RestartDelayedEvent(delayId), RestartDelayedEvent.Request()) } diff --git a/trixnity-clientserverapi/trixnity-clientserverapi-client/src/commonMain/kotlin/net/folivo/trixnity/clientserverapi/client/RoomApiClient.kt b/trixnity-clientserverapi/trixnity-clientserverapi-client/src/commonMain/kotlin/net/folivo/trixnity/clientserverapi/client/RoomApiClient.kt index bbc0722b7..462c3da02 100644 --- a/trixnity-clientserverapi/trixnity-clientserverapi-client/src/commonMain/kotlin/net/folivo/trixnity/clientserverapi/client/RoomApiClient.kt +++ b/trixnity-clientserverapi/trixnity-clientserverapi-client/src/commonMain/kotlin/net/folivo/trixnity/clientserverapi/client/RoomApiClient.kt @@ -2,7 +2,7 @@ package net.folivo.trixnity.clientserverapi.client import net.folivo.trixnity.clientserverapi.model.rooms.* import net.folivo.trixnity.core.MSC4140 -import net.folivo.trixnity.core.model.DelayId +import net.folivo.trixnity.core.model.OpaqueId import net.folivo.trixnity.core.model.EventId import net.folivo.trixnity.core.model.RoomAliasId import net.folivo.trixnity.core.model.RoomId @@ -158,7 +158,7 @@ interface RoomApiClient { delayMs: Long, // TODO: must be > 0, new type? stateKey: String = "", asUserId: UserId? = null - ): Result + ): Result /** * @see [SendMessageEvent] @@ -180,7 +180,7 @@ interface RoomApiClient { txnId: String = Random.nextString(22), delayMs: Long, // TODO: must be > 0, new type? asUserId: UserId? = null - ): Result + ): Result /** * @see [RedactEvent] @@ -675,7 +675,7 @@ class RoomApiClientImpl( delayMs: Long, stateKey: String, asUserId: UserId? - ): Result { + ): Result { val eventType = contentMappings.state.contentType(eventContent) return baseClient.request(SendDelayedStateEvent(roomId, eventType, delayMs, stateKey, asUserId), eventContent) .mapCatching { it.delayId } @@ -699,7 +699,7 @@ class RoomApiClientImpl( txnId: String, delayMs: Long, asUserId: UserId? - ): Result { + ): Result { val eventType = contentMappings.message.contentType(eventContent) return baseClient.request(SendDelayedMessageEvent(roomId, eventType, txnId, delayMs, asUserId), eventContent) .mapCatching { it.delayId } diff --git a/trixnity-clientserverapi/trixnity-clientserverapi-client/src/commonTest/kotlin/net/folivo/trixnity/clientserverapi/client/DelayedEventsClientTest.kt b/trixnity-clientserverapi/trixnity-clientserverapi-client/src/commonTest/kotlin/net/folivo/trixnity/clientserverapi/client/DelayedEventsClientTest.kt index 2c9242048..e705af953 100644 --- a/trixnity-clientserverapi/trixnity-clientserverapi-client/src/commonTest/kotlin/net/folivo/trixnity/clientserverapi/client/DelayedEventsClientTest.kt +++ b/trixnity-clientserverapi/trixnity-clientserverapi-client/src/commonTest/kotlin/net/folivo/trixnity/clientserverapi/client/DelayedEventsClientTest.kt @@ -16,7 +16,7 @@ import net.folivo.trixnity.clientserverapi.model.delayedevents.GetDelayedEvents import net.folivo.trixnity.core.model.events.ScheduledDelayedEvent.ScheduledDelayedMessageEvent import net.folivo.trixnity.core.model.events.ScheduledDelayedEvent.ScheduledDelayedStateEvent import net.folivo.trixnity.core.MSC4140 -import net.folivo.trixnity.core.model.DelayId +import net.folivo.trixnity.core.model.OpaqueId import net.folivo.trixnity.core.model.EventId import net.folivo.trixnity.core.model.RoomId import net.folivo.trixnity.core.model.events.m.room.NameEventContent @@ -37,7 +37,7 @@ class DelayedEventsClientTest : TrixnityBaseTest() { val response = GetDelayedEvents.Response( scheduled = listOf( ScheduledDelayedMessageEvent( - delayId = DelayId("d1"), + delayId = OpaqueId("d1"), roomId = RoomId("room:server"), type = "m.room.message", delayMs = 1000, @@ -51,7 +51,7 @@ class DelayedEventsClientTest : TrixnityBaseTest() { finalised = listOf( FinalizedDelayedEvent( delayedEvent = ScheduledDelayedStateEvent( - delayId = DelayId("d2"), + delayId = OpaqueId("d2"), roomId = RoomId("room:server"), type = "m.room.name", stateKey = "", @@ -85,8 +85,8 @@ class DelayedEventsClientTest : TrixnityBaseTest() { } }) val result = matrixRestClient.delayedEvents.getDelayedEvents().getOrThrow() - result.scheduled!!.first().delayId shouldBe DelayId("d1") - result.finalised!!.first().delayedEvent.delayId shouldBe DelayId("d2") + result.scheduled!!.first().delayId shouldBe OpaqueId("d1") + result.finalised!!.first().delayedEvent.delayId shouldBe OpaqueId("d2") } @Test @@ -108,7 +108,7 @@ class DelayedEventsClientTest : TrixnityBaseTest() { ) } }) - matrixRestClient.delayedEvents.sendDelayedEvent(DelayId("d1")).getOrThrow() + matrixRestClient.delayedEvents.sendDelayedEvent(OpaqueId("d1")).getOrThrow() } @Test @@ -129,7 +129,7 @@ class DelayedEventsClientTest : TrixnityBaseTest() { ) } }) - matrixRestClient.delayedEvents.cancelDelayedEvent(DelayId("d1")).getOrThrow() + matrixRestClient.delayedEvents.cancelDelayedEvent(OpaqueId("d1")).getOrThrow() } @Test @@ -150,6 +150,6 @@ class DelayedEventsClientTest : TrixnityBaseTest() { ) } }) - matrixRestClient.delayedEvents.restartDelayedEvent(DelayId("d1")).getOrThrow() + matrixRestClient.delayedEvents.restartDelayedEvent(OpaqueId("d1")).getOrThrow() } } diff --git a/trixnity-clientserverapi/trixnity-clientserverapi-client/src/commonTest/kotlin/net/folivo/trixnity/clientserverapi/client/RoomsApiClientTest.kt b/trixnity-clientserverapi/trixnity-clientserverapi-client/src/commonTest/kotlin/net/folivo/trixnity/clientserverapi/client/RoomsApiClientTest.kt index a7f28b17f..70eb512d0 100644 --- a/trixnity-clientserverapi/trixnity-clientserverapi-client/src/commonTest/kotlin/net/folivo/trixnity/clientserverapi/client/RoomsApiClientTest.kt +++ b/trixnity-clientserverapi/trixnity-clientserverapi-client/src/commonTest/kotlin/net/folivo/trixnity/clientserverapi/client/RoomsApiClientTest.kt @@ -9,7 +9,7 @@ import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.builtins.ListSerializer import net.folivo.trixnity.clientserverapi.model.rooms.* import net.folivo.trixnity.core.MSC4140 -import net.folivo.trixnity.core.model.DelayId +import net.folivo.trixnity.core.model.OpaqueId import net.folivo.trixnity.core.model.EventId import net.folivo.trixnity.core.model.RoomAliasId import net.folivo.trixnity.core.model.RoomId @@ -601,7 +601,7 @@ class RoomsApiClientTest : TrixnityBaseTest() { @OptIn(MSC4140::class) @Test fun shouldSendDelayedStateEvent() = runTest { - val response = SendDelayedEventResponse(DelayId("delay")) + val response = SendDelayedEventResponse(OpaqueId("delay")) val matrixRestClient = MatrixClientServerApiClientImpl( baseUrl = Url("https://matrix.host"), httpClientEngine = scopedMockEngine { @@ -627,7 +627,7 @@ class RoomsApiClientTest : TrixnityBaseTest() { stateKey = "someStateKey", delayMs = 123456, ).getOrThrow() - assertEquals(DelayId("delay"), result) + assertEquals(OpaqueId("delay"), result) } @Test @@ -691,7 +691,7 @@ class RoomsApiClientTest : TrixnityBaseTest() { @OptIn(MSC4140::class) @Test fun shouldSendDelayedRoomEvent() = runTest { - val response = SendDelayedEventResponse(DelayId("delay")) + val response = SendDelayedEventResponse(OpaqueId("delay")) val matrixRestClient = MatrixClientServerApiClientImpl( baseUrl = Url("https://matrix.host"), httpClientEngine = scopedMockEngine { @@ -719,7 +719,7 @@ class RoomsApiClientTest : TrixnityBaseTest() { txnId = "someTxnId", delayMs = 123456, ).getOrThrow() - assertEquals(DelayId("delay"), result) + assertEquals(OpaqueId("delay"), result) } @Test diff --git a/trixnity-clientserverapi/trixnity-clientserverapi-model/src/commonMain/kotlin/net/folivo/trixnity/clientserverapi/model/delayedevents/CancelDelayedEvent.kt b/trixnity-clientserverapi/trixnity-clientserverapi-model/src/commonMain/kotlin/net/folivo/trixnity/clientserverapi/model/delayedevents/CancelDelayedEvent.kt index e7037686b..64fa58309 100644 --- a/trixnity-clientserverapi/trixnity-clientserverapi-model/src/commonMain/kotlin/net/folivo/trixnity/clientserverapi/model/delayedevents/CancelDelayedEvent.kt +++ b/trixnity-clientserverapi/trixnity-clientserverapi-model/src/commonMain/kotlin/net/folivo/trixnity/clientserverapi/model/delayedevents/CancelDelayedEvent.kt @@ -7,7 +7,7 @@ import net.folivo.trixnity.core.HttpMethod import net.folivo.trixnity.core.HttpMethodType.POST import net.folivo.trixnity.core.MSC4140 import net.folivo.trixnity.core.MatrixEndpoint -import net.folivo.trixnity.core.model.DelayId +import net.folivo.trixnity.core.model.OpaqueId /** * @see matrix spec @@ -18,7 +18,7 @@ import net.folivo.trixnity.core.model.DelayId @Resource("/_matrix/client/unstable/org.matrix.msc4140/delayed_events/{delayId}") @HttpMethod(POST) data class CancelDelayedEvent( - @SerialName("delayId") val delayId: DelayId, + @SerialName("delayId") val delayId: OpaqueId, ) : MatrixEndpoint { @Serializable data class Request( diff --git a/trixnity-clientserverapi/trixnity-clientserverapi-model/src/commonMain/kotlin/net/folivo/trixnity/clientserverapi/model/delayedevents/GetDelayedEvents.kt b/trixnity-clientserverapi/trixnity-clientserverapi-model/src/commonMain/kotlin/net/folivo/trixnity/clientserverapi/model/delayedevents/GetDelayedEvents.kt index df70f430a..998de8ff7 100644 --- a/trixnity-clientserverapi/trixnity-clientserverapi-model/src/commonMain/kotlin/net/folivo/trixnity/clientserverapi/model/delayedevents/GetDelayedEvents.kt +++ b/trixnity-clientserverapi/trixnity-clientserverapi-model/src/commonMain/kotlin/net/folivo/trixnity/clientserverapi/model/delayedevents/GetDelayedEvents.kt @@ -8,7 +8,7 @@ import net.folivo.trixnity.core.HttpMethod import net.folivo.trixnity.core.HttpMethodType.GET import net.folivo.trixnity.core.MSC4140 import net.folivo.trixnity.core.MatrixEndpoint -import net.folivo.trixnity.core.model.DelayId +import net.folivo.trixnity.core.model.OpaqueId import net.folivo.trixnity.core.model.events.DelayedEventStatus import net.folivo.trixnity.core.model.events.FinalizedDelayedEvent import net.folivo.trixnity.core.model.events.ScheduledDelayedEvent @@ -23,7 +23,7 @@ import net.folivo.trixnity.core.model.events.ScheduledDelayedEvent @HttpMethod(GET) data class GetDelayedEvents( @SerialName("status") val status: DelayedEventStatus?, - @SerialName("delay_id") val delayIds: List?, + @SerialName("delay_id") val delayIds: List?, @SerialName("from") val from: String?, ) : MatrixEndpoint { diff --git a/trixnity-clientserverapi/trixnity-clientserverapi-model/src/commonMain/kotlin/net/folivo/trixnity/clientserverapi/model/delayedevents/RestartDelayedEvent.kt b/trixnity-clientserverapi/trixnity-clientserverapi-model/src/commonMain/kotlin/net/folivo/trixnity/clientserverapi/model/delayedevents/RestartDelayedEvent.kt index 580772076..0f8864c13 100644 --- a/trixnity-clientserverapi/trixnity-clientserverapi-model/src/commonMain/kotlin/net/folivo/trixnity/clientserverapi/model/delayedevents/RestartDelayedEvent.kt +++ b/trixnity-clientserverapi/trixnity-clientserverapi-model/src/commonMain/kotlin/net/folivo/trixnity/clientserverapi/model/delayedevents/RestartDelayedEvent.kt @@ -7,7 +7,7 @@ import net.folivo.trixnity.core.HttpMethod import net.folivo.trixnity.core.HttpMethodType.POST import net.folivo.trixnity.core.MSC4140 import net.folivo.trixnity.core.MatrixEndpoint -import net.folivo.trixnity.core.model.DelayId +import net.folivo.trixnity.core.model.OpaqueId /** * @see matrix spec @@ -18,7 +18,7 @@ import net.folivo.trixnity.core.model.DelayId @Resource("/_matrix/client/unstable/org.matrix.msc4140/delayed_events/{delayId}") @HttpMethod(POST) data class RestartDelayedEvent( - @SerialName("delayId") val delayId: DelayId, + @SerialName("delayId") val delayId: OpaqueId, ) : MatrixEndpoint { @Serializable data class Request( diff --git a/trixnity-clientserverapi/trixnity-clientserverapi-model/src/commonMain/kotlin/net/folivo/trixnity/clientserverapi/model/delayedevents/SendDelayedEvent.kt b/trixnity-clientserverapi/trixnity-clientserverapi-model/src/commonMain/kotlin/net/folivo/trixnity/clientserverapi/model/delayedevents/SendDelayedEvent.kt index ed844207a..64d4dbfbc 100644 --- a/trixnity-clientserverapi/trixnity-clientserverapi-model/src/commonMain/kotlin/net/folivo/trixnity/clientserverapi/model/delayedevents/SendDelayedEvent.kt +++ b/trixnity-clientserverapi/trixnity-clientserverapi-model/src/commonMain/kotlin/net/folivo/trixnity/clientserverapi/model/delayedevents/SendDelayedEvent.kt @@ -7,7 +7,7 @@ import net.folivo.trixnity.core.HttpMethod import net.folivo.trixnity.core.HttpMethodType.POST import net.folivo.trixnity.core.MSC4140 import net.folivo.trixnity.core.MatrixEndpoint -import net.folivo.trixnity.core.model.DelayId +import net.folivo.trixnity.core.model.OpaqueId /** * @see matrix spec @@ -18,7 +18,7 @@ import net.folivo.trixnity.core.model.DelayId @Resource("/_matrix/client/unstable/org.matrix.msc4140/delayed_events/{delayId}") @HttpMethod(POST) data class SendDelayedEvent( - @SerialName("delayId") val delayId: DelayId, + @SerialName("delayId") val delayId: OpaqueId, ) : MatrixEndpoint { @Serializable data class Request( diff --git a/trixnity-clientserverapi/trixnity-clientserverapi-model/src/commonMain/kotlin/net/folivo/trixnity/clientserverapi/model/rooms/SendDelayedEventResponse.kt b/trixnity-clientserverapi/trixnity-clientserverapi-model/src/commonMain/kotlin/net/folivo/trixnity/clientserverapi/model/rooms/SendDelayedEventResponse.kt index e9ae75df5..a229d0844 100644 --- a/trixnity-clientserverapi/trixnity-clientserverapi-model/src/commonMain/kotlin/net/folivo/trixnity/clientserverapi/model/rooms/SendDelayedEventResponse.kt +++ b/trixnity-clientserverapi/trixnity-clientserverapi-model/src/commonMain/kotlin/net/folivo/trixnity/clientserverapi/model/rooms/SendDelayedEventResponse.kt @@ -2,12 +2,11 @@ package net.folivo.trixnity.clientserverapi.model.rooms import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable -import net.folivo.trixnity.core.model.EventId import net.folivo.trixnity.core.MSC4140 -import net.folivo.trixnity.core.model.DelayId +import net.folivo.trixnity.core.model.OpaqueId @MSC4140 @Serializable data class SendDelayedEventResponse( - @SerialName("delay_id") val delayId: DelayId, + @SerialName("delay_id") val delayId: OpaqueId, ) diff --git a/trixnity-clientserverapi/trixnity-clientserverapi-server/src/commonTest/kotlin/net/folivo/trixnity/clientserverapi/server/RoomsRoutesTest.kt b/trixnity-clientserverapi/trixnity-clientserverapi-server/src/commonTest/kotlin/net/folivo/trixnity/clientserverapi/server/RoomsRoutesTest.kt index 34fac9188..f01eca1f0 100644 --- a/trixnity-clientserverapi/trixnity-clientserverapi-server/src/commonTest/kotlin/net/folivo/trixnity/clientserverapi/server/RoomsRoutesTest.kt +++ b/trixnity-clientserverapi/trixnity-clientserverapi-server/src/commonTest/kotlin/net/folivo/trixnity/clientserverapi/server/RoomsRoutesTest.kt @@ -15,7 +15,7 @@ import kotlinx.serialization.json.JsonPrimitive import net.folivo.trixnity.api.server.matrixApiServer import net.folivo.trixnity.clientserverapi.model.rooms.* import net.folivo.trixnity.core.MSC4140 -import net.folivo.trixnity.core.model.DelayId +import net.folivo.trixnity.core.model.OpaqueId import net.folivo.trixnity.core.model.EventId import net.folivo.trixnity.core.model.RoomAliasId import net.folivo.trixnity.core.model.RoomId @@ -700,7 +700,7 @@ class RoomsRoutesTest : TrixnityBaseTest() { fun shouldSendDelayedStateEvent() = testApplication { initCut() everySuspend { handlerMock.sendDelayedStateEvent(any()) } - .returns(SendDelayedEventResponse(DelayId("delay123"))) + .returns(SendDelayedEventResponse(OpaqueId("delay123"))) val response = client.put("/_matrix/client/v3/rooms/!room:server/state/m.room.name/?delay=1000") { bearerAuth("token") @@ -795,7 +795,7 @@ class RoomsRoutesTest : TrixnityBaseTest() { fun shouldSendDelayedMessageEvent() = testApplication { initCut() everySuspend { handlerMock.sendDelayedMessageEvent(any()) } - .returns(SendDelayedEventResponse(DelayId("delay123"))) + .returns(SendDelayedEventResponse(OpaqueId("delay123"))) val response = client.put("/_matrix/client/v3/rooms/!room:server/send/m.room.message/someTxnId?delay=1000") { bearerAuth("token") diff --git a/trixnity-core/src/commonMain/kotlin/net/folivo/trixnity/core/model/DelayId.kt b/trixnity-core/src/commonMain/kotlin/net/folivo/trixnity/core/model/OpaqueId.kt similarity index 65% rename from trixnity-core/src/commonMain/kotlin/net/folivo/trixnity/core/model/DelayId.kt rename to trixnity-core/src/commonMain/kotlin/net/folivo/trixnity/core/model/OpaqueId.kt index 433210aba..3e8141ec6 100644 --- a/trixnity-core/src/commonMain/kotlin/net/folivo/trixnity/core/model/DelayId.kt +++ b/trixnity-core/src/commonMain/kotlin/net/folivo/trixnity/core/model/OpaqueId.kt @@ -10,11 +10,10 @@ import kotlinx.serialization.encoding.Encoder import net.folivo.trixnity.core.MSC4140 import net.folivo.trixnity.core.util.MatrixIdRegex -@MSC4140 -@Serializable(with = DelayIdSerializer::class) -data class DelayId(val full: String) { +@Serializable(with = OpaqueIdSerializer::class) +data class OpaqueId(val full: String) { companion object { - fun isValid(id: String): Boolean = id.length <= 255 && id.matches(MatrixIdRegex.delayId) + fun isValid(id: String): Boolean = id.length <= 255 && id.matches(MatrixIdRegex.opaqueId) } val isValid by lazy { isValid(full) } @@ -22,12 +21,11 @@ data class DelayId(val full: String) { override fun toString() = full } -@OptIn(MSC4140::class) -object DelayIdSerializer : KSerializer { - override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("DelayIdSerializer", PrimitiveKind.STRING) - override fun deserialize(decoder: Decoder): DelayId = DelayId(decoder.decodeString()) +object OpaqueIdSerializer : KSerializer { + override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("OpaqueIdSerializer", PrimitiveKind.STRING) + override fun deserialize(decoder: Decoder): OpaqueId = OpaqueId(decoder.decodeString()) - override fun serialize(encoder: Encoder, value: DelayId) { + override fun serialize(encoder: Encoder, value: OpaqueId) { encoder.encodeString(value.full) } } diff --git a/trixnity-core/src/commonMain/kotlin/net/folivo/trixnity/core/model/events/DelayedEvent.kt b/trixnity-core/src/commonMain/kotlin/net/folivo/trixnity/core/model/events/DelayedEvent.kt index 5c2bb5557..1271285a6 100644 --- a/trixnity-core/src/commonMain/kotlin/net/folivo/trixnity/core/model/events/DelayedEvent.kt +++ b/trixnity-core/src/commonMain/kotlin/net/folivo/trixnity/core/model/events/DelayedEvent.kt @@ -4,7 +4,7 @@ import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable import net.folivo.trixnity.core.ErrorResponse import net.folivo.trixnity.core.MSC4140 -import net.folivo.trixnity.core.model.DelayId +import net.folivo.trixnity.core.model.OpaqueId import net.folivo.trixnity.core.model.EventId import net.folivo.trixnity.core.model.RoomId @@ -32,7 +32,7 @@ enum class DelayedEventStatus { @MSC4140 @Serializable sealed interface ScheduledDelayedEvent: Event { - val delayId: DelayId + val delayId: OpaqueId val roomId: RoomId val type: String val delayMs: Long @@ -43,7 +43,7 @@ sealed interface ScheduledDelayedEvent: Event { */ @Serializable data class ScheduledDelayedStateEvent( - @SerialName("delay_id") override val delayId: DelayId, + @SerialName("delay_id") override val delayId: OpaqueId, @SerialName("room_id") override val roomId: RoomId, @SerialName("type") override val type: String, @SerialName("state_key") val stateKey: String, @@ -55,7 +55,7 @@ sealed interface ScheduledDelayedEvent: Event { @Serializable data class ScheduledDelayedMessageEvent( - @SerialName("delay_id") override val delayId: DelayId, + @SerialName("delay_id") override val delayId: OpaqueId, @SerialName("room_id") override val roomId: RoomId, @SerialName("type") override val type: String, @SerialName("delay") override val delayMs: Long, diff --git a/trixnity-core/src/commonMain/kotlin/net/folivo/trixnity/core/util/MatrixIdRegex.kt b/trixnity-core/src/commonMain/kotlin/net/folivo/trixnity/core/util/MatrixIdRegex.kt index c0b412bc6..afbad0193 100644 --- a/trixnity-core/src/commonMain/kotlin/net/folivo/trixnity/core/util/MatrixIdRegex.kt +++ b/trixnity-core/src/commonMain/kotlin/net/folivo/trixnity/core/util/MatrixIdRegex.kt @@ -14,6 +14,7 @@ internal object MatrixIdRegex { */ // language=Regexp private const val OPAQUE_ID_PATTERN = "[0-9A-Za-z-._~]+" + val opaqueId = OPAQUE_ID_PATTERN.toRegex() /** * clients and servers MUST accept user IDs with localparts consisting of any legal non-surrogate Unicode @@ -66,15 +67,6 @@ internal object MatrixIdRegex { private const val REASONABLE_EVENT_ID_PATTERN = "\\$$OPAQUE_ID_PATTERN(?::$DOMAIN_PATTERN)?" val reasonableEventId = REASONABLE_EVENT_ID_PATTERN.toRegex() - /** - * The delay_id is an opaque identifier generated by the server. - * - * @see [https://github.com/matrix-org/matrix-spec-proposals/blob/toger5/expiring-events-keep-alive/proposals/4140-delayed-events-futures.md#scheduling-a-delayed-event] - */ - @MSC4140 - // language=Regexp - val delayId = OPAQUE_ID_PATTERN.toRegex() - /** * This matches user ids and room aliases on best-effort basis, * it is NOT intended to match all valid ids. -- GitLab From ab8308c7a371dfc51c176f6089919d80cf353ffd Mon Sep 17 00:00:00 2001 From: Manuel Stahl Date: Tue, 21 Oct 2025 17:13:42 +0200 Subject: [PATCH 07/11] MSC4140: Remove type from ScheduledDelayedEvent --- .../clientserverapi/client/DelayedEventsClientTest.kt | 2 -- .../net/folivo/trixnity/core/model/events/DelayedEvent.kt | 4 ---- 2 files changed, 6 deletions(-) diff --git a/trixnity-clientserverapi/trixnity-clientserverapi-client/src/commonTest/kotlin/net/folivo/trixnity/clientserverapi/client/DelayedEventsClientTest.kt b/trixnity-clientserverapi/trixnity-clientserverapi-client/src/commonTest/kotlin/net/folivo/trixnity/clientserverapi/client/DelayedEventsClientTest.kt index e705af953..22424e8e5 100644 --- a/trixnity-clientserverapi/trixnity-clientserverapi-client/src/commonTest/kotlin/net/folivo/trixnity/clientserverapi/client/DelayedEventsClientTest.kt +++ b/trixnity-clientserverapi/trixnity-clientserverapi-client/src/commonTest/kotlin/net/folivo/trixnity/clientserverapi/client/DelayedEventsClientTest.kt @@ -39,7 +39,6 @@ class DelayedEventsClientTest : TrixnityBaseTest() { ScheduledDelayedMessageEvent( delayId = OpaqueId("d1"), roomId = RoomId("room:server"), - type = "m.room.message", delayMs = 1000, runningSince = 123456789, content = RoomMessageEventContent.Location( @@ -53,7 +52,6 @@ class DelayedEventsClientTest : TrixnityBaseTest() { delayedEvent = ScheduledDelayedStateEvent( delayId = OpaqueId("d2"), roomId = RoomId("room:server"), - type = "m.room.name", stateKey = "", delayMs = 1000, runningSince = 123456789, diff --git a/trixnity-core/src/commonMain/kotlin/net/folivo/trixnity/core/model/events/DelayedEvent.kt b/trixnity-core/src/commonMain/kotlin/net/folivo/trixnity/core/model/events/DelayedEvent.kt index 1271285a6..a08e675ea 100644 --- a/trixnity-core/src/commonMain/kotlin/net/folivo/trixnity/core/model/events/DelayedEvent.kt +++ b/trixnity-core/src/commonMain/kotlin/net/folivo/trixnity/core/model/events/DelayedEvent.kt @@ -24,7 +24,6 @@ enum class DelayedEventStatus { * @see MSC4140 * @param delayId Required. The ID of the delayed event. * @param roomId Required. The room ID of the delayed event. - * @param type Required. The event type of the delayed event. * @param delayMs Required. The delay in milliseconds before the event is to be sent. * @param runningSince Required. The timestamp (as Unix time in milliseconds) when the delayed event was scheduled or last restarted. * @param content Required. The content of the delayed event. This is the body of the original PUT request, not a preview of the full event after sending. @@ -34,7 +33,6 @@ enum class DelayedEventStatus { sealed interface ScheduledDelayedEvent: Event { val delayId: OpaqueId val roomId: RoomId - val type: String val delayMs: Long val runningSince: Long @@ -45,7 +43,6 @@ sealed interface ScheduledDelayedEvent: Event { data class ScheduledDelayedStateEvent( @SerialName("delay_id") override val delayId: OpaqueId, @SerialName("room_id") override val roomId: RoomId, - @SerialName("type") override val type: String, @SerialName("state_key") val stateKey: String, @SerialName("delay") override val delayMs: Long, @SerialName("running_since") override val runningSince: Long, @@ -57,7 +54,6 @@ sealed interface ScheduledDelayedEvent: Event { data class ScheduledDelayedMessageEvent( @SerialName("delay_id") override val delayId: OpaqueId, @SerialName("room_id") override val roomId: RoomId, - @SerialName("type") override val type: String, @SerialName("delay") override val delayMs: Long, @SerialName("running_since") override val runningSince: Long, @SerialName("content") override val content: C, -- GitLab From bccb50729ad59aec0d05b23357cc720df5dcdc80 Mon Sep 17 00:00:00 2001 From: Manuel Stahl Date: Tue, 21 Oct 2025 17:15:57 +0200 Subject: [PATCH 08/11] MSC4140: Reorder arguments for sendDelayed*Event --- .../trixnity/clientserverapi/client/RoomApiClient.kt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/trixnity-clientserverapi/trixnity-clientserverapi-client/src/commonMain/kotlin/net/folivo/trixnity/clientserverapi/client/RoomApiClient.kt b/trixnity-clientserverapi/trixnity-clientserverapi-client/src/commonMain/kotlin/net/folivo/trixnity/clientserverapi/client/RoomApiClient.kt index 462c3da02..e0b7173dc 100644 --- a/trixnity-clientserverapi/trixnity-clientserverapi-client/src/commonMain/kotlin/net/folivo/trixnity/clientserverapi/client/RoomApiClient.kt +++ b/trixnity-clientserverapi/trixnity-clientserverapi-client/src/commonMain/kotlin/net/folivo/trixnity/clientserverapi/client/RoomApiClient.kt @@ -153,9 +153,9 @@ interface RoomApiClient { */ @MSC4140 suspend fun sendDelayedStateEvent( + delayMs: Long, // TODO: must be > 0, new type? roomId: RoomId, eventContent: StateEventContent, - delayMs: Long, // TODO: must be > 0, new type? stateKey: String = "", asUserId: UserId? = null ): Result @@ -175,10 +175,10 @@ interface RoomApiClient { */ @MSC4140 suspend fun sendDelayedMessageEvent( + delayMs: Long, // TODO: must be > 0, new type? roomId: RoomId, eventContent: MessageEventContent, txnId: String = Random.nextString(22), - delayMs: Long, // TODO: must be > 0, new type? asUserId: UserId? = null ): Result @@ -670,9 +670,9 @@ class RoomApiClientImpl( @MSC4140 override suspend fun sendDelayedStateEvent( + delayMs: Long, roomId: RoomId, eventContent: StateEventContent, - delayMs: Long, stateKey: String, asUserId: UserId? ): Result { @@ -694,10 +694,10 @@ class RoomApiClientImpl( @MSC4140 override suspend fun sendDelayedMessageEvent( + delayMs: Long, roomId: RoomId, eventContent: MessageEventContent, txnId: String, - delayMs: Long, asUserId: UserId? ): Result { val eventType = contentMappings.message.contentType(eventContent) -- GitLab From 3503b90015fe4ea269291d5c4bf2a2a24abeb953 Mon Sep 17 00:00:00 2001 From: Manuel Stahl Date: Tue, 21 Oct 2025 17:13:11 +0200 Subject: [PATCH 09/11] MSC4140: Add tests for DelayedEventSerializer --- .../events/DelayedEventSerializerTest.kt | 189 ++++++++++++++++++ 1 file changed, 189 insertions(+) create mode 100644 trixnity-core/src/commonTest/kotlin/net/folivo/trixnity/core/serialization/events/DelayedEventSerializerTest.kt diff --git a/trixnity-core/src/commonTest/kotlin/net/folivo/trixnity/core/serialization/events/DelayedEventSerializerTest.kt b/trixnity-core/src/commonTest/kotlin/net/folivo/trixnity/core/serialization/events/DelayedEventSerializerTest.kt new file mode 100644 index 000000000..08422ce89 --- /dev/null +++ b/trixnity-core/src/commonTest/kotlin/net/folivo/trixnity/core/serialization/events/DelayedEventSerializerTest.kt @@ -0,0 +1,189 @@ +package net.folivo.trixnity.core.serialization.events + +import io.kotest.matchers.shouldBe +import net.folivo.trixnity.core.ErrorResponse +import net.folivo.trixnity.core.MSC4140 +import net.folivo.trixnity.core.model.EventId +import net.folivo.trixnity.core.model.OpaqueId +import net.folivo.trixnity.core.model.RoomAliasId +import net.folivo.trixnity.core.model.RoomId +import net.folivo.trixnity.core.model.events.FinalizedDelayedEvent +import net.folivo.trixnity.core.model.events.ScheduledDelayedEvent.ScheduledDelayedMessageEvent +import net.folivo.trixnity.core.model.events.ScheduledDelayedEvent.ScheduledDelayedStateEvent +import net.folivo.trixnity.core.model.events.m.room.CanonicalAliasEventContent +import net.folivo.trixnity.core.model.events.m.room.RoomMessageEventContent +import net.folivo.trixnity.core.serialization.createMatrixEventJson +import net.folivo.trixnity.core.serialization.trimToFlatJson +import net.folivo.trixnity.test.utils.TrixnityBaseTest +import kotlin.test.Test +import kotlin.test.assertEquals + +@OptIn(MSC4140::class) +class DelayedEventSerializerTest : TrixnityBaseTest() { + + private val json = createMatrixEventJson() + + val scheduledDelayedStateEvent = ScheduledDelayedStateEvent( + content = CanonicalAliasEventContent(RoomAliasId("somewhere", "example.org")), + delayId = OpaqueId("delay123"), + roomId = RoomId("!jEsUZKDJdhlrceRyVU:example.org"), + stateKey = "", + delayMs = 1000, + runningSince = 123456789, + ) + + val serializedScheduledDelayedStateEvent = """{ + "content": { + "alias":"#somewhere:example.org" + }, + "delay": 1000, + "delay_id": "delay123", + "room_id": "!jEsUZKDJdhlrceRyVU:example.org", + "running_since": 123456789, + "state_key": "", + "type": "m.room.canonical_alias" + }""".trimToFlatJson() + + @Test + fun shouldSerializeScheduledDelayedStateEvent() { + val result = json.encodeToString( + DelayedStateEventSerializer( + DefaultEventContentSerializerMappings.state + ), scheduledDelayedStateEvent + ) + result shouldBe serializedScheduledDelayedStateEvent + } + + @Test + fun shouldDeserializeDelayedStateEvent() { + val result = json.decodeFromString( + DelayedStateEventSerializer( + DefaultEventContentSerializerMappings.state + ), serializedScheduledDelayedStateEvent + ) + assertEquals(scheduledDelayedStateEvent, result) + } + + val scheduledDelayedMessageEvent = ScheduledDelayedMessageEvent( + content = RoomMessageEventContent.TextBased.Text("Hello world!"), + delayId = OpaqueId("delay456"), + roomId = RoomId("!anotherRoom:example.org"), + delayMs = 2000, + runningSince = 987654321, + ) + + val serializedScheduledDelayedMessageEvent = """{ + "content": { + "body": "Hello world!", + "msgtype": "m.text" + }, + "delay": 2000, + "delay_id": "delay456", + "room_id": "!anotherRoom:example.org", + "running_since": 987654321, + "type": "m.room.message" + }""".trimToFlatJson() + + @Test + fun shouldSerializeScheduledDelayedMessageEvent() { + val result = json.encodeToString( + DelayedMessageEventSerializer( + DefaultEventContentSerializerMappings.message + ), scheduledDelayedMessageEvent + ) + result shouldBe serializedScheduledDelayedMessageEvent + } + + @Test + fun shouldDeserializeScheduledDelayedMessageEvent() { + val result = json.decodeFromString( + DelayedMessageEventSerializer( + DefaultEventContentSerializerMappings.message + ), serializedScheduledDelayedMessageEvent + ) + assertEquals(scheduledDelayedMessageEvent, result) + } + + private val finalizedDelayedEvent: FinalizedDelayedEvent> + = FinalizedDelayedEvent( + delayedEvent = scheduledDelayedStateEvent, + outcome = FinalizedDelayedEvent.Outcome.CANCEL, + reason = FinalizedDelayedEvent.Reason.ACTION, + originServerTs = 987654321, + ) + private val serializedFinalizedDelayedEvent = """{ + "delayed_event": $serializedScheduledDelayedStateEvent, + "outcome": "cancel", + "reason": "action", + "origin_server_ts": 987654321 + }""".trimToFlatJson() + + @Test + fun shouldSerializeFinalizedDelayedEvent() { + val result = json.encodeToString( + FinalizedDelayedEvent.serializer( + DelayedStateEventSerializer( + DefaultEventContentSerializerMappings.state + ) + ), finalizedDelayedEvent + ) + result shouldBe serializedFinalizedDelayedEvent + } + + @Test + fun shouldDeserializeFinalizedDelayedEvent() { + val result = json.decodeFromString( + FinalizedDelayedEvent.serializer( + DelayedStateEventSerializer( + DefaultEventContentSerializerMappings.state + ) + ), serializedFinalizedDelayedEvent + ) + assertEquals(finalizedDelayedEvent, result) + } + + private val finalizedDelayedEventWithAllProperties: FinalizedDelayedEvent> + = FinalizedDelayedEvent( + delayedEvent = scheduledDelayedMessageEvent, + outcome = FinalizedDelayedEvent.Outcome.SEND, + reason = FinalizedDelayedEvent.Reason.DELAY, + error = ErrorResponse.Unknown("something went wrong"), + eventId = EventId("$143273582443PhrSn:example.org"), + originServerTs = 987654321, + ) + private val serializedFinalizedDelayedEventWithAllProperties = """{ + "delayed_event": $serializedScheduledDelayedMessageEvent, + "outcome": "send", + "reason": "delay", + "error": { + "errcode": "M_UNKNOWN", + "error": "something went wrong" + }, + "event_id": "$143273582443PhrSn:example.org", + "origin_server_ts": 987654321 + }""".trimToFlatJson() + + @Test + fun shouldSerializeFinalizedDelayedEventWithAllProperties() { + val result = json.encodeToString( + FinalizedDelayedEvent.serializer( + DelayedMessageEventSerializer( + DefaultEventContentSerializerMappings.message + ) + ), finalizedDelayedEventWithAllProperties + ) + result shouldBe serializedFinalizedDelayedEventWithAllProperties + } + + @Test + fun shouldDeserializeFinalizedDelayedEventWithAllProperties() { + val result = json.decodeFromString( + FinalizedDelayedEvent.serializer( + DelayedMessageEventSerializer( + DefaultEventContentSerializerMappings.message + ) + ), serializedFinalizedDelayedEventWithAllProperties + ) + assertEquals(finalizedDelayedEventWithAllProperties, result) + } +} -- GitLab From c9c119da86941da8fc65a93fe72f0d6ad6b745df Mon Sep 17 00:00:00 2001 From: Manuel Stahl Date: Tue, 21 Oct 2025 17:54:16 +0200 Subject: [PATCH 10/11] MSC4140: Merge DelayedEventsClient into RoomApiClient --- .../client/DelayedEventsClient.kt | 59 ------- .../client/MatrixClientServerApiClient.kt | 6 - .../clientserverapi/client/RoomApiClient.kt | 153 ++++++++++++------ .../client/DelayedEventsClientTest.kt | 153 ------------------ .../client/RoomsApiClientTest.kt | 127 +++++++++++++++ 5 files changed, 230 insertions(+), 268 deletions(-) delete mode 100644 trixnity-clientserverapi/trixnity-clientserverapi-client/src/commonMain/kotlin/net/folivo/trixnity/clientserverapi/client/DelayedEventsClient.kt delete mode 100644 trixnity-clientserverapi/trixnity-clientserverapi-client/src/commonTest/kotlin/net/folivo/trixnity/clientserverapi/client/DelayedEventsClientTest.kt diff --git a/trixnity-clientserverapi/trixnity-clientserverapi-client/src/commonMain/kotlin/net/folivo/trixnity/clientserverapi/client/DelayedEventsClient.kt b/trixnity-clientserverapi/trixnity-clientserverapi-client/src/commonMain/kotlin/net/folivo/trixnity/clientserverapi/client/DelayedEventsClient.kt deleted file mode 100644 index cb45c6578..000000000 --- a/trixnity-clientserverapi/trixnity-clientserverapi-client/src/commonMain/kotlin/net/folivo/trixnity/clientserverapi/client/DelayedEventsClient.kt +++ /dev/null @@ -1,59 +0,0 @@ -package net.folivo.trixnity.clientserverapi.client - -import net.folivo.trixnity.clientserverapi.model.delayedevents.CancelDelayedEvent -import net.folivo.trixnity.core.model.events.DelayedEventStatus -import net.folivo.trixnity.clientserverapi.model.delayedevents.GetDelayedEvents -import net.folivo.trixnity.clientserverapi.model.delayedevents.RestartDelayedEvent -import net.folivo.trixnity.clientserverapi.model.delayedevents.SendDelayedEvent -import net.folivo.trixnity.core.MSC4140 -import net.folivo.trixnity.core.model.OpaqueId - -@MSC4140 -interface DelayedEventsClient { - - /** - * @see [GetDelayedEvents] - */ - suspend fun getDelayedEvents( - status: DelayedEventStatus? = null, - delayIds: List? = null, - from: String? = null, - ): Result - - /** - * @see [SendDelayedEvent] - */ - suspend fun sendDelayedEvent(delayId: OpaqueId): Result - - /** - * @see [CancelDelayedEvent] - */ - suspend fun cancelDelayedEvent(delayId: OpaqueId): Result - - /** - * @see [RestartDelayedEvent] - */ - suspend fun restartDelayedEvent(delayId: OpaqueId): Result -} - -@MSC4140 -class DelayedEventsClientImpl( - private val baseClient: MatrixClientServerApiBaseClient, -) : DelayedEventsClient { - - override suspend fun getDelayedEvents( - status: DelayedEventStatus?, - delayIds: List?, - from: String?, - ): Result = - baseClient.request(GetDelayedEvents(status, delayIds, from)) - - override suspend fun sendDelayedEvent(delayId: OpaqueId): Result = - baseClient.request(SendDelayedEvent(delayId), SendDelayedEvent.Request()) - - override suspend fun cancelDelayedEvent(delayId: OpaqueId): Result = - baseClient.request(CancelDelayedEvent(delayId), CancelDelayedEvent.Request()) - - override suspend fun restartDelayedEvent(delayId: OpaqueId): Result = - baseClient.request(RestartDelayedEvent(delayId), RestartDelayedEvent.Request()) -} diff --git a/trixnity-clientserverapi/trixnity-clientserverapi-client/src/commonMain/kotlin/net/folivo/trixnity/clientserverapi/client/MatrixClientServerApiClient.kt b/trixnity-clientserverapi/trixnity-clientserverapi-client/src/commonMain/kotlin/net/folivo/trixnity/clientserverapi/client/MatrixClientServerApiClient.kt index 4d09e7c12..0cc6981eb 100644 --- a/trixnity-clientserverapi/trixnity-clientserverapi-client/src/commonMain/kotlin/net/folivo/trixnity/clientserverapi/client/MatrixClientServerApiClient.kt +++ b/trixnity-clientserverapi/trixnity-clientserverapi-client/src/commonMain/kotlin/net/folivo/trixnity/clientserverapi/client/MatrixClientServerApiClient.kt @@ -29,9 +29,6 @@ interface MatrixClientServerApiClient : AutoCloseable { val device: DeviceApiClient val push: PushApiClient - @OptIn(MSC4140::class) - val delayedEvents: DelayedEventsClient - val eventContentSerializerMappings: EventContentSerializerMappings val json: Json @@ -106,9 +103,6 @@ class MatrixClientServerApiClientImpl( override val device = DeviceApiClientImpl(baseClient) override val push = PushApiClientImpl(baseClient) - @OptIn(MSC4140::class) - override val delayedEvents = DelayedEventsClientImpl(baseClient) - override fun close() { coroutineScope.cancel() baseClient.close() diff --git a/trixnity-clientserverapi/trixnity-clientserverapi-client/src/commonMain/kotlin/net/folivo/trixnity/clientserverapi/client/RoomApiClient.kt b/trixnity-clientserverapi/trixnity-clientserverapi-client/src/commonMain/kotlin/net/folivo/trixnity/clientserverapi/client/RoomApiClient.kt index e0b7173dc..8c7b8921f 100644 --- a/trixnity-clientserverapi/trixnity-clientserverapi-client/src/commonMain/kotlin/net/folivo/trixnity/clientserverapi/client/RoomApiClient.kt +++ b/trixnity-clientserverapi/trixnity-clientserverapi-client/src/commonMain/kotlin/net/folivo/trixnity/clientserverapi/client/RoomApiClient.kt @@ -1,5 +1,9 @@ package net.folivo.trixnity.clientserverapi.client +import net.folivo.trixnity.clientserverapi.model.delayedevents.CancelDelayedEvent +import net.folivo.trixnity.clientserverapi.model.delayedevents.GetDelayedEvents +import net.folivo.trixnity.clientserverapi.model.delayedevents.RestartDelayedEvent +import net.folivo.trixnity.clientserverapi.model.delayedevents.SendDelayedEvent import net.folivo.trixnity.clientserverapi.model.rooms.* import net.folivo.trixnity.core.MSC4140 import net.folivo.trixnity.core.model.OpaqueId @@ -9,6 +13,7 @@ import net.folivo.trixnity.core.model.RoomId import net.folivo.trixnity.core.model.UserId import net.folivo.trixnity.core.model.events.ClientEvent.RoomEvent import net.folivo.trixnity.core.model.events.ClientEvent.RoomEvent.StateEvent +import net.folivo.trixnity.core.model.events.DelayedEventStatus import net.folivo.trixnity.core.model.events.InitialStateEvent import net.folivo.trixnity.core.model.events.MessageEventContent import net.folivo.trixnity.core.model.events.RoomAccountDataEventContent @@ -148,18 +153,6 @@ interface RoomApiClient { asUserId: UserId? = null ): Result - /** - * @see [SendDelayedStateEvent] - */ - @MSC4140 - suspend fun sendDelayedStateEvent( - delayMs: Long, // TODO: must be > 0, new type? - roomId: RoomId, - eventContent: StateEventContent, - stateKey: String = "", - asUserId: UserId? = null - ): Result - /** * @see [SendMessageEvent] */ @@ -170,18 +163,6 @@ interface RoomApiClient { asUserId: UserId? = null ): Result - /** - * @see [SendDelayedMessageEvent] - */ - @MSC4140 - suspend fun sendDelayedMessageEvent( - delayMs: Long, // TODO: must be > 0, new type? - roomId: RoomId, - eventContent: MessageEventContent, - txnId: String = Random.nextString(22), - asUserId: UserId? = null - ): Result - /** * @see [RedactEvent] */ @@ -531,6 +512,58 @@ interface RoomApiClient { timestamp: Long, dir: TimestampToEvent.Direction = TimestampToEvent.Direction.FORWARDS, ): Result + + /** + * @see [SendDelayedStateEvent] + */ + @MSC4140 + suspend fun sendDelayedStateEvent( + delayMs: Long, // TODO: must be > 0, new type? + roomId: RoomId, + eventContent: StateEventContent, + stateKey: String = "", + asUserId: UserId? = null + ): Result + + /** + * @see [SendDelayedMessageEvent] + */ + @MSC4140 + suspend fun sendDelayedMessageEvent( + delayMs: Long, // TODO: must be > 0, new type? + roomId: RoomId, + eventContent: MessageEventContent, + txnId: String = Random.nextString(22), + asUserId: UserId? = null + ): Result + + /** + * @see [GetDelayedEvents] + */ + @MSC4140 + suspend fun getDelayedEvents( + status: DelayedEventStatus? = null, + delayIds: List? = null, + from: String? = null, + ): Result + + /** + * @see [SendDelayedEvent] + */ + @MSC4140 + suspend fun sendDelayedEvent(delayId: OpaqueId): Result + + /** + * @see [CancelDelayedEvent] + */ + @MSC4140 + suspend fun cancelDelayedEvent(delayId: OpaqueId): Result + + /** + * @see [RestartDelayedEvent] + */ + @MSC4140 + suspend fun restartDelayedEvent(delayId: OpaqueId): Result } class RoomApiClientImpl( @@ -668,19 +701,6 @@ class RoomApiClientImpl( .mapCatching { it.eventId } } - @MSC4140 - override suspend fun sendDelayedStateEvent( - delayMs: Long, - roomId: RoomId, - eventContent: StateEventContent, - stateKey: String, - asUserId: UserId? - ): Result { - val eventType = contentMappings.state.contentType(eventContent) - return baseClient.request(SendDelayedStateEvent(roomId, eventType, delayMs, stateKey, asUserId), eventContent) - .mapCatching { it.delayId } - } - override suspend fun sendMessageEvent( roomId: RoomId, eventContent: MessageEventContent, @@ -692,19 +712,6 @@ class RoomApiClientImpl( .mapCatching { it.eventId } } - @MSC4140 - override suspend fun sendDelayedMessageEvent( - delayMs: Long, - roomId: RoomId, - eventContent: MessageEventContent, - txnId: String, - asUserId: UserId? - ): Result { - val eventType = contentMappings.message.contentType(eventContent) - return baseClient.request(SendDelayedMessageEvent(roomId, eventType, txnId, delayMs, asUserId), eventContent) - .mapCatching { it.delayId } - } - override suspend fun redactEvent( roomId: RoomId, eventId: EventId, @@ -1024,6 +1031,52 @@ class RoomApiClientImpl( dir: TimestampToEvent.Direction ): Result = baseClient.request(TimestampToEvent(roomId, timestamp, dir)) + + @MSC4140 + override suspend fun sendDelayedStateEvent( + delayMs: Long, + roomId: RoomId, + eventContent: StateEventContent, + stateKey: String, + asUserId: UserId? + ): Result { + val eventType = contentMappings.state.contentType(eventContent) + return baseClient.request(SendDelayedStateEvent(roomId, eventType, delayMs, stateKey, asUserId), eventContent) + .mapCatching { it.delayId } + } + + @MSC4140 + override suspend fun sendDelayedMessageEvent( + delayMs: Long, + roomId: RoomId, + eventContent: MessageEventContent, + txnId: String, + asUserId: UserId? + ): Result { + val eventType = contentMappings.message.contentType(eventContent) + return baseClient.request(SendDelayedMessageEvent(roomId, eventType, txnId, delayMs, asUserId), eventContent) + .mapCatching { it.delayId } + } + + @MSC4140 + override suspend fun getDelayedEvents( + status: DelayedEventStatus?, + delayIds: List?, + from: String?, + ): Result = + baseClient.request(GetDelayedEvents(status, delayIds, from)) + + @MSC4140 + override suspend fun sendDelayedEvent(delayId: OpaqueId): Result = + baseClient.request(SendDelayedEvent(delayId), SendDelayedEvent.Request()) + + @MSC4140 + override suspend fun cancelDelayedEvent(delayId: OpaqueId): Result = + baseClient.request(CancelDelayedEvent(delayId), CancelDelayedEvent.Request()) + + @MSC4140 + override suspend fun restartDelayedEvent(delayId: OpaqueId): Result = + baseClient.request(RestartDelayedEvent(delayId), RestartDelayedEvent.Request()) } /** diff --git a/trixnity-clientserverapi/trixnity-clientserverapi-client/src/commonTest/kotlin/net/folivo/trixnity/clientserverapi/client/DelayedEventsClientTest.kt b/trixnity-clientserverapi/trixnity-clientserverapi-client/src/commonTest/kotlin/net/folivo/trixnity/clientserverapi/client/DelayedEventsClientTest.kt deleted file mode 100644 index 22424e8e5..000000000 --- a/trixnity-clientserverapi/trixnity-clientserverapi-client/src/commonTest/kotlin/net/folivo/trixnity/clientserverapi/client/DelayedEventsClientTest.kt +++ /dev/null @@ -1,153 +0,0 @@ -package net.folivo.trixnity.clientserverapi.client - -import io.kotest.matchers.shouldBe -import io.ktor.client.engine.mock.respond -import io.ktor.client.engine.mock.toByteArray -import io.ktor.http.ContentType -import io.ktor.http.HttpHeaders -import io.ktor.http.HttpMethod -import io.ktor.http.HttpStatusCode -import io.ktor.http.Url -import io.ktor.http.fullPath -import io.ktor.http.headersOf -import kotlinx.coroutines.test.runTest -import net.folivo.trixnity.core.model.events.FinalizedDelayedEvent -import net.folivo.trixnity.clientserverapi.model.delayedevents.GetDelayedEvents -import net.folivo.trixnity.core.model.events.ScheduledDelayedEvent.ScheduledDelayedMessageEvent -import net.folivo.trixnity.core.model.events.ScheduledDelayedEvent.ScheduledDelayedStateEvent -import net.folivo.trixnity.core.MSC4140 -import net.folivo.trixnity.core.model.OpaqueId -import net.folivo.trixnity.core.model.EventId -import net.folivo.trixnity.core.model.RoomId -import net.folivo.trixnity.core.model.events.m.room.NameEventContent -import net.folivo.trixnity.core.model.events.m.room.RoomMessageEventContent -import net.folivo.trixnity.core.serialization.createMatrixEventJson -import net.folivo.trixnity.test.utils.TrixnityBaseTest -import net.folivo.trixnity.testutils.scopedMockEngine -import kotlin.test.Test -import kotlin.test.assertEquals - -@OptIn(MSC4140::class) -class DelayedEventsClientTest : TrixnityBaseTest() { - - private val json = createMatrixEventJson() - - @Test - fun shouldGetDelayedEvents() = runTest { - val response = GetDelayedEvents.Response( - scheduled = listOf( - ScheduledDelayedMessageEvent( - delayId = OpaqueId("d1"), - roomId = RoomId("room:server"), - delayMs = 1000, - runningSince = 123456789, - content = RoomMessageEventContent.Location( - body = "Somewhere", - geoUri = "geo:0,0?q=Somewhere" - ) - ) - ), - finalised = listOf( - FinalizedDelayedEvent( - delayedEvent = ScheduledDelayedStateEvent( - delayId = OpaqueId("d2"), - roomId = RoomId("room:server"), - stateKey = "", - delayMs = 1000, - runningSince = 123456789, - content = NameEventContent( - name = "This is another delayed message." - ), - ), - outcome = FinalizedDelayedEvent.Outcome.SEND, - reason = FinalizedDelayedEvent.Reason.DELAY, - eventId = EventId("$123"), - originServerTs = 123456789, - ) - ), - nextBatch = "foo1234", - ) - val matrixRestClient = MatrixClientServerApiClientImpl( - baseUrl = Url("https://matrix.host"), - httpClientEngine = scopedMockEngine { - addHandler { request -> - assertEquals( - "/_matrix/client/unstable/org.matrix.msc4140/delayed_events", - request.url.fullPath) - assertEquals(HttpMethod.Get, request.method) - respond( - json.encodeToString(response), - HttpStatusCode.OK, - headersOf(HttpHeaders.ContentType, ContentType.Application.Json.toString()) - ) - } - }) - val result = matrixRestClient.delayedEvents.getDelayedEvents().getOrThrow() - result.scheduled!!.first().delayId shouldBe OpaqueId("d1") - result.finalised!!.first().delayedEvent.delayId shouldBe OpaqueId("d2") - } - - @Test - fun shouldSendDelayedEvent() = runTest { - val matrixRestClient = MatrixClientServerApiClientImpl( - baseUrl = Url("https://matrix.host"), - httpClientEngine = scopedMockEngine { - addHandler { request -> - assertEquals( - "/_matrix/client/unstable/org.matrix.msc4140/delayed_events/d1", - request.url.fullPath - ) - assertEquals(HttpMethod.Post, request.method) - assertEquals("{\"action\":\"send\"}", request.body.toByteArray().decodeToString()) - respond( - "{}", - HttpStatusCode.OK, - headersOf(HttpHeaders.ContentType, ContentType.Application.Json.toString()) - ) - } - }) - matrixRestClient.delayedEvents.sendDelayedEvent(OpaqueId("d1")).getOrThrow() - } - - @Test - fun shouldCancelDelayedEvent() = runTest { - val matrixRestClient = MatrixClientServerApiClientImpl( - baseUrl = Url("https://matrix.host"), - httpClientEngine = scopedMockEngine { - addHandler { request -> - assertEquals( - "/_matrix/client/unstable/org.matrix.msc4140/delayed_events/d1", - request.url.fullPath) - assertEquals(HttpMethod.Post, request.method) - assertEquals("{\"action\":\"cancel\"}", request.body.toByteArray().decodeToString()) - respond( - "{}", - HttpStatusCode.OK, - headersOf(HttpHeaders.ContentType, ContentType.Application.Json.toString()) - ) - } - }) - matrixRestClient.delayedEvents.cancelDelayedEvent(OpaqueId("d1")).getOrThrow() - } - - @Test - fun shouldRestartDelayedEvent() = runTest { - val matrixRestClient = MatrixClientServerApiClientImpl( - baseUrl = Url("https://matrix.host"), - httpClientEngine = scopedMockEngine { - addHandler { request -> - assertEquals( - "/_matrix/client/unstable/org.matrix.msc4140/delayed_events/d1", - request.url.fullPath) - assertEquals(HttpMethod.Post, request.method) - assertEquals("{\"action\":\"restart\"}", request.body.toByteArray().decodeToString()) - respond( - "{}", - HttpStatusCode.OK, - headersOf(HttpHeaders.ContentType, ContentType.Application.Json.toString()) - ) - } - }) - matrixRestClient.delayedEvents.restartDelayedEvent(OpaqueId("d1")).getOrThrow() - } -} diff --git a/trixnity-clientserverapi/trixnity-clientserverapi-client/src/commonTest/kotlin/net/folivo/trixnity/clientserverapi/client/RoomsApiClientTest.kt b/trixnity-clientserverapi/trixnity-clientserverapi-client/src/commonTest/kotlin/net/folivo/trixnity/clientserverapi/client/RoomsApiClientTest.kt index 70eb512d0..ed0ce97c1 100644 --- a/trixnity-clientserverapi/trixnity-clientserverapi-client/src/commonTest/kotlin/net/folivo/trixnity/clientserverapi/client/RoomsApiClientTest.kt +++ b/trixnity-clientserverapi/trixnity-clientserverapi-client/src/commonTest/kotlin/net/folivo/trixnity/clientserverapi/client/RoomsApiClientTest.kt @@ -7,6 +7,7 @@ import io.ktor.http.ContentType.* import kotlinx.coroutines.test.runTest import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.builtins.ListSerializer +import net.folivo.trixnity.clientserverapi.model.delayedevents.GetDelayedEvents import net.folivo.trixnity.clientserverapi.model.rooms.* import net.folivo.trixnity.core.MSC4140 import net.folivo.trixnity.core.model.OpaqueId @@ -18,7 +19,10 @@ import net.folivo.trixnity.core.model.events.ClientEvent.RoomEvent.MessageEvent import net.folivo.trixnity.core.model.events.ClientEvent.RoomEvent.StateEvent import net.folivo.trixnity.core.model.events.ClientEvent.StrippedStateEvent import net.folivo.trixnity.core.model.events.Event +import net.folivo.trixnity.core.model.events.FinalizedDelayedEvent import net.folivo.trixnity.core.model.events.MessageEventContent +import net.folivo.trixnity.core.model.events.ScheduledDelayedEvent.ScheduledDelayedMessageEvent +import net.folivo.trixnity.core.model.events.ScheduledDelayedEvent.ScheduledDelayedStateEvent import net.folivo.trixnity.core.model.events.StateEventContent import net.folivo.trixnity.core.model.events.UnsignedRoomEventData import net.folivo.trixnity.core.model.events.UnsignedRoomEventData.UnsignedStateEventData @@ -2137,4 +2141,127 @@ class RoomsApiClientTest : TrixnityBaseTest() { originTimestamp = 1432735824653, ) } + + @Test + @OptIn(MSC4140::class) + fun shouldGetDelayedEvents() = runTest { + val response = GetDelayedEvents.Response( + scheduled = listOf( + ScheduledDelayedMessageEvent( + delayId = OpaqueId("d1"), + roomId = RoomId("room:server"), + delayMs = 1000, + runningSince = 123456789, + content = RoomMessageEventContent.Location( + body = "Somewhere", + geoUri = "geo:0,0?q=Somewhere" + ) + ) + ), + finalised = listOf( + FinalizedDelayedEvent( + delayedEvent = ScheduledDelayedStateEvent( + delayId = OpaqueId("d2"), + roomId = RoomId("room:server"), + stateKey = "", + delayMs = 1000, + runningSince = 123456789, + content = NameEventContent( + name = "This is another delayed message." + ), + ), + outcome = FinalizedDelayedEvent.Outcome.SEND, + reason = FinalizedDelayedEvent.Reason.DELAY, + eventId = EventId("$123"), + originServerTs = 123456789, + ) + ), + nextBatch = "foo1234", + ) + val matrixRestClient = MatrixClientServerApiClientImpl( + baseUrl = Url("https://matrix.host"), + httpClientEngine = scopedMockEngine { + addHandler { request -> + assertEquals( + "/_matrix/client/unstable/org.matrix.msc4140/delayed_events", + request.url.fullPath) + assertEquals(HttpMethod.Get, request.method) + respond( + json.encodeToString(response), + HttpStatusCode.OK, + headersOf(HttpHeaders.ContentType, ContentType.Application.Json.toString()) + ) + } + }) + val result = matrixRestClient.room.getDelayedEvents().getOrThrow() + result.scheduled!!.first().delayId shouldBe OpaqueId("d1") + result.finalised!!.first().delayedEvent.delayId shouldBe OpaqueId("d2") + } + + @Test + @OptIn(MSC4140::class) + fun shouldSendDelayedEvent() = runTest { + val matrixRestClient = MatrixClientServerApiClientImpl( + baseUrl = Url("https://matrix.host"), + httpClientEngine = scopedMockEngine { + addHandler { request -> + assertEquals( + "/_matrix/client/unstable/org.matrix.msc4140/delayed_events/d1", + request.url.fullPath + ) + assertEquals(HttpMethod.Post, request.method) + assertEquals("{\"action\":\"send\"}", request.body.toByteArray().decodeToString()) + respond( + "{}", + HttpStatusCode.OK, + headersOf(HttpHeaders.ContentType, ContentType.Application.Json.toString()) + ) + } + }) + matrixRestClient.room.sendDelayedEvent(OpaqueId("d1")).getOrThrow() + } + + @Test + @OptIn(MSC4140::class) + fun shouldCancelDelayedEvent() = runTest { + val matrixRestClient = MatrixClientServerApiClientImpl( + baseUrl = Url("https://matrix.host"), + httpClientEngine = scopedMockEngine { + addHandler { request -> + assertEquals( + "/_matrix/client/unstable/org.matrix.msc4140/delayed_events/d1", + request.url.fullPath) + assertEquals(HttpMethod.Post, request.method) + assertEquals("{\"action\":\"cancel\"}", request.body.toByteArray().decodeToString()) + respond( + "{}", + HttpStatusCode.OK, + headersOf(HttpHeaders.ContentType, ContentType.Application.Json.toString()) + ) + } + }) + matrixRestClient.room.cancelDelayedEvent(OpaqueId("d1")).getOrThrow() + } + + @Test + @OptIn(MSC4140::class) + fun shouldRestartDelayedEvent() = runTest { + val matrixRestClient = MatrixClientServerApiClientImpl( + baseUrl = Url("https://matrix.host"), + httpClientEngine = scopedMockEngine { + addHandler { request -> + assertEquals( + "/_matrix/client/unstable/org.matrix.msc4140/delayed_events/d1", + request.url.fullPath) + assertEquals(HttpMethod.Post, request.method) + assertEquals("{\"action\":\"restart\"}", request.body.toByteArray().decodeToString()) + respond( + "{}", + HttpStatusCode.OK, + headersOf(HttpHeaders.ContentType, ContentType.Application.Json.toString()) + ) + } + }) + matrixRestClient.room.restartDelayedEvent(OpaqueId("d1")).getOrThrow() + } } -- GitLab From 03c8c5b493624fdbe2eacc3c458e5d802440cd77 Mon Sep 17 00:00:00 2001 From: Manuel Stahl Date: Thu, 23 Oct 2025 15:22:14 +0200 Subject: [PATCH 11/11] MSC4140: Add new error responses --- .../client/RoomsApiClientTest.kt | 38 +++++++++++++++++++ .../net/folivo/trixnity/core/ErrorResponse.kt | 15 +++++++- 2 files changed, 52 insertions(+), 1 deletion(-) diff --git a/trixnity-clientserverapi/trixnity-clientserverapi-client/src/commonTest/kotlin/net/folivo/trixnity/clientserverapi/client/RoomsApiClientTest.kt b/trixnity-clientserverapi/trixnity-clientserverapi-client/src/commonTest/kotlin/net/folivo/trixnity/clientserverapi/client/RoomsApiClientTest.kt index ed0ce97c1..a2abe45fd 100644 --- a/trixnity-clientserverapi/trixnity-clientserverapi-client/src/commonTest/kotlin/net/folivo/trixnity/clientserverapi/client/RoomsApiClientTest.kt +++ b/trixnity-clientserverapi/trixnity-clientserverapi-client/src/commonTest/kotlin/net/folivo/trixnity/clientserverapi/client/RoomsApiClientTest.kt @@ -1,6 +1,7 @@ package net.folivo.trixnity.clientserverapi.client import io.kotest.matchers.shouldBe +import io.kotest.matchers.types.shouldBeInstanceOf import io.ktor.client.engine.mock.* import io.ktor.http.* import io.ktor.http.ContentType.* @@ -9,7 +10,9 @@ import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.builtins.ListSerializer import net.folivo.trixnity.clientserverapi.model.delayedevents.GetDelayedEvents import net.folivo.trixnity.clientserverapi.model.rooms.* +import net.folivo.trixnity.core.ErrorResponse import net.folivo.trixnity.core.MSC4140 +import net.folivo.trixnity.core.MatrixServerException import net.folivo.trixnity.core.model.OpaqueId import net.folivo.trixnity.core.model.EventId import net.folivo.trixnity.core.model.RoomAliasId @@ -634,6 +637,41 @@ class RoomsApiClientTest : TrixnityBaseTest() { assertEquals(OpaqueId("delay"), result) } + @OptIn(MSC4140::class) + @Test + fun shouldReportMaxDelayExceededOnSendDelayedStateEvent() = runTest { + val response = """{ + "errcode": "M_MAX_DELAY_EXCEEDED", + "error": "The requested delay exceeds the allowed maximum.", + "max_delay": 86400000 + } + """ + val matrixRestClient = MatrixClientServerApiClientImpl( + baseUrl = Url("https://matrix.host"), + httpClientEngine = scopedMockEngine { + addHandler { request -> + respondError( + HttpStatusCode.BadRequest, + response, + headersOf(HttpHeaders.ContentType, Application.Json.toString()) + ) + } + }) + val eventContent = NameEventContent("name") + + val result = matrixRestClient.room.sendDelayedStateEvent( + roomId = RoomId("!room:server"), + eventContent = eventContent, + stateKey = "someStateKey", + delayMs = 123456, + ).exceptionOrNull() + result.shouldBeInstanceOf() + result.errorResponse.shouldBe(ErrorResponse.MaxDelayExceeded( + "The requested delay exceeds the allowed maximum.", + 86400000 + )) + } + @Test fun shouldSendRoomEvent() = runTest { val response = SendEventResponse(EventId("event")) diff --git a/trixnity-core/src/commonMain/kotlin/net/folivo/trixnity/core/ErrorResponse.kt b/trixnity-core/src/commonMain/kotlin/net/folivo/trixnity/core/ErrorResponse.kt index bc82c2df9..1118abbbb 100644 --- a/trixnity-core/src/commonMain/kotlin/net/folivo/trixnity/core/ErrorResponse.kt +++ b/trixnity-core/src/commonMain/kotlin/net/folivo/trixnity/core/ErrorResponse.kt @@ -312,6 +312,19 @@ sealed interface ErrorResponse { @SerialName("M_DUPLICATE_ANNOTATION") data class DuplicateAnnotation(override val error: String) : ErrorResponse + @MSC4140 + @Serializable + @SerialName("M_MAX_DELAY_EXCEEDED") + data class MaxDelayExceeded( + override val error: String, + @SerialName("max_delay") val maxDelay: Long, + ) : ErrorResponse + + @MSC4140 + @Serializable + @SerialName("M_MAX_DELAYED_EVENTS_EXCEEDED") + data class MaxDelayedEventsExceeded(override val error: String) : ErrorResponse + /** * All ErrorResponses, that we cannot map to a subtype of ErrorResponse. */ @@ -365,4 +378,4 @@ fun Json.decodeErrorResponse(body: JsonObject): ErrorResponse = ErrorResponse.BadJson( "response could not be parsed to ErrorResponse (body=$body)", ) - } \ No newline at end of file + } -- GitLab