Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Spec MSC2285: Private read receipts #1216

Merged
merged 7 commits into from
Sep 12, 2022
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelogs/client_server/newsfragments/1216.feature.1
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add `m.read.private` receipts, as per [MSC2285](https://github.com/matrix-org/matrix-spec-proposals/pull/2285).
1 change: 1 addition & 0 deletions changelogs/client_server/newsfragments/1216.feature.2
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Make `m.fully_read` optional on `/read_markers`, as per [MSC2285](https://github.com/matrix-org/matrix-spec-proposals/pull/2285).
14 changes: 13 additions & 1 deletion content/client-server-api/modules/push.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,19 @@ determined by the push rules which apply to an event.

When the user updates their read receipt (either by using the API or by
sending an event), notifications prior to and including that event MUST
be marked as read.
be marked as read. Note that users can send both an `m.read` and
`m.read.private` receipt, both of which are capable of clearing notifications.

If the user has both `m.read` and `m.read.private` set in the room then
the receipt which is more recent/ahead must be used to determine where
the user has read up to. For example, given an ordered set of events A,
turt2live marked this conversation as resolved.
Show resolved Hide resolved
B, C, and D the `m.read` receipt could be at event C and `m.read.private`
at event A - the user is considered to have read up to event C. If the
`m.read.private` receipt is then updated to point to B or C, the user's
notification state doesn't change (the `m.read` receipt is still more
ahead), however if the `m.read.private` receipt were to be updated to
event D then the user has read up to D (the `m.read` receipt is now
behind the `m.read.private` receipt).

##### Push Rules

Expand Down
19 changes: 12 additions & 7 deletions content/client-server-api/modules/read_markers.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,21 +31,26 @@ The client cannot update fully read markers by directly modifying the
`m.fully_read` account data event. Instead, the client must make use of
the read markers API to change the values.

{{< changed-in v="1.4" >}} `m.read.private` receipts can now be sent from
`/read_markers`.

The read markers API can additionally update the user's read receipt
(`m.read`) location in the same operation as setting the fully read
marker location. This is because read receipts and read markers are
commonly updated at the same time, and therefore the client might wish
to save an extra HTTP call. Providing an `m.read` location performs the
same task as a request to `/receipt/m.read/$event:example.org`.
(`m.read` or `m.read.private`) location in the same operation as setting
the fully read marker location. This is because read receipts and read
markers are commonly updated at the same time, and therefore the client
might wish to save an extra HTTP call. Providing `m.read` and/or
`m.read.private` performs the same task as a request to
[`/receipt/{receiptType}/{eventId}`](#post_matrixclientv3roomsroomidreceiptreceipttypeeventid).

{{% http-api spec="client-server" api="read_markers" %}}

#### Server behaviour

The server MUST prevent clients from setting `m.fully_read` directly in
room account data. The server must additionally ensure that it treats
the presence of `m.read` in the `/read_markers` request the same as how
it would for a request to `/receipt/m.read/$event:example.org`.
the presence of `m.read` and `m.read.private` in the `/read_markers`
request the same as how it would for a request to
[`/receipt/{receiptType}/{eventId}`](#post_matrixclientv3roomsroomidreceiptreceipttypeeventid).

Upon updating the `m.fully_read` event due to a request to
`/read_markers`, the server MUST send the updated account data event
Expand Down
39 changes: 34 additions & 5 deletions content/client-server-api/modules/receipts.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,14 @@ type: module

### Receipts

{{< changed-in v="1.4" >}} Added private read receipts.

This module adds in support for receipts. These receipts are a form of
acknowledgement of an event. This module defines a single
acknowledgement: `m.read` which indicates that the user has read up to a
given event.
acknowledgement of an event. This module defines the `m.read` receipt
for indicating that the user has read up to a given event, and `m.read.private`
to achieve the same purpose without any other user being aware. Primarily,
`m.read.private` is meant to clear [notifications](#receiving-notifications)
turt2live marked this conversation as resolved.
Show resolved Hide resolved
without advertising read-up-to status to others.

Sending a receipt for each event can result in sending large amounts of
traffic to a homeserver. To prevent this from becoming a problem,
Expand Down Expand Up @@ -59,12 +63,36 @@ following HTTP APIs.

{{% http-api spec="client-server" api="receipts" %}}

##### Private read receipts

{{% added-in v="1.4" %}}

Some users would like to clear their [notification counts](#receiving-notifications),
turt2live marked this conversation as resolved.
Show resolved Hide resolved
but not give away the fact that they've read a particular message yet. To
achieve this, clients can send `m.read.private` receipts instead of `m.read`
to do exactly that: clear notifications and not broadcast the receipt to
other users.

Servers MUST NOT send the `m.read.private` receipt to any other user than the
one which originally sent it.

Between `m.read` and `m.read.private`, the receipt which is more "ahead" or
"recent" is used when determining the highest read-up-to mark. See the
[notifications](#receiving-notifications) section for more information on
how this affects notification counts.

If a client sends an `m.read` receipt which is "behind" the `m.read.private`
receipt, other users will see that change happen but the sending user will
not have their notification counts rewound to that point in time. While
uncommon, it is considered valid to have an `m.read` (public) receipt lag
several messages behind the `m.read.private` receipt, for example.

#### Server behaviour

For efficiency, receipts SHOULD be batched into one event per room
before delivering them to clients.

Receipts are sent across federation as EDUs with type `m.receipt`. The
Some receipts are sent across federation as EDUs with type `m.receipt`. The
format of the EDUs are:

```
Expand All @@ -80,7 +108,8 @@ format of the EDUs are:
```

These are always sent as deltas to previously sent receipts. Currently
only a single `<receipt_type>` should be used: `m.read`.
only a single `<receipt_type>` should be used: `m.read`. `m.read.private`
MUST NOT appear in this federated `m.receipt` EDU.

#### Security considerations

Expand Down
15 changes: 13 additions & 2 deletions data/api/client-server/read_markers.yaml
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
# Copyright 2018 New Vector Ltd
# Copyright 2022 The Matrix.org Foundation C.I.C.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -56,18 +57,28 @@ paths:
The event ID the read marker should be located at. The
event MUST belong to the room.
example: "$somewhere:example.org"
x-changedInMatrixVersion:
1.4: |
This property is no longer required.
"m.read":
type: string
description: |-
The event ID to set the read receipt location at. This is
equivalent to calling `/receipt/m.read/$elsewhere:example.org`
and is provided here to save that extra call.
example: "$elsewhere:example.org"
required: ['m.fully_read']
"m.read.private":
x-addedInMatrixVersion: "1.4"
type: string
description: |-
The event ID to set the *private* read receipt location at. This
equivalent to calling `/receipt/m.read.private/$elsewhere:example.org`
and is provided here to save that extra call.
example: "$elsewhere:example.org"
responses:
200:
description: |-
The read marker, and read receipt if provided, have been updated.
The read marker, and read receipt(s) if provided, have been updated.
schema:
type: object
properties: {}
Expand Down
6 changes: 5 additions & 1 deletion data/api/client-server/receipts.yaml
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
# Copyright 2016 OpenMarket Ltd
# Copyright 2022 The Matrix.org Foundation C.I.C.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -49,7 +50,10 @@ paths:
description: The type of receipt to send.
required: true
x-example: "m.read"
enum: ["m.read"]
x-changedInMatrixVersion:
1.4: |
Allow `m.read.private` receipts.
turt2live marked this conversation as resolved.
Show resolved Hide resolved
enum: ["m.read", "m.read.private"]
- in: path
type: string
name: eventId
Expand Down
5 changes: 5 additions & 0 deletions data/event-schemas/examples/m.receipt.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@
"@rikj:jki.re": {
"ts": 1436451550453
}
},
"m.read.private": {
"@self:example.org": {
"ts": 1661384801651
}
}
}
}
Expand Down
103 changes: 54 additions & 49 deletions data/event-schemas/schema/m.receipt.yaml
Original file line number Diff line number Diff line change
@@ -1,49 +1,54 @@
{
"type": "object",
"title": "Receipt Event",
"description": "Informs the client of new receipts.",
"allOf": [{
"$ref": "core-event-schema/event.yaml"
}],
"properties": {
"content": {
"type": "object",
"patternProperties": {
"^\\$": {
"type": "object",
"x-pattern": "$EVENT_ID",
"title": "Receipts",
"description": "The mapping of event ID to a collection of receipts for this event ID. The event ID is the ID of the event being acknowledged and *not* an ID for the receipt itself.",
"properties": {
"m.read": {
"type": "object",
"title": "Users",
"description": "A collection of users who have sent `m.read` receipts for this event.",
"patternProperties": {
"^@": {
"type": "object",
"title": "Receipt",
"description": "The mapping of user ID to receipt. The user ID is the entity who sent this receipt.",
"x-pattern": "$USER_ID",
"properties": {
"ts": {
"type": "integer",
"format": "int64",
"description": "The timestamp the receipt was sent at."
}
}
}
}
}
}
}
},
"additionalProperties": false
},
"type": {
"type": "string",
"enum": ["m.receipt"]
}
},
"required": ["type", "content"]
}
type: object
title: Receipt Event
description: Informs the client of new receipts.
x-changedInMatrixVersion:
1.4: |
Added `m.read.private` receipts to the event's `content`.
allOf:
- $ref: "core-event-schema/event.yaml"
properties:
content:
type: object
patternProperties:
"^\\$":
type: object
x-pattern: "$EVENT_ID"
title: Receipts
description: |-
The mapping of event ID to a collection of receipts for this
event ID. The event ID is the ID of the event being acknowledged
and *not* an ID for the receipt itself.
properties:
"m.read":
type: object
title: Users
description: |-
A collection of users who have sent `m.read` receipts for
this event.
patternProperties:
"^@": &receiptUserMap
type: object
title: Receipt
description: |-
The mapping of user ID to receipt. The user ID is the
entity who sent this receipt.
x-pattern: "$USER_ID"
properties:
ts:
type: integer
format: int64
description: The timestamp the receipt was sent at.
"m.read.private":
type: object
title: Own User
description: |-
Similar to `m.read`, the users who have sent `m.read.private`
receipts for this event. Due to the nature of private read
receipts, this should only ever have the current user's ID.
patternProperties:
"^@": *receiptUserMap
additionalProperties: false
type:
type: string
enum: ["m.receipt", "m.receipt.private"]
required: ["type", "content"]