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 f584c1b6128fde06ac718603d07887afe1cd2424..0cc6981eb3923d0155928bc4a86e12cd74d0fdb7 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 @@ -112,4 +113,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 7b4039f5a31d9c5c1d5d9af4c7ec460819ab8df8..8c7b8921fada43db9b452d03ba009c38b0950fb7 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,12 +1,19 @@ 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 import net.folivo.trixnity.core.model.EventId import net.folivo.trixnity.core.model.RoomAliasId 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 @@ -505,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( @@ -972,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/RoomsApiClientTest.kt b/trixnity-clientserverapi/trixnity-clientserverapi-client/src/commonTest/kotlin/net/folivo/trixnity/clientserverapi/client/RoomsApiClientTest.kt index 445ec42da576fc07096e739d04eea5881decb02f..a2abe45fd2231f4e7916a50f112f784448accae2 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,13 +1,19 @@ 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.* 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.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 import net.folivo.trixnity.core.model.RoomId @@ -16,7 +22,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 @@ -596,6 +605,73 @@ class RoomsApiClientTest : TrixnityBaseTest() { } } + @OptIn(MSC4140::class) + @Test + fun shouldSendDelayedStateEvent() = runTest { + val response = SendDelayedEventResponse(OpaqueId("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(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")) @@ -654,6 +730,40 @@ class RoomsApiClientTest : TrixnityBaseTest() { } } + @OptIn(MSC4140::class) + @Test + fun shouldSendDelayedRoomEvent() = runTest { + val response = SendDelayedEventResponse(OpaqueId("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(OpaqueId("delay"), result) + } + @Test fun shouldSendRedactEvent() = runTest { val response = SendEventResponse(EventId("event")) @@ -2069,4 +2179,127 @@ class RoomsApiClientTest : TrixnityBaseTest() { originTimestamp = 1432735824653, ) } -} \ No newline at end of file + + @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() + } +} 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 new file mode 100644 index 0000000000000000000000000000000000000000..64fa583099b13777ac6d8eef40951d74e1ba1034 --- /dev/null +++ b/trixnity-clientserverapi/trixnity-clientserverapi-model/src/commonMain/kotlin/net/folivo/trixnity/clientserverapi/model/delayedevents/CancelDelayedEvent.kt @@ -0,0 +1,27 @@ +package net.folivo.trixnity.clientserverapi.model.delayedevents + +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.OpaqueId + +/** + * @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: OpaqueId, +) : 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/delayedevents/GetDelayedEvents.kt b/trixnity-clientserverapi/trixnity-clientserverapi-model/src/commonMain/kotlin/net/folivo/trixnity/clientserverapi/model/delayedevents/GetDelayedEvents.kt new file mode 100644 index 0000000000000000000000000000000000000000..998de8ff712bf8f08eaa00c8a66db84d0b49ba52 --- /dev/null +++ b/trixnity-clientserverapi/trixnity-clientserverapi-model/src/commonMain/kotlin/net/folivo/trixnity/clientserverapi/model/delayedevents/GetDelayedEvents.kt @@ -0,0 +1,36 @@ +package net.folivo.trixnity.clientserverapi.model.delayedevents + +import io.ktor.resources.Resource +import kotlinx.serialization.Contextual +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +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.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 + +/** + * @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 { + + @Serializable + data class Response( + @SerialName("scheduled") val scheduled: List<@Contextual ScheduledDelayedEvent<*>>? = 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/RestartDelayedEvent.kt b/trixnity-clientserverapi/trixnity-clientserverapi-model/src/commonMain/kotlin/net/folivo/trixnity/clientserverapi/model/delayedevents/RestartDelayedEvent.kt new file mode 100644 index 0000000000000000000000000000000000000000..0f8864c1334f958b1a93ebc6d39509c7bd2f5e98 --- /dev/null +++ b/trixnity-clientserverapi/trixnity-clientserverapi-model/src/commonMain/kotlin/net/folivo/trixnity/clientserverapi/model/delayedevents/RestartDelayedEvent.kt @@ -0,0 +1,27 @@ +package net.folivo.trixnity.clientserverapi.model.delayedevents + +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.OpaqueId + +/** + * @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: OpaqueId, +) : 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/delayedevents/SendDelayedEvent.kt b/trixnity-clientserverapi/trixnity-clientserverapi-model/src/commonMain/kotlin/net/folivo/trixnity/clientserverapi/model/delayedevents/SendDelayedEvent.kt new file mode 100644 index 0000000000000000000000000000000000000000..64d4dbfbc3e6b03a7365c6c5a71a040f6376a3ee --- /dev/null +++ b/trixnity-clientserverapi/trixnity-clientserverapi-model/src/commonMain/kotlin/net/folivo/trixnity/clientserverapi/model/delayedevents/SendDelayedEvent.kt @@ -0,0 +1,27 @@ +package net.folivo.trixnity.clientserverapi.model.delayedevents + +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.OpaqueId + +/** + * @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: OpaqueId, +) : 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 0000000000000000000000000000000000000000..a229d0844b2121e989dc179f1934844f3b7fd6c4 --- /dev/null +++ b/trixnity-clientserverapi/trixnity-clientserverapi-model/src/commonMain/kotlin/net/folivo/trixnity/clientserverapi/model/rooms/SendDelayedEventResponse.kt @@ -0,0 +1,12 @@ +package net.folivo.trixnity.clientserverapi.model.rooms + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import net.folivo.trixnity.core.MSC4140 +import net.folivo.trixnity.core.model.OpaqueId + +@MSC4140 +@Serializable +data class SendDelayedEventResponse( + @SerialName("delay_id") val delayId: OpaqueId, +) 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 0000000000000000000000000000000000000000..99a300554d418b2b8ddc8ecb7dca642192356c04 --- /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 0000000000000000000000000000000000000000..83cc98a98b58bcc47f4cb1a359a7adac45613a0d --- /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-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 b72306eb030e618a50053888cf4b5e5750ccef36..0892c81688fa095f5c07122a160fbc4644908d7b 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 0b8cdb1123719d14492244e7254205268dcf1fee..2f484f781ce555da4f54d1462e284e0663a518b9 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 dee4ebaed3799dc0c2b843d40213ffcdac989e7e..f01eca1f06564efb943e84503734d653a515f713 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.OpaqueId 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(OpaqueId("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(OpaqueId("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() 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 bc82c2df9f4e2d44ccc76fb0b900391a786c49f1..1118abbbbcbec0f8d4aa637676e79697cd2df40c 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 + } 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 7187d63bf3ddefa5d7fc1236c4aa59b32ef2d15a..fffcedc5d3dc25478caabc31f97cdd7df50ec22b 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/OpaqueId.kt b/trixnity-core/src/commonMain/kotlin/net/folivo/trixnity/core/model/OpaqueId.kt new file mode 100644 index 0000000000000000000000000000000000000000..3e8141ec6be4cac6a08f8d5faaf88ea7ed74e033 --- /dev/null +++ b/trixnity-core/src/commonMain/kotlin/net/folivo/trixnity/core/model/OpaqueId.kt @@ -0,0 +1,31 @@ +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 + +@Serializable(with = OpaqueIdSerializer::class) +data class OpaqueId(val full: String) { + companion object { + fun isValid(id: String): Boolean = id.length <= 255 && id.matches(MatrixIdRegex.opaqueId) + } + + val isValid by lazy { isValid(full) } + + override fun toString() = full +} + +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: 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 new file mode 100644 index 0000000000000000000000000000000000000000..a08e675ea7d874c494a9e4a3b56221e4e7589f6e --- /dev/null +++ b/trixnity-core/src/commonMain/kotlin/net/folivo/trixnity/core/model/events/DelayedEvent.kt @@ -0,0 +1,102 @@ +package net.folivo.trixnity.core.model.events + +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.OpaqueId +import net.folivo.trixnity.core.model.EventId +import net.folivo.trixnity.core.model.RoomId + +@MSC4140 +@Serializable +enum class DelayedEventStatus { + @SerialName("scheduled") + SCHEDULED, + + @SerialName("finalised") + FINALIZED, +} + +/** + * 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. + * @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: Event { + val delayId: OpaqueId + val roomId: RoomId + val delayMs: Long + val runningSince: Long + + /** + * @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: OpaqueId, + @SerialName("room_id") override val roomId: RoomId, + @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, + ): ScheduledDelayedEvent + + + @Serializable + data class ScheduledDelayedMessageEvent( + @SerialName("delay_id") override val delayId: OpaqueId, + @SerialName("room_id") override val roomId: RoomId, + @SerialName("delay") override val delayMs: Long, + @SerialName("running_since") override val runningSince: Long, + @SerialName("content") override val content: C, + ): ScheduledDelayedEvent +} + +/** + * @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: E, + @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, + } +} 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 0000000000000000000000000000000000000000..c9515aaf023dc9b8e43c6146c3a7bf260ad7a79a --- /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 0000000000000000000000000000000000000000..9e560614b9554d205ac2f1c7b016ec3ce0eea15a --- /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 0000000000000000000000000000000000000000..96d71ed5c5f92b697831c7030d77556f183ca107 --- /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 1293611a41fab564588b5384d450d37128f1eee8..eca30bca046a6ca8bc1b6d6ed33bd6960011c0c2 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 +} 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 cdbea6b0757d345dd9a6ad5874c586c809ce8510..afbad01934d682241b97f81c6f389cc7842c1aca 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] @@ -12,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 @@ -71,4 +74,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 +} 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 0000000000000000000000000000000000000000..08422ce8914081c4aae776417ac43b78d535fa12 --- /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) + } +}