-
Notifications
You must be signed in to change notification settings - Fork 379
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
MSC2675: Serverside aggregations of message relationships #2675
Changes from 100 commits
c9794d4
03ce398
e5d133b
2d36c36
8eec329
343afff
8e6c0a3
d5bbc72
2091034
68c95a4
84a4a75
ca4ceff
7de182c
143a70e
ad159f4
948f7cd
5d7b404
e8aca3e
657617f
ffa3995
561cfc3
42221ab
037cab3
d7aa3ed
30cf5f8
ee152e0
483224a
16723da
8e532ac
6cf4d58
b0fbee1
53a30fc
f959119
dead293
7299a8c
37db984
36fb70b
061a104
bf9340e
c8533ec
51bc1da
7559275
c475bb9
872d3f5
7119947
0d7b525
764d785
26376de
382bb1d
a0af573
2557d23
c62165a
e26e465
503a569
049863b
44d04f4
1160b4a
0abf2ce
e460a27
58cb3ef
de00362
3dcf0ce
9d96311
541632f
a9943c0
f97df03
798835a
fe78152
17122bf
ba59c43
68b302e
d182b9a
67d60b1
71757a4
48c3063
f93143b
29fcd87
6a76e5c
f61d8dc
c65b467
0717a51
71e4e5c
942cfcb
b680406
ef57449
33471e6
9399b0f
6e8ff69
8033733
3b5f05c
513cd91
178976a
06fcc15
ea8ada3
6cdf9e7
bcf7d15
0114090
3c77504
8a3d9b1
8460f64
e1e2593
e72837a
ae20bcd
1dbe2f0
88892a5
67fa56e
be6b1c7
8f3a28a
ae7dc26
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,313 @@ | ||
# MSC2675: Serverside aggregations of message relationships | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @alphapapa says:
|
||
|
||
It's common to want to send events in Matrix which relate to existing events - | ||
for instance, reactions, edits and even replies/threads. | ||
|
||
Clients typically need to track the related events alongside the original | ||
event they relate to, in order to correctly display them. For instance, | ||
reaction events need to be aggregated together by summing and be shown next to | ||
the event they react to; edits need to be aggregated together by replacing the | ||
original event and subsequent edits, etc. | ||
|
||
It is possible to treat relations as normal events and aggregate them | ||
clientside, but to do so comprehensively could be very resource intensive, as | ||
the client would need to spider all possible events in a room to find | ||
relationships and maintain a correct view. | ||
|
||
Instead, this proposal seeks to solve this problem by defining APIs to let the | ||
server calculate the aggregations on behalf of the client, and so bundle the | ||
aggregated data with the original event where appropriate. It also proposes an | ||
API to let clients paginate through all relations of an event. | ||
|
||
This proposal is one in a series of proposals that defines a mechanism for | ||
events to relate to each other. Together, these proposals replace | ||
[MSC1849](https://github.com/matrix-org/matrix-doc/pull/1849). | ||
|
||
* [MSC2674](https://github.com/matrix-org/matrix-doc/pull/2674) defines a | ||
standard shape for indicating events which relate to other events. | ||
* This proposal defines APIs to let the server calculate the aggregations on | ||
behalf of the client, and so bundle the aggregated data with the original | ||
event where appropriate. | ||
* [MSC2676](https://github.com/matrix-org/matrix-doc/pull/2676) defines how | ||
users can edit messages using this mechanism. | ||
* [MSC2677](https://github.com/matrix-org/matrix-doc/pull/2677) defines how | ||
users can annotate events, such as reacting to events with emoji, using this | ||
mechanism. | ||
|
||
## Proposal | ||
|
||
### Aggregations | ||
|
||
richvdh marked this conversation as resolved.
Show resolved
Hide resolved
|
||
Relation events can be aggregated per relation type by the server. | ||
The format of the aggregated value (hereafter called "aggregation") | ||
depends on the relation type. | ||
|
||
Some relation types might group the aggregations by the `key` property | ||
in the relation and aggregate to an array, | ||
bwindels marked this conversation as resolved.
Show resolved
Hide resolved
|
||
others might aggregate to a single object or any other value really. | ||
|
||
Here are some non-normative examples of what aggregations can look like: | ||
|
||
Example aggregation for [`m.thread`](https://github.com/matrix-org/matrix-doc/pull/3440) (which | ||
aggregates all relations into a single object): | ||
``` | ||
{ | ||
"latest_event": { | ||
"content": { ... }, | ||
... | ||
}, | ||
"count": 7, | ||
"current_user_participated": true | ||
} | ||
``` | ||
|
||
Example aggregation for [`m.annotation`](https://github.com/matrix-org/matrix-doc/pull/2677) (which | ||
aggregates relations into a list of objects, grouped by `key`). | ||
``` | ||
[ | ||
{ | ||
"key": "👍", | ||
"origin_server_ts": 1562763768320, | ||
"count": 3 | ||
}, | ||
{ | ||
"key": "👎", | ||
"origin_server_ts": 1562763768320, | ||
"count": 2 | ||
} | ||
] | ||
``` | ||
|
||
#### Bundling | ||
|
||
Other than during non-gappy incremental syncs, timeline events that have other | ||
events relate to them should include the aggregation of those related events | ||
in the `m.relations` property of their unsigned data. This process is | ||
referred to as "bundling", and the aggregated relations included via | ||
this mechanism are called "bundled aggregations". | ||
turt2live marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
By sending a summary of the relations, bundling | ||
avoids us having to always send lots of individual relation events | ||
to the client. | ||
|
||
Aggregations are never bundled into state events. This is a current | ||
implementation detail that could be revisited later, | ||
rather than a specific design decision. | ||
|
||
The following client-server APIs should bundle aggregations | ||
with events they return: | ||
bwindels marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
- `GET /rooms/{roomId}/messages` | ||
- `GET /rooms/{roomId}/context/{eventId}` | ||
- `GET /rooms/{roomId}/event/{eventId}` | ||
- `GET /sync`, only for room sections in the response where `limited` field | ||
is `true`; this amounts to all rooms in the response if | ||
the `since` request parameter was not passed, also known as an initial sync. | ||
- `GET /relations`, as proposed in this MSC. | ||
|
||
Deprecated APIs like `/initialSync` and `/events/{eventId}` are *not* required | ||
to bundle aggregations. | ||
|
||
The bundled aggregations are grouped according to their relation type. | ||
The format of `m.relations` (here with ficticious relation types) is as follows: | ||
|
||
```json | ||
{ | ||
"event_id": "abc", | ||
"unsigned": { | ||
"m.relations": { | ||
"some_rel_type": { "some_aggregated_prop": true }, | ||
"other_rel_type": { "other_aggregated_prop": 5 }, | ||
} | ||
} | ||
} | ||
``` | ||
|
||
#### Client-side aggregation | ||
|
||
Bundled aggregations on an event give a snapshot of what relations were know | ||
bwindels marked this conversation as resolved.
Show resolved
Hide resolved
|
||
at the time the event was received. When relations are received through `/sync`, | ||
clients should locally aggregate (as they might have done already before | ||
supporting this MSC) the relation on top of any bundled aggregation the server | ||
might have sent along previously with the target event, to get an up to date | ||
view of the aggregations for the target event. The aggregation algorithm is the | ||
same as the one described here for the server. | ||
|
||
### Querying relations | ||
bwindels marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
A single event can have lots of associated relations, and we do not want to | ||
overload the client by, for example, including them all bundled with the | ||
related-to event. Instead, we also provide a new `/relations` API in | ||
order to paginate over the relations, which behaves in a similar way to | ||
`/messages`, except using `next_batch` and `prev_batch` names | ||
(in line with `/sync` API). Tokens from `/sync` or `/messages` can be | ||
passed to `/relations` to only get relating events from a section of | ||
the timeline. | ||
|
||
The `/relations` API returns the discrete relation events | ||
associated with an event that the server is aware of | ||
in standard topological order. Note that events may be missing, | ||
see [limitations](#servers-might-not-be-aware-of-all-relations-of-an-event). | ||
You can optionally filter by a given relation type and the event type of the | ||
relating event: | ||
|
||
``` | ||
GET /_matrix/client/v1/rooms/{roomID}/relations/{event_id}[/{rel_type}[/{event_type}]][?from=token][&to=token][&limit=amount] | ||
``` | ||
|
||
``` | ||
{ | ||
"chunk": [ | ||
{ | ||
"type": "m.reaction", | ||
"sender": "...", | ||
"content": { | ||
"m.relates_to": { | ||
"rel_type": "m.annotation", | ||
... | ||
} | ||
} | ||
} | ||
], | ||
"prev_batch": "some_token", | ||
"next_batch": "some_token", | ||
} | ||
``` | ||
|
||
The endpoint does not have any trailing slashes. | ||
bwindels marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
The `from` and `limit` query parameters are used for pagination, and work | ||
just like described for the `/messages` endpoint. | ||
|
||
Note that [MSC2676](https://github.com/matrix-org/matrix-doc/pull/2676) | ||
adds the related-to event in `original_event` property of the response. | ||
This way the full history (e.g. also the first, original event) of the event | ||
is obtained without further requests. See that MSC for further details. | ||
|
||
### End to end encryption | ||
bwindels marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
Since the server has to be able to aggregate relation events, structural | ||
information about relations must be visible to the server, and so the | ||
`m.relates_to` field must be included in the plaintext. | ||
|
||
A future MSC may define a method for encrypting certain parts of the | ||
`m.relates_to` field that may contain sensitive information. | ||
bwindels marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
### Redactions | ||
bwindels marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
Redacted relations should not be taken into consideration in | ||
bundled aggregations, nor should they be returned from `/relations`. | ||
|
||
Requesting `/relations` on a redacted event should | ||
still return any existing relation events. | ||
This is in line with other APIs like `/context` and `/messages`. | ||
|
||
### Local echo | ||
|
||
For the best possible user experience, clients should also include unsent | ||
relations into the client-side aggregation. When adding a relation to the send | ||
queue, clients should locally aggregate it into the relations of the target | ||
event, ideally regardless of the target event having received an `event_id` | ||
already or still being pending. If the client gives up on sending the relation | ||
for some reason, the relation should be de-aggregated from the relations of | ||
the target event. If the client offers the user a possibility of manually | ||
retrying to send the relation, it should be re-aggregated when the user does so. | ||
|
||
De-aggregating a relation refers to rerunning the aggregation for a given | ||
target event while not considering the de-aggregated event any more. | ||
|
||
Upon receiving the remote echo for any relations, a client is likely to remove | ||
the pending event from the send queue. Here, it should also de-aggregate the | ||
pending event from the target event's relations, and re-aggregate the received | ||
remote event from `/sync` to make sure the client-side aggregation happens with | ||
the same event data as on the server. | ||
|
||
When adding a redaction for a relation to the send queue, the relation | ||
referred to should be de-aggregated from the relations of the target of the | ||
relation. Similar to a relation, when the sending of the redaction fails or | ||
is cancelled, the relation should be aggregated again. | ||
|
||
One possible way clients can locally relate pending events is by their | ||
`transaction_id`. When the target event receives its `event_id` (either receives | ||
the remote echo, or receives the `event_id` from the `/send` response, | ||
whichever comes first), the target event id (`m.relates_to`.`event_id`) of | ||
any relations in the send queue will | ||
need to be set the newly received `event_id`. | ||
turt2live marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
Particularly, please remember to let users edit unsent messages (as this is a | ||
common case for rapidly fixing a typo in a msg which is still in flight!) | ||
bwindels marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
## Edge cases | ||
bwindels marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
### How do you handle ignored users? | ||
|
||
* Information about relations sent from ignored users must never be sent to | ||
the client, either in aggregations or discrete relation events. | ||
This is to let you block someone from harassing you with emoji reactions | ||
(or using edits as a side-channel to harass you). Therefor, it is possible | ||
bwindels marked this conversation as resolved.
Show resolved
Hide resolved
|
||
that different users will see different aggregations (a different last edit, | ||
or a different reaction count) on an event. | ||
|
||
## Limitations | ||
|
||
### Relations can be missed while not being in the room | ||
|
||
Relation events behave no different from other events in terms of room history visibility, | ||
which means that some relations might not be visible to a user while they are not invited | ||
or has not joined the room. This can cause a user to see an incomplete edit history or reaction count | ||
bwindels marked this conversation as resolved.
Show resolved
Hide resolved
|
||
based on discrete relation events upon (re)joining a room. | ||
|
||
Ideally the server would not include these events in aggregations, as it would mean breaking | ||
the room history visibility rules, but this MSC defers addressing this limitation and | ||
specifying the exact server behaviour to [MSC3570](https://github.com/matrix-org/matrix-doc/pull/3570). | ||
|
||
### Servers might not be aware of all relations of an event | ||
|
||
The response of `/relations` might be incomplete because the homeserver | ||
potentially doesn't have the full DAG of the room. The federation API doens't | ||
have an equivalent of the `/relations` API, so has no way but to fetch the | ||
full DAG over federation to assure itself that it is aware of all relations. | ||
|
||
[MSC2836](https://github.com/matrix-org/matrix-doc/blob/kegan/msc/threading/proposals/2836-threading.md#making-relationships) also makes mention of this issue. | ||
bwindels marked this conversation as resolved.
Show resolved
Hide resolved
bwindels marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
### Event type based aggregation and filtering won't work well in encrypted rooms | ||
|
||
The `/relations` endpoint allows filtering by event type, | ||
which for encrypted rooms will be `m.room.encrypted`, rendering this filtering | ||
less useful for encrypted rooms. Aggregation algorithms that take the type of | ||
the relating events they aggregate into account will suffer from the same | ||
limitation. | ||
|
||
## Future extensions | ||
|
||
### Handling limited (gappy) syncs | ||
|
||
For the special case of a gappy incremental sync, many relations (particularly | ||
reactions) may have occurred during the gap. It would be inefficient to send | ||
each one individually to the client, but it would also be inefficient to send | ||
all possible bundled aggregations to the client. | ||
|
||
The server could tell the client the event IDs of events which | ||
predate the gap which received relations during the gap. This means that the | ||
client could invalidate its copy of those events (if any) and then requery them | ||
(including their bundled relations) from the server if/when needed, | ||
for example using an extension of the `/event` API for batch requests. | ||
|
||
The server could do this with a new `stale_events` field of each room object | ||
in the sync response. The `stale_events` field would list all the event IDs | ||
prior to the gap which had updated relations during the gap. The event IDs | ||
would be grouped by relation type, | ||
and paginated as per the normal Matrix pagination model. | ||
|
||
This was originally part of this MSC but left out to limit the scope | ||
to what is implemented at the time of writing. | ||
bwindels marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
## Prefix | ||
|
||
While this MSC is not considered stable, the endpoints become: | ||
|
||
- `GET /_matrix/client/unstable/rooms/{roomID}/relations/{eventID}[/{relationType}[/{eventType}]]` | ||
|
||
None of the newly introduced identifiers should use a prefix though, as this MSC | ||
tries to document relation support already being used in | ||
the wider matrix ecosystem. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@revidee says:
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Correct, there is currently not MSC which proposes this behavior.
Correct, the local client might not know some of the related messages without filling the entire gap. (Note that I don't think this is any different than if only local aggregation was done -- so I'm unsure if this MSC really changes that behavior.)
I think this is pretty much what the gappy syncs issue in the MSC describes. I'm unsure what current clients do -- @gsouquet do you know if Element Web does anything special in this case?
This might work, but I doubt the savings in traffic would be the tremendous, unless the majority of your events are annotations, specifically. (I think you would still want to fetch references / edits / threads since the aggregation of those somewhat assumes you're going to fetch the full event anyway).
Note that the
/aggregations
endpoint was removed from this MSC (and from Synapse). MSC3571 includes the bits split out of MSC2675.I don't think you're missing anything obvious, but maybe @gsouquet has some ideas of how the clients deal with this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you for raising this @revidee
Just like @clokep said, I believe you're pretty much on point and your assessment of the situation is pretty correct.
Besides threads, Element Web does not use bundled aggregation.
If we take the example of reactions, the bundled aggregations does not give you enough information to generate the user interface that Element wants to provide. To be able to do that you will need a complete list of the annotations, which is not practical and comes with quite a big network overhead
The way that threads went around this problem is by providing a set of information to render the initial event tile without having to fetch all events that belong to a thread (we're currently giving the number of replies to a thread, whether the logged in user has participated to it, and the last event of that thread). That means that client only have to fetch the root event of that thread to refresh the bundled relationship on app load.
This is more practical but comes at the cost of slightly coupling your server implementation with your UI