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

MSC4140: Delayed events (Futures) #4140

Open
wants to merge 31 commits into
base: main
Choose a base branch
from
Open
Changes from 10 commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
480b00a
draft for expiring event PR
toger5 May 7, 2024
8839b8d
Add msc number
toger5 May 7, 2024
8bf6db7
add security consideration and alternatives
toger5 May 7, 2024
8ec6374
alternative name and alternative content
toger5 May 8, 2024
9f45cfa
review andrewF
toger5 May 8, 2024
54fff99
draft of iteration two (after meeting with the backend team)
toger5 May 10, 2024
abdfe1c
timeout_refresh_token is not a well description since the same token …
toger5 May 13, 2024
53f6186
rename msc, rephrase introduction
toger5 May 14, 2024
087c74e
Add usecase specific section.
toger5 May 20, 2024
538b853
add GET futures endpoint
toger5 May 20, 2024
f7a1aad
shorten introduction
toger5 May 21, 2024
f5f4b38
add alternative section to not include the `m.send_now` field
toger5 May 22, 2024
c16afbc
Update proposals/4140-delayed-events-futures.md
toger5 May 31, 2024
c52c80d
batch sending considerations
toger5 May 31, 2024
bf22260
Update proposals/4140-delayed-events-futures.md
toger5 Jun 3, 2024
7f0d80f
Update proposals/4140-delayed-events-futures.md
toger5 Jun 3, 2024
7b192ac
Update proposals/4140-delayed-events-futures.md
toger5 Jun 3, 2024
f3bf66d
review
toger5 Jun 3, 2024
2d7b27d
add background to usecase specific considerations
toger5 Jun 4, 2024
1140ce9
Simplify main proposal for widget api usage
toger5 Jun 5, 2024
0a7896e
make `future_group_id` server generated and small adjustments
toger5 Jun 6, 2024
8fa33d6
review
toger5 Jun 6, 2024
49d5294
user scoping details
toger5 Jun 13, 2024
7550d9b
Update proposals/4140-delayed-events-futures.md
toger5 Jun 13, 2024
a663bb4
add rate limiting section
toger5 Jun 14, 2024
99b3a20
rename `/futures` to `/future`
toger5 Jun 19, 2024
eb50a19
Update everything to v1
toger5 Jun 20, 2024
9ff051e
Update proposals/4140-delayed-events-futures.md
toger5 Jun 22, 2024
425b9bf
review
toger5 Jun 24, 2024
2e7be46
Swap the alternative of reusing the send and state request with the m…
toger5 Jun 24, 2024
5653fe1
add reference to MSC4143 (MatrixRTC)
toger5 Jul 2, 2024
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
286 changes: 286 additions & 0 deletions proposals/4140-delayed-events-futures.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,286 @@
# MSC4140: Delayed events (Futures)

In the context of matrixRTC where we want
to ignore expired state events of users who left the call without sending a new
state empty `m.call.member` event.

We would like the homeserver to mark this event as expired/send an expired version
in a reasonable time window after a user disconnected.

Currently there is no mechanism for a client to provide a reliable way of
communicating that an event is still valid.
The only way to update an event is to post a new one.
In some situations the client just looses connection or fails to sent the expired
toger5 marked this conversation as resolved.
Show resolved Hide resolved
version of the event.

A generic way in which one can automate expirations is desired.

The described usecase is solved if we allow to send an event in advance
to the homeserver but let the homeserver compute when its actually added to the
DAG.
The condition for actually sending the delayed event would could be a timeout.

## Proposal

To make this as generic as possible, the proposed solution is to allow sending
multiple presigned events and delegate the control of when to actually send these
events to an external services. This allows to exactly define what expiration means,
since any event that will be sent once expired can be defined.

We call those events `Futures`.

A new endpoint is introduced:
`PUT /_matrix/client/v3/rooms/{roomId}/send/future/{txnId}`
toger5 marked this conversation as resolved.
Show resolved Hide resolved
It behaves exactly like the normal send endpoint except that that it allows
to send a list of event contents. The body looks as following:
toger5 marked this conversation as resolved.
Show resolved Hide resolved

```json
{
"m.timeout": 10,
"m.send_on_timeout": {
"content": sendEventBody0,
"type": "m.room.message",
},

"m.send_on_action:${actionName}": {
"content": sendEventBody1,
"type": "m.room.message"
},

// optional
"m.send_now": {
"content": sendEventBody2,
"type": "m.room.message"
},
}
```
toger5 marked this conversation as resolved.
Show resolved Hide resolved

Each of the `sendEventBody` objects are exactly the same as sending a normal
event.

There can be an arbitrary amount of `actionName`s.
toger5 marked this conversation as resolved.
Show resolved Hide resolved
toger5 marked this conversation as resolved.
Show resolved Hide resolved

All of the fields are optional except the `timeout` and the `send_on_timeout`.
This guarantees that all tokens will expire eventually.
toger5 marked this conversation as resolved.
Show resolved Hide resolved

The homeserver can set a limit to the timeout and return an error if the limit
is exceeded.

### Response

The response will mimic the request:

```json
toger5 marked this conversation as resolved.
Show resolved Hide resolved
toger5 marked this conversation as resolved.
Show resolved Hide resolved
{
"m.send_on_timeout": { "eventId": "id_hash" },
"m.send_on_action:${actionName}": { "eventId": "id_hash" },

"future_token": "token",

// optional
"m.send_now": { "eventId": "id_hash" }
}
```

### Delegating futures

The `token` can be used to call another future related endpoint:
`PUT /_matrix/client/v3/futures/refresh` and `PUT /_matrix/client/v3/futures/action/${actionName}`.
toger5 marked this conversation as resolved.
Show resolved Hide resolved
toger5 marked this conversation as resolved.
Show resolved Hide resolved
where the body is:

```json
{
"future_token": "token"
}
```

The information required to call this endpoint is very limited so that almost
no metadata is leaked. This allows to share a refresh link to a different
service. This allows to delegate the send time. An SFU for instance, that tracks the current client connection state,
and pings the HS to refresh and call a dedicated action to communicate
that the user has intentionally left the conference.

The homeserver does the following when receiving a Future.

- It **sends** the optional `m.send_now` event.
- It **generates** a `future_token` and stores it alongside with the time
of retrieval, the event list and the timeout duration.
- **Starts a timer** for the stored `future_token`.

- If a `PUT /_matrix/client/v3/futures/refresh` is received, it
**restarts the timer** with the stored timeout duration.
- If a `PUT /_matrix/client/v3/futures/action/${actionName}` is received, it **sends the associated action event**
`m.action:${actionName}`.
toger5 marked this conversation as resolved.
Show resolved Hide resolved
- If the timer times out, **it sends the timeout event** `m.send_timeout`.
- If the future is a state event and includes a `m.send_now` event
the future is only valid while the `m.send_now`
is still the current state:
AndrewFerr marked this conversation as resolved.
Show resolved Hide resolved

- This means, if the homeserver receives
a new state event for the same state key, the **`future_token`**
**gets invalidated and the associated timer is stopped**.

- There is no race condition here since a possible race between timeout and
new event will always converge to the new event:
- Timeout -> new event: the room state will be updated twice. once by
the content of the `m.send_on_timeout` event but later with the new event.
- new event -> timeout: the new event will invalidate the future. No
toger5 marked this conversation as resolved.
Show resolved Hide resolved

- After the homeservers sends a timeout or action future event, the associated
timer and `future_token` is canceled/invalidated.

So for each Future the client sends, the homeserver will send one event
conditionally at an unknown time that can trigger logic on the client.
This allows for any generic timeout logic.

Timed messages/reminders or ephemeral events could be implemented using this where
clients send a redact as a future or a room event with intentional mentions.

In some scenarios it is important to allow to send an event with an associated
future at the same time.

- One example would be redacting an event. It only makes sense to redact the event
if it exists.
It might be important to have the guarantee, that the redact is received
by the server at the time where the original message is sent.
- In the case of a state event we might want to set the state to `A` and after a
timeout reset it to `{}`. If we have two separate request sending `A` could work
but the event with content `{}` could fail. The state would not automatically
reset to `{}`.

For this usecase an optional `m.send_now` field can be added to the body.
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we have a generlized way to batch sent matrix events this could be leverged on this and a future itself is JUST the future. And the batch send event would define the semantics for sending the future and guaranteeing that the send_now event is also sent

Copy link
Author

@toger5 toger5 May 21, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MSC2716 allows bulk sending events.
It is limited to application services however and focuses on historic data. Since we also need the additional capability to use a template event_id parameter, this probably is not a good fit.


### Getting running futures

Using `GET /_matrix/client/v3/futures` it is possible to get the list of all running futures.
This is an authenticated endpoint. It sends back the json
of the final events how they will end up in the DAG with the associated `future_token`.

```json
toger5 marked this conversation as resolved.
Show resolved Hide resolved
[
{
"m.send_now": finalEvent_0,
AndrewFerr marked this conversation as resolved.
Show resolved Hide resolved
"m.send_on_timeout": finalEvent_1,
...,

"future_token":"token"
},
]
```

This can be used so clients can optionally display events
that will be send in the future.
For self destructing messages it is recommanded to include

Check warning on line 173 in proposals/4140-delayed-events-futures.md

View workflow job for this annotation

GitHub Actions / Spell Check with Typos

"recommanded" should be "recommended".
toger5 marked this conversation as resolved.
Show resolved Hide resolved
this information in the event itself so that the usage of
this endpoint can be minimized.

## Usecase specific considerations

### MatrixRTC

We want can use the actions and the timeout for matrix rtc for the following situations

- If the client takes care of its membership, we use a short timeout value (around 5-20 seconds)
The client will have to ping the refresh endpoint approx every 2-19 seconds.
- When the SFU is capable of taking care of managing our connection state and we trust the SFU to
not disconnect a really long value can be chosen (approx. 2-10hours). The SFU will then only send
an action once the user disconnects or looses connection (it could even be a different action for both cases
handling them differently on the client)
This significantly reduces the amount of calls for the `/future` endpoint since the sfu only needs to ping
once per session (per user) and every 2-5hours (instead of every `X` seconds.)

### Self destructing messages
toger5 marked this conversation as resolved.
Show resolved Hide resolved

This MSC also allows to implement self destructing messages:
toger5 marked this conversation as resolved.
Show resolved Hide resolved
toger5 marked this conversation as resolved.
Show resolved Hide resolved

`PUT /_matrix/client/v3/rooms/{roomId}/send/{eventType}/{txnId}`

```json
{
"m.text": "my msg"
}
```

`PUT /_matrix/client/v3/rooms/{roomId}/send/future/{txnId}`

```json
{
"m.timeout": 10*60,
"m.send_on_timeout": {
"type":"m.room.readact",
"content":{
"redacts": "EvId"
}
}
}
```

## EventId template variable
toger5 marked this conversation as resolved.
Show resolved Hide resolved

It would be useful to be able to send redactions and edits as one http request.
toger5 marked this conversation as resolved.
Show resolved Hide resolved
This would make sure that the client cannot loose connection after sending the first event.
toger5 marked this conversation as resolved.
Show resolved Hide resolved
For instance sending a self destructing message without the redaction.
toger5 marked this conversation as resolved.
Show resolved Hide resolved

The optional proposal is to introduce template variables that are only valid in `Future` events.
`$m.send_now.event_id` in the content of one of the `m.send_on_action:${actionName}` and
`m.send_on_timeout` contents this template variable can be used.
The **Self destructing messages** example would simplify to:
toger5 marked this conversation as resolved.
Show resolved Hide resolved

`PUT /_matrix/client/v3/rooms/{roomId}/send/future/{txnId}`

```json
{
"m.send_now":{
"type":"m.room.message",
"content":{
"m.text": "my msg"
}
},
"m.timeout": 10*60,
"m.send_on_timeout": {
"type":"m.room.readact",
"content":{
"redacts": "$m.send_now.event_id"
}
}
}
```

## Potential issues

## Alternatives

[MSC4018](https://github.com/matrix-org/matrix-spec-proposals/pull/4018) also
proposes a way to make call memberships reliable. It uses the client sync loop as
an indicator to determine if the event is expired. Instead of letting the SFU
inform about the call termination or using the call app ping loop like we propose
here.

---

The following names for the endpoint are considered

- Future
- DelayedEvents
- RetardedEvents
toger5 marked this conversation as resolved.
Show resolved Hide resolved
toger5 marked this conversation as resolved.
Show resolved Hide resolved

## Security considerations

We are using an unauthenticated endpoint to refresh the expirations. Since we use
the token it is hard to guess a correct request and force one of the actions
events of the Future.

It is an intentional decision to not provide an endpoint like
`PUT /_matrix/client/v3/futures/room/{roomId}/event/{eventId}`
where any client with access to the room could also `end` or `refresh`
the expiration. With the token the client sending the event has ownership
over the expiration and only intentional delegation of that ownership
(sharing the token) is possible.

On the other hand the token makes sure that the instance gets as little
information about the matrix metadata of the associated `future` event. It cannot
toger5 marked this conversation as resolved.
Show resolved Hide resolved
even tell with which room or user it is interacting.

## Unstable prefix

## Dependencies
Loading