diff --git a/proposals/4140-expiring-events-with-keep-alive-endpoint.md b/proposals/4140-expiring-events-with-keep-alive-endpoint.md index 392a86c31..b784c2502 100644 --- a/proposals/4140-expiring-events-with-keep-alive-endpoint.md +++ b/proposals/4140-expiring-events-with-keep-alive-endpoint.md @@ -19,102 +19,124 @@ time window after a user disconnected. ## Proposal -Events can contain a `"m.will_expire": "running" | "expired" | "ended"` field. -This is marking the event as expired `"m.will_expire": "expired" | "ended"` or as -still alive `"m.will_expire": "running"`. -This field lives outside the ciphertext content (hence it also works for encrypted -events) and is set via the usual `PUT` request if the content contains the additional -`"m.will_expire": 10` field (similar how it is done with relations), with the desired -timeout duration in seconds. +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. -Request +We call those events `Futures`. + +A new endpoint is introduced: +`PUT /_matrix/client/v3/rooms/{roomId}/send/{eventType}/{txnId}/future` +and +`PUT /_matrix/client/v3/rooms/{roomId}/state/{eventType}/{stateKey}/future` +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: ```json { - "m.will_expire": 10, - "body": "hello" + "m.timeout": 10, + "m.send_on_timeout": {...sendEventBody}, + + "m.send_on_action:${actionName}": {...sendEventBody}, + + // optional + "m.send_now": {...sendEventBody}, } ``` -If the homeserver detects a `m.will_expire` field it will store and distribute the -event as hiding the timeout duration: +Each of the `sendEventBody` objects are exactly the same as sending a normal +event. -```json -{ - "content":{ - "m.will_expire": "running", - "body": "hello", - }, - "other_fields":"sender, origin_server_ts ..." -} -``` +There can be an arbitrary amount of `actionName`s. + +All of the fields are optional except the `timeout` and the `send_on_timeout`. +This guarantees that all tokens will expire eventually. + +The homeserver can set a limit to the timeout and return an error if the limit +is exceeded. -The response to the client will be: +The response will mimic the request: ```json { - "eventId": "id_hash", - "expire_refresh_token": "refresh_hash", + + "m.send_on_timeout": { + "eventId": "id_hash" + }, + "m.send_on_action:${actionName}": { + "eventId": "id_hash" + }, + + "timeout_refresh_token": "refresh_token", + + // optional + "m.send_now": { "eventId": "id_hash"}, } ``` -The default response is extended with the `expire_refresh_token` which -can be used to reset the expiration timeout (in this example 10 seconds). -A new unauthenticated endpoint is introduced: -`PUT /_matrix/client/v3/expiration/{refresh_method}` -where the `refresh_method` is one of: `[refresh, end]` -The body contains the refresh token so the homeserver knows what to refresh. +The `refresh_token` can be used to call another future related endpoint: +`PUT /_matrix/client/v3/futures/refresh` and `PUT /_matrix/client/v3/futures/action/${actionName}`. +where the body is: ```json { - "expire_refresh_token": "refresh_hash", + "timeout_refresh_token":"refresh_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 (an SFU for instance) that can track the current client connection state, -and pings the HS to refresh and informs the HS about a disconnect. - -The homeserver does the following when receiving an event with `m.will_expire` - -- It generates a token and stores it alongside with the time of retrieval, -the eventId and the expire duration. -- Starts a timer for the stored expiration token. - - If a `PUT /_matrix/client/v3/expiration/refresh` is received, the - timer is restarted with the stored expire duration. - - If a `PUT /_matrix/client/v3/expiration/end` is received, the - event _gets ended_. - - If the timer times out, the event _gets expired_. - - If the event is a state event only the latest/current state is considered. If - the homeserver receives a new state event without `m.will_expire` but with the - same state key, the expire_refresh_token gets invalidated and the associated timer - is stopped. - -The event _gets expired_/_gets ended_ means: - -- The homeserver **sends a new event** that is a copy of the previous event but: - - If it gets _expired_ the event will include: `"m.will_expire": "expired"` - - If it gets _ended_ the event will include: `"m.will_expire": "ended"`. - - Additionally it includes a relation to the original event with `rel_type: "m.expire.relationship"` - - ```json - "m.relates_to": { - "event_id": "$original_event", - "rel_type": "m.expire.relationship" - }, - "m.will_expire": "ended" | "expired", - ``` - -- The homeserver stops the associated timer and invalidates (deletes) the `expire_refresh_token` - -So for each event that is sent with `m.will_expire: X` where X is duration in -seconds > 0. The homeserver will sent another event which can be used to trigger -logic on the client. This allows for any generic timeout logic. - -Timed messages/reminders could also be implemented using this where clients ignore -the `"will_expire":"running"` events for a specific event type but render the -`"will_expire":"expired"` events. +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 `timeout_refresh_token` and stores it alongside with the time +of retrieval, the event list and the timeout duration. +- Starts a timer for the stored `timeout_refresh_token`. + - If a `PUT /_matrix/client/v3/futures/refresh` is received, the + timer is restarted with the stored timeout duration. + - If a `PUT /_matrix/client/v3/futures/action/${actionName}` is received, one of + the associated `m.action:${actionName}` + event will be send. + - If the timer times out, the one of the `m.send_timeout` event will be sent. + - If the future + - is a state event (`PUT /_matrix/client/v3/rooms/{roomId}/state/{eventType}/{stateKey}/future`) + - and includes a `m.send_now` event + + the future is only valid while the `m.send_now` + is still the current state. This means, if the homeserver receives + a new state event for the same state key, the `timeout_refresh_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 +- When a timeout or action future is sent, the homeserver stops the associated +timer and invalidates (deletes) the `timeout_refresh_token`. + +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. ## Potential issues @@ -126,43 +148,22 @@ 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. ---- -It might not be necessary to change the value of `"m.will_expire" = 10` to -`"m.will_expire" = "running"` it makes it easier to understand and also -hides more potential metadata but it is questionable if that bring any benefit. - ---- -The name `m.will_expire` has been chosen since it communicates that it becomes -invalid. And that it is an event that automatically changes state -(`will_expire` vs `expired`). But it does not imply what expired vs non expired -means, it is flexible in how can be used. -Alternatives could by: - -- `m.alive` - - pro: communicates it might change (alive is always temporal) - - con: ver strong bias on how to use it `valid/invalid` -- `m.timeout` - - pro: very unbiased in how its used - timeout over can also mean the client - will show a reminder. - - pro: clear that it has something to do with time. - - con: not so clear the homeserver will automatically do sth. - - con: not so clear that this timeout can be refreshed? - ## Security considerations -We are using unauthenticated endpoint to refresh the expirations. Since we use -the token it is hard to guess a correct endpoint and randomly end `will_expire` -events. +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/expiration/room/{roomId}/event/{eventId}` +`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 `will_expire` event. +information about the matrix metadata of the associated `future` event. It cannot +even tell with which room or user it is interacting. ## Unstable prefix