Skip to content

Commit

Permalink
Add m.read.private receipts, as per [MSC2285](matrix-org/matrix-spe…
Browse files Browse the repository at this point in the history
…c-proposals#2285).

Make `m.fully_read` optional on `/read_markers`, as per [MSC2285](matrix-org/matrix-spec-proposals#2285).
Allow `m.fully_read` markers to be set from `/receipts`, as per [MSC2285](matrix-org/matrix-spec-proposals#2285).
  • Loading branch information
benkuly committed Nov 1, 2022
1 parent 98b3dd5 commit d02c263
Show file tree
Hide file tree
Showing 12 changed files with 129 additions and 137 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import net.folivo.trixnity.core.model.EventId
import net.folivo.trixnity.core.model.RoomId
import net.folivo.trixnity.core.model.UserId
import net.folivo.trixnity.core.model.events.Event
import net.folivo.trixnity.core.model.events.m.ReceiptEventContent
import net.folivo.trixnity.core.model.events.m.ReceiptType
import net.folivo.trixnity.core.model.events.m.room.MemberEventContent

@Serializable
Expand All @@ -14,5 +16,11 @@ data class RoomUser(
val userId: UserId,
val name: String,
val event: @Contextual Event<MemberEventContent>,
val lastReadMessage: EventId? = null,
)
val receipts: Map<ReceiptType, RoomUserReceipt> = mapOf(),
) {
@Serializable
data class RoomUserReceipt(
val eventId: EventId,
val receipt: ReceiptEventContent.Receipt
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.job
import mu.KotlinLogging
import net.folivo.trixnity.client.getRoomId
import net.folivo.trixnity.client.store.RoomUser
import net.folivo.trixnity.client.store.RoomUserStore
import net.folivo.trixnity.clientserverapi.client.MatrixClientServerApiClient
import net.folivo.trixnity.core.EventHandler
Expand Down Expand Up @@ -31,11 +32,13 @@ class ReceiptEventHandler(
log.debug { "set read receipts of room $roomId" }
receiptEvent.content.events.forEach { (eventId, receipts) ->
receipts
.filterIsInstance<ReceiptEventContent.Receipt.ReadReceipt>()
.forEach { receipt ->
receipt.read.keys.forEach { userId ->
.forEach { (receiptType, receipts) ->
receipts.forEach { (userId, receipt) ->
roomUserStore.update(userId, roomId) { oldRoomUser ->
oldRoomUser?.copy(lastReadMessage = eventId)
oldRoomUser?.copy(
receipts = oldRoomUser.receipts +
(receiptType to RoomUser.RoomUserReceipt(eventId, receipt))
)
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,6 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.first
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.JsonPrimitive
import net.folivo.trixnity.client.getInMemoryRoomUserStore
import net.folivo.trixnity.client.mockMatrixClientServerApiClient
import net.folivo.trixnity.client.store.RoomUser
Expand All @@ -19,7 +17,7 @@ import net.folivo.trixnity.core.model.UserId
import net.folivo.trixnity.core.model.events.Event
import net.folivo.trixnity.core.model.events.m.ReceiptEventContent
import net.folivo.trixnity.core.model.events.m.ReceiptEventContent.Receipt
import net.folivo.trixnity.core.model.events.m.ReceiptEventContent.Receipt.ReadReceipt
import net.folivo.trixnity.core.model.events.m.ReceiptType
import net.folivo.trixnity.core.model.events.m.room.MemberEventContent
import net.folivo.trixnity.core.model.events.m.room.Membership
import net.folivo.trixnity.core.serialization.createMatrixEventJson
Expand Down Expand Up @@ -47,7 +45,11 @@ class ReceiptEventHandlerTest : ShouldSpec({
scope.cancel()
}

fun roomUser(roomId: RoomId, userId: UserId, lastReadMessage: EventId? = null): RoomUser {
fun roomUser(
roomId: RoomId,
userId: UserId,
receipts: Map<ReceiptType, RoomUser.RoomUserReceipt> = mapOf()
): RoomUser {
return RoomUser(
roomId,
userId,
Expand All @@ -60,7 +62,7 @@ class ReceiptEventHandlerTest : ShouldSpec({
0L,
stateKey = ""
),
lastReadMessage = lastReadMessage,
receipts = receipts,
)
}

Expand All @@ -71,11 +73,9 @@ class ReceiptEventHandlerTest : ShouldSpec({
val event = Event.EphemeralEvent(
ReceiptEventContent(
events = mapOf(
EventId("eventId") to setOf(
ReadReceipt(
read = mapOf(
UserId("unknownUser", "localhost") to ReadReceipt.ReadEvent(0L)
)
EventId("eventId") to mapOf(
ReceiptType.Read to mapOf(
UserId("unknownUser", "localhost") to Receipt(0L)
)
)
)
Expand All @@ -93,10 +93,9 @@ class ReceiptEventHandlerTest : ShouldSpec({
val event = Event.EphemeralEvent(
ReceiptEventContent(
events = mapOf(
EventId("eventId") to setOf(
Receipt.Unknown(
type = "awesome",
raw = JsonObject(mapOf("dino" to JsonPrimitive("unicorn"))),
EventId("eventId") to mapOf(
ReceiptType.Unknown("awesome") to mapOf(
UserId("unknownUser", "localhost") to Receipt(0L)
)
)
)
Expand All @@ -115,11 +114,9 @@ class ReceiptEventHandlerTest : ShouldSpec({
val event = Event.EphemeralEvent(
ReceiptEventContent(
events = mapOf(
eventId to setOf(
ReadReceipt(
read = mapOf(
alice to ReadReceipt.ReadEvent(0L)
)
eventId to mapOf(
ReceiptType.Read to mapOf(
alice to Receipt(0L)
)
)
)
Expand All @@ -128,61 +125,37 @@ class ReceiptEventHandlerTest : ShouldSpec({
)
cut.setReadReceipts(event)

roomUserStore.get(alice, room).first()?.lastReadMessage shouldBe eventId
roomUserStore.get(alice, room).first()?.receipts?.get(ReceiptType.Read) shouldBe
RoomUser.RoomUserReceipt(eventId, Receipt(0L))
}

should("replace the last read message of a user when a new last message is received") {
val existingEventId = EventId("existingEvent")
val existingRoomUser = roomUser(room, alice, lastReadMessage = existingEventId)
roomUserStore.update(alice, room) { existingRoomUser }
val eventId = EventId("eventId")
val event = Event.EphemeralEvent(
ReceiptEventContent(
events = mapOf(
eventId to setOf(
ReadReceipt(
read = mapOf(
alice to ReadReceipt.ReadEvent(0L)
)
)
)
val existingRoomUser = roomUser(
room, alice, receipts = mapOf(
ReceiptType.Read to RoomUser.RoomUserReceipt(
existingEventId,
Receipt(0)
)
),
roomId = room,
)
)
cut.setReadReceipts(event)
roomUserStore.get(alice, room).first()?.lastReadMessage shouldBe eventId
}

should("set the last read message even when unknown receipt types are encountered") {
val eventId = EventId("eventId")
val existingRoomUser = roomUser(room, alice)
roomUserStore.update(alice, room) { existingRoomUser }
val eventId = EventId("eventId")
val event = Event.EphemeralEvent(
ReceiptEventContent(
events = mapOf(
eventId to setOf(
Receipt.Unknown(
type = "awesome",
raw = JsonObject(mapOf("dino" to JsonPrimitive("unicorn")))
),
ReadReceipt(
read = mapOf(
alice to ReadReceipt.ReadEvent(0L)
)
),
Receipt.Unknown(
type = "awesome",
raw = JsonObject(mapOf("unicorn" to JsonPrimitive("dino")))
),
eventId to mapOf(
ReceiptType.Read to mapOf(
alice to Receipt(3)
)
)
)
),
roomId = room,
)
cut.setReadReceipts(event)

roomUserStore.get(alice, room).first()?.lastReadMessage shouldBe eventId
roomUserStore.get(alice, room).first()?.receipts?.get(ReceiptType.Read) shouldBe
RoomUser.RoomUserReceipt(eventId, Receipt(3L))
}
}
})
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import net.folivo.trixnity.core.model.RoomId
import net.folivo.trixnity.core.model.UserId
import net.folivo.trixnity.core.model.events.*
import net.folivo.trixnity.core.model.events.Event.StateEvent
import net.folivo.trixnity.core.model.events.m.ReceiptType
import net.folivo.trixnity.core.model.events.m.TagEventContent
import net.folivo.trixnity.core.model.events.m.room.CreateEventContent
import net.folivo.trixnity.core.model.events.m.room.MemberEventContent
Expand Down Expand Up @@ -309,7 +310,7 @@ interface RoomsApiClient {
suspend fun setReceipt(
roomId: RoomId,
eventId: EventId,
receiptType: SetReceipt.ReceiptType = SetReceipt.ReceiptType.READ,
receiptType: ReceiptType = ReceiptType.Read,
asUserId: UserId? = null,
): Result<Unit>

Expand Down Expand Up @@ -751,7 +752,7 @@ class RoomsApiClientImpl(
override suspend fun setReceipt(
roomId: RoomId,
eventId: EventId,
receiptType: SetReceipt.ReceiptType,
receiptType: ReceiptType,
asUserId: UserId?,
): Result<Unit> =
httpClient.request(SetReceipt(roomId.e(), receiptType, eventId.e(), asUserId))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ data class GetHierarchy(
@SerialName("name") val name: String? = null,
@SerialName("num_joined_members") val joinedMembersCount: Long,
@SerialName("room_id") val roomId: RoomId,
@SerialName("room_type") val roomType: CreateEventContent.RoomType,
@SerialName("room_type") val roomType: CreateEventContent.RoomType? = null,
@SerialName("topic") val topic: String? = null,
@SerialName("world_readable") val worldReadable: Boolean,
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@ data class SetReadMarkers(
) : MatrixEndpoint<SetReadMarkers.Request, Unit> {
@Serializable
data class Request(
@SerialName("m.fully_read") val fullyRead: EventId,
@SerialName("m.fully_read") val fullyRead: EventId? = null,
@SerialName("m.read") val read: EventId? = null,
@SerialName("m.read.private") val privateRead: EventId? = null,
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import net.folivo.trixnity.core.MatrixEndpoint
import net.folivo.trixnity.core.model.EventId
import net.folivo.trixnity.core.model.RoomId
import net.folivo.trixnity.core.model.UserId
import net.folivo.trixnity.core.model.events.m.ReceiptType

/**
* @see <a href="https://spec.matrix.org/v1.3/client-server-api/#post_matrixclientv3roomsroomidreceiptreceipttypeeventid">matrix spec</a>
Expand All @@ -21,10 +22,4 @@ data class SetReceipt(
@SerialName("receiptType") val receiptType: ReceiptType,
@SerialName("eventId") val eventId: EventId,
@SerialName("user_id") val asUserId: UserId? = null
) : MatrixEndpoint<Unit, Unit> {
@Serializable
enum class ReceiptType {
@SerialName("m.read")
READ
}
}
) : MatrixEndpoint<Unit, Unit>
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import net.folivo.trixnity.core.model.RoomId
import net.folivo.trixnity.core.model.UserId
import net.folivo.trixnity.core.model.events.*
import net.folivo.trixnity.core.model.events.m.FullyReadEventContent
import net.folivo.trixnity.core.model.events.m.ReceiptType
import net.folivo.trixnity.core.model.events.m.TagEventContent
import net.folivo.trixnity.core.model.events.m.room.*
import net.folivo.trixnity.core.model.events.m.space.ChildEventContent
Expand Down Expand Up @@ -1143,7 +1144,7 @@ class RoomsRoutesTest : TestsWithMocks() {
verifyWithSuspend {
handlerMock.setReceipt(assert {
it.endpoint.roomId shouldBe RoomId("!room:server")
it.endpoint.receiptType shouldBe SetReceipt.ReceiptType.READ
it.endpoint.receiptType shouldBe ReceiptType.Read
it.endpoint.eventId shouldBe EventId("\$event")
})
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,58 +11,29 @@ import kotlinx.serialization.json.*
import net.folivo.trixnity.core.model.EventId
import net.folivo.trixnity.core.model.UserId
import net.folivo.trixnity.core.model.events.EphemeralEventContent
import net.folivo.trixnity.core.model.events.m.ReceiptEventContent.Receipt

/**
* @see <a href="https://spec.matrix.org/v1.3/client-server-api/#receipts">matrix spec</a>
*/
@Serializable(with = ReadEventsSerializer::class)
@Serializable(with = ReceiptEventContentSerializer::class)
data class ReceiptEventContent(
val events: Map<EventId, Set<Receipt>>
val events: Map<EventId, Map<ReceiptType, Map<UserId, Receipt>>>
) : EphemeralEventContent {

sealed interface Receipt {
data class ReadReceipt(
val read: Map<UserId, ReadEvent>
) : Receipt {
@Serializable
data class ReadEvent(@SerialName("ts") val timestamp: Long)
}

data class Unknown(
val raw: JsonElement,
val type: String
) : Receipt
}
@Serializable
data class Receipt(@SerialName("ts") val timestamp: Long)
}

object ReadEventsSerializer : KSerializer<ReceiptEventContent> {
override val descriptor: SerialDescriptor = buildClassSerialDescriptor("ReadEventsSerializer")
object ReceiptEventContentSerializer : KSerializer<ReceiptEventContent> {
override val descriptor: SerialDescriptor = buildClassSerialDescriptor("ReceiptEventContentSerializer")

override fun deserialize(decoder: Decoder): ReceiptEventContent {
require(decoder is JsonDecoder)
val jsonObject = decoder.decodeJsonElement().jsonObject
return ReceiptEventContent(jsonObject.entries.associate { (eventId, jsonObject) ->
EventId(eventId) to jsonObject.jsonObject.entries.map { (type, receipt) ->
when (type) {
"m.read" -> Receipt.ReadReceipt(decoder.json.decodeFromJsonElement(receipt))
else -> Receipt.Unknown(raw = receipt, type = type)
}
}.toSet()
})
return ReceiptEventContent(decoder.json.decodeFromJsonElement(decoder.decodeJsonElement()))
}

override fun serialize(encoder: Encoder, value: ReceiptEventContent) {
require(encoder is JsonEncoder)
val json = JsonObject(value.events.entries.associate { (eventId, receipts) ->
eventId.full to JsonObject(receipts.associate { receipt ->
when (receipt) {
is Receipt.ReadReceipt -> "m.read" to encoder.json.encodeToJsonElement(receipt.read)
is Receipt.Unknown -> receipt.type to receipt.raw
}
})
})
encoder.encodeJsonElement(json)
encoder.encodeJsonElement(encoder.json.encodeToJsonElement(value.events))
}

}
Loading

0 comments on commit d02c263

Please sign in to comment.