Add knocking to the spec

Spec for https://github.com/matrix-org/matrix-doc/pull/2998
Spec for https://github.com/matrix-org/matrix-doc/pull/2403

This deliberately does not help towards fixing https://github.com/matrix-org/matrix-doc/issues/3153 in order to remain consistent with prior room versions, and to keep the diff smaller on this change. A future change will address room version legibility.
pull/977/head
Travis Ralston 4 years ago committed by Richard van der Hoff
parent 194fef8022
commit fa6cc8a1ff

@ -327,12 +327,12 @@ sent to the room `!qporfwt:matrix.org`:
Federation maintains *shared data structures* per-room between multiple Federation maintains *shared data structures* per-room between multiple
homeservers. The data is split into `message events` and `state events`. homeservers. The data is split into `message events` and `state events`.
Message events: Message events:
These describe transient 'once-off' activity in a room such as an These describe transient 'once-off' activity in a room such as an
instant messages, VoIP call setups, file transfers, etc. They generally instant messages, VoIP call setups, file transfers, etc. They generally
describe communication activity. describe communication activity.
State events: State events:
These describe updates to a given piece of persistent information These describe updates to a given piece of persistent information
('state') related to a room, such as the room's name, topic, membership, ('state') related to a room, such as the room's name, topic, membership,
participating servers, etc. State is modelled as a lookup table of participating servers, etc. State is modelled as a lookup table of
@ -505,7 +505,7 @@ stable and unstable periodically for a variety of reasons, including
discovered security vulnerabilities and age. discovered security vulnerabilities and age.
Clients should not ask room administrators to upgrade their rooms if the Clients should not ask room administrators to upgrade their rooms if the
room is running a stable version. Servers SHOULD use room version 6 as room is running a stable version. Servers SHOULD use **room version 6** as
the default room version when creating new rooms. the default room version when creating new rooms.
The available room versions are: The available room versions are:
@ -522,10 +522,11 @@ The available room versions are:
signing key validity periods. signing key validity periods.
- [Version 6](/rooms/v6) - **Stable**. Alters several - [Version 6](/rooms/v6) - **Stable**. Alters several
authorization rules for events. authorization rules for events.
- [Version 7](/rooms/v7) - **Stable**. Introduces knocking.
## Specification Versions ## Specification Versions
The specification for each API is versioned in the form `rX.Y.Z`. The specification for each API is versioned in the form `rX.Y.Z`.
- A change to `X` reflects a breaking change: a client implemented - A change to `X` reflects a breaking change: a client implemented
against `r1.0.0` may need changes to work with a server which against `r1.0.0` may need changes to work with a server which
supports (only) `r2.0.0`. supports (only) `r2.0.0`.

@ -68,118 +68,118 @@ request being made was invalid.
The common error codes are: The common error codes are:
`M_FORBIDDEN` `M_FORBIDDEN`
Forbidden access, e.g. joining a room without permission, failed login. Forbidden access, e.g. joining a room without permission, failed login.
`M_UNKNOWN_TOKEN` `M_UNKNOWN_TOKEN`
The access token specified was not recognised. The access token specified was not recognised.
An additional response parameter, `soft_logout`, might be present on the An additional response parameter, `soft_logout`, might be present on the
response for 401 HTTP status codes. See [the soft logout response for 401 HTTP status codes. See [the soft logout
section](#soft-logout) for more information. section](#soft-logout) for more information.
`M_MISSING_TOKEN` `M_MISSING_TOKEN`
No access token was specified for the request. No access token was specified for the request.
`M_BAD_JSON` `M_BAD_JSON`
Request contained valid JSON, but it was malformed in some way, e.g. Request contained valid JSON, but it was malformed in some way, e.g.
missing required keys, invalid values for keys. missing required keys, invalid values for keys.
`M_NOT_JSON` `M_NOT_JSON`
Request did not contain valid JSON. Request did not contain valid JSON.
`M_NOT_FOUND` `M_NOT_FOUND`
No resource was found for this request. No resource was found for this request.
`M_LIMIT_EXCEEDED` `M_LIMIT_EXCEEDED`
Too many requests have been sent in a short period of time. Wait a while Too many requests have been sent in a short period of time. Wait a while
then try again. then try again.
`M_UNKNOWN` `M_UNKNOWN`
An unknown error has occurred. An unknown error has occurred.
Other error codes the client might encounter are: Other error codes the client might encounter are:
`M_UNRECOGNIZED` `M_UNRECOGNIZED`
The server did not understand the request. The server did not understand the request.
`M_UNAUTHORIZED` `M_UNAUTHORIZED`
The request was not correctly authorized. Usually due to login failures. The request was not correctly authorized. Usually due to login failures.
`M_USER_DEACTIVATED` `M_USER_DEACTIVATED`
The user ID associated with the request has been deactivated. Typically The user ID associated with the request has been deactivated. Typically
for endpoints that prove authentication, such as `/login`. for endpoints that prove authentication, such as `/login`.
`M_USER_IN_USE` `M_USER_IN_USE`
Encountered when trying to register a user ID which has been taken. Encountered when trying to register a user ID which has been taken.
`M_INVALID_USERNAME` `M_INVALID_USERNAME`
Encountered when trying to register a user ID which is not valid. Encountered when trying to register a user ID which is not valid.
`M_ROOM_IN_USE` `M_ROOM_IN_USE`
Sent when the room alias given to the `createRoom` API is already in Sent when the room alias given to the `createRoom` API is already in
use. use.
`M_INVALID_ROOM_STATE` `M_INVALID_ROOM_STATE`
Sent when the initial state given to the `createRoom` API is invalid. Sent when the initial state given to the `createRoom` API is invalid.
`M_THREEPID_IN_USE` `M_THREEPID_IN_USE`
Sent when a threepid given to an API cannot be used because the same Sent when a threepid given to an API cannot be used because the same
threepid is already in use. threepid is already in use.
`M_THREEPID_NOT_FOUND` `M_THREEPID_NOT_FOUND`
Sent when a threepid given to an API cannot be used because no record Sent when a threepid given to an API cannot be used because no record
matching the threepid was found. matching the threepid was found.
`M_THREEPID_AUTH_FAILED` `M_THREEPID_AUTH_FAILED`
Authentication could not be performed on the third party identifier. Authentication could not be performed on the third party identifier.
`M_THREEPID_DENIED` `M_THREEPID_DENIED`
The server does not permit this third party identifier. This may happen The server does not permit this third party identifier. This may happen
if the server only permits, for example, email addresses from a if the server only permits, for example, email addresses from a
particular domain. particular domain.
`M_SERVER_NOT_TRUSTED` `M_SERVER_NOT_TRUSTED`
The client's request used a third party server, e.g. identity server, The client's request used a third party server, e.g. identity server,
that this server does not trust. that this server does not trust.
`M_UNSUPPORTED_ROOM_VERSION` `M_UNSUPPORTED_ROOM_VERSION`
The client's request to create a room used a room version that the The client's request to create a room used a room version that the
server does not support. server does not support.
`M_INCOMPATIBLE_ROOM_VERSION` `M_INCOMPATIBLE_ROOM_VERSION`
The client attempted to join a room that has a version the server does The client attempted to join a room that has a version the server does
not support. Inspect the `room_version` property of the error response not support. Inspect the `room_version` property of the error response
for the room's version. for the room's version.
`M_BAD_STATE` `M_BAD_STATE`
The state change requested cannot be performed, such as attempting to The state change requested cannot be performed, such as attempting to
unban a user who is not banned. unban a user who is not banned.
`M_GUEST_ACCESS_FORBIDDEN` `M_GUEST_ACCESS_FORBIDDEN`
The room or resource does not permit guests to access it. The room or resource does not permit guests to access it.
`M_CAPTCHA_NEEDED` `M_CAPTCHA_NEEDED`
A Captcha is required to complete the request. A Captcha is required to complete the request.
`M_CAPTCHA_INVALID` `M_CAPTCHA_INVALID`
The Captcha provided did not match what was expected. The Captcha provided did not match what was expected.
`M_MISSING_PARAM` `M_MISSING_PARAM`
A required parameter was missing from the request. A required parameter was missing from the request.
`M_INVALID_PARAM` `M_INVALID_PARAM`
A parameter that was specified has the wrong value. For example, the A parameter that was specified has the wrong value. For example, the
server expected an integer and instead received a string. server expected an integer and instead received a string.
`M_TOO_LARGE` `M_TOO_LARGE`
The request or entity was too large. The request or entity was too large.
`M_EXCLUSIVE` `M_EXCLUSIVE`
The resource being requested is reserved by an application service, or The resource being requested is reserved by an application service, or
the application service making the request has not created the resource. the application service making the request has not created the resource.
`M_RESOURCE_LIMIT_EXCEEDED` `M_RESOURCE_LIMIT_EXCEEDED`
The request cannot be completed because the homeserver has reached a The request cannot be completed because the homeserver has reached a
resource limit imposed on it. For example, a homeserver held in a shared resource limit imposed on it. For example, a homeserver held in a shared
hosting environment may reach a resource limit if it starts using too hosting environment may reach a resource limit if it starts using too
@ -189,7 +189,7 @@ Typically, this error will appear on routes which attempt to modify
state (e.g.: sending messages, account data, etc) and not routes which state (e.g.: sending messages, account data, etc) and not routes which
only read state (e.g.: `/sync`, get account data, etc). only read state (e.g.: `/sync`, get account data, etc).
`M_CANNOT_LEAVE_SERVER_NOTICE_ROOM` `M_CANNOT_LEAVE_SERVER_NOTICE_ROOM`
The user is unable to reject an invite to join the server notices room. The user is unable to reject an invite to join the server notices room.
See the [Server Notices](#server-notices) module for more information. See the [Server Notices](#server-notices) module for more information.
@ -238,23 +238,23 @@ time.
In this section, the following terms are used with specific meanings: In this section, the following terms are used with specific meanings:
`PROMPT` `PROMPT`
Retrieve the specific piece of information from the user in a way which Retrieve the specific piece of information from the user in a way which
fits within the existing client user experience, if the client is fits within the existing client user experience, if the client is
inclined to do so. Failure can take place instead if no good user inclined to do so. Failure can take place instead if no good user
experience for this is possible at this point. experience for this is possible at this point.
`IGNORE` `IGNORE`
Stop the current auto-discovery mechanism. If no more auto-discovery Stop the current auto-discovery mechanism. If no more auto-discovery
mechanisms are available, then the client may use other methods of mechanisms are available, then the client may use other methods of
determining the required parameters, such as prompting the user, or determining the required parameters, such as prompting the user, or
using default values. using default values.
`FAIL_PROMPT` `FAIL_PROMPT`
Inform the user that auto-discovery failed due to invalid/empty data and Inform the user that auto-discovery failed due to invalid/empty data and
`PROMPT` for the parameter. `PROMPT` for the parameter.
`FAIL_ERROR` `FAIL_ERROR`
Inform the user that auto-discovery did not return any usable URLs. Do Inform the user that auto-discovery did not return any usable URLs. Do
not continue further with the current login process. At this point, not continue further with the current login process. At this point,
valid data was obtained, but no server is available to serve the client. valid data was obtained, but no server is available to serve the client.
@ -606,7 +606,7 @@ flow with three stages will resemble the following diagram:
#### Authentication types #### Authentication types
This specification defines the following auth types: This specification defines the following auth types:
- `m.login.password` - `m.login.password`
- `m.login.recaptcha` - `m.login.recaptcha`
- `m.login.sso` - `m.login.sso`
@ -893,7 +893,7 @@ type of identifier being used, and depending on the type, has other
fields giving the information required to identify the user as described fields giving the information required to identify the user as described
below. below.
This specification defines the following identifier types: This specification defines the following identifier types:
- `m.id.user` - `m.id.user`
- `m.id.thirdparty` - `m.id.thirdparty`
- `m.id.phone` - `m.id.phone`
@ -1712,7 +1712,7 @@ event also has a `creator` key which contains the user ID of the room
creator. It will also generate several other events in order to manage creator. It will also generate several other events in order to manage
permissions in this room. This includes: permissions in this room. This includes:
- `m.room.power_levels` : Sets the power levels of users and required power - `m.room.power_levels` : Sets the power levels of users and required power
levels for various actions within the room such as sending events. levels for various actions within the room such as sending events.
- `m.room.join_rules` : Whether the room is "invite-only" or not. - `m.room.join_rules` : Whether the room is "invite-only" or not.
@ -1778,6 +1778,8 @@ in that room. There are several states in which a user may be, in
relation to a room: relation to a room:
- Unrelated (the user cannot send or receive events in the room) - Unrelated (the user cannot send or receive events in the room)
- Knocking (the user has requested to participate in the room, but has
not yet been allowed to)
- Invited (the user has been invited to participate in the room, but - Invited (the user has been invited to participate in the room, but
is not yet participating) is not yet participating)
- Joined (the user can send and receive events in the room) - Joined (the user can send and receive events in the room)
@ -1786,49 +1788,28 @@ relation to a room:
There is an exception to the requirement that a user join a room before There is an exception to the requirement that a user join a room before
sending events to it: users may send an `m.room.member` event to a room sending events to it: users may send an `m.room.member` event to a room
with `content.membership` set to `leave` to reject an invitation if they with `content.membership` set to `leave` to reject an invitation if they
have currently been invited to a room but have not joined it. have currently been invited to a room but have not joined it. The same
applies for retracting knocks on the room that the user sent.
Some rooms require that users be invited to it before they can join; Some rooms require that users be invited to it before they can join;
others allow anyone to join. Whether a given room is an "invite-only" others allow anyone to join. Whether a given room is an "invite-only"
room is determined by the room config key `m.room.join_rules`. It can room is determined by the room config key `m.room.join_rules`. It can
have one of the following values: have one of the following values:
`public` `public`
This room is free for anyone to join without an invite. This room is free for anyone to join without an invite.
`invite` `invite`
This room can only be joined if you were invited. This room can only be joined if you were invited.
`knock`
This room can only be joined if you were invited, and allows anyone to
request an invite to the room. Note that this join rule is only available
to rooms based upon [room version 7](/rooms/v7).
The allowable state transitions of membership are: The allowable state transitions of membership are:
``` ![membership-flow-diagram](/diagrams/membership.png)
/ban
+------------------------------------------------------+
| |
| +----------------+ +----------------+ |
| | /leave | | | |
| | v v | |
/invite +--------+ +-------+ | |
------------>| invite |<----------| leave |----+ | |
+--------+ /invite +-------+ | | |
| | ^ | | |
| | | | | |
/join | +---------------+ | | | |
| | /join if | | | |
| | join_rules | | /ban | /unban |
| | public /leave | | | |
v v or | | | |
+------+ /kick | | | |
------------>| join |-------------------+ | | |
/join +------+ v | |
if | +-----+ | |
join_rules +-------------------------->| ban |-----+ |
public /ban +-----+ |
^ ^ |
| | |
----------------------------------------------+ +----------------------+
/ban
```
{{% http-api spec="client-server" api="list_joined_rooms" %}} {{% http-api spec="client-server" api="list_joined_rooms" %}}
@ -1838,14 +1819,51 @@ The allowable state transitions of membership are:
{{% http-api spec="client-server" api="joining" %}} {{% http-api spec="client-server" api="joining" %}}
##### Knocking on rooms
<!--
This section is here because it's most similar to being invited/joining a
room, though has added complexity which needs to be explained. Otherwise
this will have been just the API definition and nothing more (like invites).
-->
If the join rules allow, external users to the room can `/knock` on it to
request permission to join. Users with appropriate permissions within the
room can then approve (`/invite`) or deny (`/kick`, `/ban`, or otherwise
set membership to `leave`) the knock. Knocks can be retracted by calling
`/leave` or otherwise setting membership to `leave`.
Users who are currently in the room, already invited, or banned cannot
knock on the room.
To accept another user's knock, the user must have permission to invite
users to the room. To reject another user's knock, the user must have
permission to either kick or ban users (whichever is being performed).
Note that setting another user's membership to `leave` is kicking them.
The knocking homeserver should assume that an invite to the room means
that the knock was accepted, even if the invite is not explicitly related
to the knock.
Homeservers are permitted to automatically accept invites as a result of
knocks as they should be aware of the user's intent to join the room. If
the homeserver is not auto-accepting invites (or there was an unrecoverable
problem with accepting it), the invite is expected to be passed down normally
to the client to handle. Clients can expect to see the join event if the
server chose to auto-accept.
{{% http-api spec="client-server" api="knocking" %}}
#### Leaving rooms #### Leaving rooms
A user can leave a room to stop receiving events for that room. A user A user can leave a room to stop receiving events for that room. A user
must have been invited to or have joined the room before they are must have been invited to or have joined the room before they are
eligible to leave the room. Leaving a room to which the user has been eligible to leave the room. Leaving a room to which the user has been
invited rejects the invite. Once a user leaves a room, it will no longer invited rejects the invite, and can retract a knock. Once a user leaves
appear in the response to the [`/sync`](/client-server-api/#get_matrixclientr0sync) API unless it is explicitly a room, it will no longer appear in the response to the
requested via a filter with the `include_leave` field set to `true`. [`/sync`](/client-server-api/#get_matrixclientr0sync) API unless it is
explicitly requested via a filter with the `include_leave` field set
to `true`.
Whether or not they actually joined the room, if the room is an Whether or not they actually joined the room, if the room is an
"invite-only" room the user will need to be re-invited before they can "invite-only" room the user will need to be re-invited before they can
@ -1853,7 +1871,7 @@ re-join the room.
A user can also forget a room which they have left. Rooms which have A user can also forget a room which they have left. Rooms which have
been forgotten will never appear the response to the [`/sync`](/client-server-api/#get_matrixclientr0sync) API, been forgotten will never appear the response to the [`/sync`](/client-server-api/#get_matrixclientr0sync) API,
until the user re-joins or is re-invited. until the user re-joins, is re-invited, or knocks.
A user may wish to force another user to leave a room. This can be done A user may wish to force another user to leave a room. This can be done
by 'kicking' the other user. To do so, the user performing the kick MUST by 'kicking' the other user. To do so, the user performing the kick MUST

@ -10,3 +10,4 @@ weight: 60
* [Room Version 4](v4) * [Room Version 4](v4)
* [Room Version 5](v5) * [Room Version 5](v5)
* [Room Version 6](v6) * [Room Version 6](v6)
* [Room Version 7](v7)

@ -0,0 +1,52 @@
---
title: Room Version 7
type: docs
weight: 60
---
This room version builds on [version 6](/rooms/v6) to introduce knocking
as a possible join rule and membership state.
## Client considerations
This is the first room version to support knocking completely. As such,
users will not be able to knock on rooms which are not based off v7.
## Server implementation components
{{% boxes/warning %}}
The information contained in this section is strictly for server
implementors. Applications which use the Client-Server API are generally
unaffected by the intricacies contained here. The section above
regarding client considerations is the resource that Client-Server API
use cases should reference.
{{% /boxes/warning %}}
Room version 7 adds new authorization rules for events to support knocking.
[Room version 6](/rooms/v6) has details of other authorization rule changes,
as do the versions v6 is based upon.
For checks perfomed upon `m.room.member` events, the following conditions
are added in context:
If type is `m.room.member`:
...
* If `membership` is `ban`:
...
* If `membership` is `knock`:
i. If the `join_rule` is anything other than `knock`, reject.
ii. If `sender` does not match `state_key`, reject.
iii. If the `sender`'s current membership is not `ban`, `invite`, or `join`, allow.
iv. Otherwise, reject.
...
The remaining rules are the same as in [room version 6](/rooms/v6#authorization-rules-for-events).

@ -18,7 +18,7 @@ signatures in HTTP Authorization headers at the HTTP layer.
There are three main kinds of communication that occur between There are three main kinds of communication that occur between
homeservers: homeservers:
Persisted Data Units (PDUs): Persisted Data Units (PDUs):
These events are broadcast from one homeserver to any others that have These events are broadcast from one homeserver to any others that have
joined the same room (identified by Room ID). They are persisted in joined the same room (identified by Room ID). They are persisted in
long-term storage and record the history of messages and state for a long-term storage and record the history of messages and state for a
@ -29,12 +29,12 @@ to deliver that event to its recipient servers. However PDUs are signed
using the originating server's private key so that it is possible to using the originating server's private key so that it is possible to
deliver them through third-party servers. deliver them through third-party servers.
Ephemeral Data Units (EDUs): Ephemeral Data Units (EDUs):
These events are pushed between pairs of homeservers. They are not These events are pushed between pairs of homeservers. They are not
persisted and are not part of the history of a room, nor does the persisted and are not part of the history of a room, nor does the
receiving homeserver have to reply to them. receiving homeserver have to reply to them.
Queries: Queries:
These are single request/response interactions between a given pair of These are single request/response interactions between a given pair of
servers, initiated by one side sending an HTTPS GET request to obtain servers, initiated by one side sending an HTTPS GET request to obtain
some information, and responded by the other. They are not persisted and some information, and responded by the other. They are not persisted and
@ -365,19 +365,19 @@ them.
#### Definitions #### Definitions
Required Power Level Required Power Level
A given event type has an associated *required power level*. This is A given event type has an associated *required power level*. This is
given by the current `m.room.power_levels` event. The event type is given by the current `m.room.power_levels` event. The event type is
either listed explicitly in the `events` section or given by either either listed explicitly in the `events` section or given by either
`state_default` or `events_default` depending on if the event is a state `state_default` or `events_default` depending on if the event is a state
event or not. event or not.
Invite Level, Kick Level, Ban Level, Redact Level Invite Level, Kick Level, Ban Level, Redact Level
The levels given by the `invite`, `kick`, `ban`, and `redact` properties The levels given by the `invite`, `kick`, `ban`, and `redact` properties
in the current `m.room.power_levels` state. Each defaults to 50 if in the current `m.room.power_levels` state. Each defaults to 50 if
unspecified. unspecified.
Target User Target User
For an `m.room.member` state event, the user given by the `state_key` of For an `m.room.member` state event, the user given by the `state_key` of
the event. the event.
@ -720,6 +720,24 @@ other servers participating in the room.
{{% http-api spec="server-server" api="joins-v2" %}} {{% http-api spec="server-server" api="joins-v2" %}}
## Knocking upon a room
Rooms can permit knocking through the join rules, and if permitted this
gives users a way to request to join (be invited) to the room. Users who
knock on a room where the server is already a resident of the room can
just send the knock event directly without using this process, however
much like [joining rooms](/server-server-api/#joining-rooms) the server
must handshake their way into having the knock sent on its behalf.
The handshake is largely the same as the joining rooms handshake, where
instead of a "joining server" there is a "knocking server", and the APIs
to be called are different (`/make_knock` and `/send_knock`).
Servers can retract knocks over federation by leaving the room, as described
below for rejecting invites.
{{% http-api spec="server-server" api="knocks" %}}
## Inviting to a room ## Inviting to a room
When a user on a given homeserver invites another user on the same When a user on a given homeserver invites another user on the same
@ -728,6 +746,10 @@ the process defined here. However, when a user invites another user on a
different homeserver, a request to that homeserver to have the event different homeserver, a request to that homeserver to have the event
signed and verified must be made. signed and verified must be made.
Note that invites are used to indicate that knocks were accepted. As such,
receiving servers should be prepared to manually link up a previous knock
to an invite if the invite event does not directly reference the knock.
{{% http-api spec="server-server" api="invites-v1" %}} {{% http-api spec="server-server" api="invites-v1" %}}
{{% http-api spec="server-server" api="invites-v2" %}} {{% http-api spec="server-server" api="invites-v2" %}}
@ -735,10 +757,10 @@ signed and verified must be made.
## Leaving Rooms (Rejecting Invites) ## Leaving Rooms (Rejecting Invites)
Normally homeservers can send appropriate `m.room.member` events to have Normally homeservers can send appropriate `m.room.member` events to have
users leave the room, or to reject local invites. Remote invites from users leave the room, to reject local invites, or to retract a knock.
other homeservers do not involve the server in the graph and therefore Remote invites/knocks from other homeservers do not involve the server in the
need another approach to reject the invite. Joining the room and graph and therefore need another approach to reject the invite. Joining
promptly leaving is not recommended as clients and servers will the room and promptly leaving is not recommended as clients and servers will
interpret that as accepting the invite, then leaving the room rather interpret that as accepting the invite, then leaving the room rather
than rejecting the invite. than rejecting the invite.
@ -1009,6 +1031,8 @@ The following endpoint prefixes MUST be protected:
- `/_matrix/federation/v2/send_leave` - `/_matrix/federation/v2/send_leave`
- `/_matrix/federation/v1/invite` - `/_matrix/federation/v1/invite`
- `/_matrix/federation/v2/invite` - `/_matrix/federation/v2/invite`
- `/_matrix/federation/v1/make_knock`
- `/_matrix/federation/v1/send_knock`
- `/_matrix/federation/v1/state` - `/_matrix/federation/v1/state`
- `/_matrix/federation/v1/state_ids` - `/_matrix/federation/v1/state_ids`
- `/_matrix/federation/v1/backfill` - `/_matrix/federation/v1/backfill`

@ -1,4 +1,4 @@
# Copyright 2018 New Vector Ltd # Copyright 2018, 2021 The Matrix.org Foundation C.I.C.
# #
# Licensed under the Apache License, Version 2.0 (the "License"); # Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License. # you may not use this file except in compliance with the License.
@ -69,6 +69,13 @@ properties:
avatar_url: avatar_url:
type: string type: string
description: The URL for the room's avatar, if one is set. description: The URL for the room's avatar, if one is set.
join_rule:
type: string
description: |-
The room's join rule. When not present, the room is assumed to
be `public`. Note that rooms with `invite` join rules are not
expected here, but rooms with `knock` rules are given their
near-public nature.
next_batch: next_batch:
type: string type: string
description: |- description: |-
@ -96,7 +103,8 @@ example: {
"num_joined_members": 37, "num_joined_members": 37,
"room_id": "!ol19s:bleecker.street", "room_id": "!ol19s:bleecker.street",
"topic": "Tasty tasty cheese", "topic": "Tasty tasty cheese",
"world_readable": true "world_readable": true,
"join_rule": "public"
} }
], ],
"next_batch": "p190q", "next_batch": "p190q",

@ -0,0 +1,124 @@
# Copyright 2021 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.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
swagger: '2.0'
info:
title: "Matrix Client-Server Room Knocking API"
version: "1.0.0"
host: localhost:8008
schemes:
- https
- http
basePath: /_matrix/client/%CLIENT_MAJOR_VERSION%
consumes:
- application/json
produces:
- application/json
securityDefinitions:
$ref: definitions/security.yaml
paths:
"/knock/{roomIdOrAlias}":
post:
summary: Knock on a room, requesting permission to join.
description: |-
*Note that this API takes either a room ID or alias, unlike other membership APIs.*
This API "knocks" on the room to ask for permission to join, if the user
is allowed to knock on the room. Acceptance of the knock happens out of
band from this API, meaning that the client will have to watch for updates
regarding the acceptance/rejection of the knock.
If the room history settings allow, the user will still be able to see
history of the room while being in the "knock" state. The user will have
to accept the invitation to join the room (acceptance of knock) to see
messages reliably. See the `/join` endpoints for more information about
history visibility to the user.
The knock will appear as an entry in the response of the
[`/sync`](/client-server-api/#get_matrixclientr0sync) API.
operationId: knockRoom
security:
- accessToken: []
parameters:
- in: path
type: string
name: roomIdOrAlias
description: The room identifier or alias to knock upon.
required: true
x-example: "#monkeys:matrix.org"
- in: query
type: array
items:
type: string
name: server_name
description: |-
The servers to attempt to knock on the room through. One of the servers
must be participating in the room.
x-example: ["matrix.org", "elsewhere.ca"]
- in: body
name: body
required: true
schema:
type: object
properties:
reason:
type: string
description: |-
Optional reason to be included as the `reason` on the subsequent
membership event.
example: "Looking for support"
responses:
200:
description: |-
The room has been knocked upon.
The knocked room ID must be returned in the `room_id` field.
examples:
application/json: {
"room_id": "!d41d8cd:matrix.org"
}
schema:
type: object
properties:
room_id:
type: string
description: The knocked room ID.
required: ["room_id"]
403:
description: |-
You do not have permission to knock on the room. A meaningful `errcode`
and description error text will be returned. Example reasons for rejection are:
- The room is not set up for knocking.
- The user has been banned from the room.
examples:
application/json: {
"errcode": "M_FORBIDDEN", "error": "You are not allowed to knock on this room."
}
schema:
"$ref": "definitions/errors/error.yaml"
404:
description: |-
The room could not be found or resolved to a room ID.
examples:
application/json: {
"errcode": "M_NOT_FOUND", "error": "That room does not appear to exist."
}
schema:
"$ref": "definitions/errors/error.yaml"
429:
description: This request was rate-limited.
schema:
"$ref": "definitions/errors/rate_limited.yaml"
tags:
- Room membership

@ -1,4 +1,4 @@
# Copyright 2016 OpenMarket Ltd # Copyright 2016, 2021 The Matrix.org Foundation C.I.C.
# #
# Licensed under the Apache License, Version 2.0 (the "License"); # Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License. # you may not use this file except in compliance with the License.
@ -281,6 +281,28 @@ paths:
items: items:
$ref: "../../event-schemas/schema/stripped_state.yaml" $ref: "../../event-schemas/schema/stripped_state.yaml"
type: array type: array
knock:
title: Knocked rooms
type: object
description: |-
The rooms that the user has knocked upon, mapped as room ID to room information.
additionalProperties:
title: Knocked Room
type: object
properties:
knock_state:
title: KnockState
type: object
description: |-
The state of a room that the user has knocked upon. The state
events contained here have the same restrictions as `InviteState`
above.
properties:
events:
description: The StrippedState events that form the knock state.
items:
$ref: "../../event-schemas/schema/stripped_state.yaml"
type: array
leave: leave:
title: Left rooms title: Left rooms
type: object type: object
@ -436,6 +458,26 @@ paths:
} }
} }
}, },
"knock": {
"!223asd456:example.com": {
"invite_state": {
"events": [
{
"sender": "@alice:example.com",
"type": "m.room.name",
"state_key": "",
"content": {"name": "My Room Name"}
},
{
"sender": "@bob:example.com",
"type": "m.room.member",
"state_key": "@bob:example.com",
"content": {"membership": "knock"}
}
]
}
}
},
"leave": {} "leave": {}
} }
} }

@ -0,0 +1,321 @@
# Copyright 2021 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.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
swagger: '2.0'
info:
title: "Matrix Federation Knock Room API"
version: "1.0.0"
host: localhost:8448
schemes:
- https
basePath: /_matrix/federation/v1
consumes:
- application/json
produces:
- application/json
securityDefinitions:
$ref: definitions/security.yaml
paths:
"/make_knock/{roomId}/{userId}":
get:
summary: Get information required to make a knock event for a room.
description: |-
Asks the receiving server to return information that the sending
server will need to prepare a knock event for the room.
operationId: makeKnock
security:
- signedRequest: []
parameters:
- in: path
name: roomId
type: string
description: The room ID that is about to be knocked.
required: true
x-example: "!abc123:example.org"
- in: path
name: userId
type: string
description: The user ID the knock event will be for.
required: true
x-example: "@someone:example.org"
- in: query
type: array
items:
type: string
name: ver
description: |-
The room versions the sending server has support for.
required: true # knocking was supported in v7
x-example: ["1", "7"]
responses:
200:
description: |-
A template to be used for the rest of the [KNocking Rooms](/server-server-api/#knocking-rooms)
handshake. Note that events have a different format depending on room version - check the
[room version specification](/#room-versions) for precise event formats. **The response body
here describes the common event fields in more detail and may be missing other
required fields for a PDU.**
schema:
type: object
properties:
room_version:
type: string
required: true # knocking was supported in v7
description: |-
The version of the room where the server is trying to knock.
example: "7"
event:
description: |-
An unsigned template event. Note that events have a different format
depending on the room version - check the [room version specification](/#room-versions)
for precise event formats.
type: object
title: Event Template
properties:
sender:
type: string
description: The user ID of the knocking member.
example: "@someone:example.org"
origin:
type: string
description: The name of the resident homeserver.
example: "matrix.org"
origin_server_ts:
type: integer
format: int64
description: A timestamp added by the resident homeserver.
example: 1234567890
type:
type: string
description: The value `m.room.member`.
example: "m.room.member"
state_key:
type: string
description: The user ID of the knocking member.
example: "@someone:example.org"
content:
type: object
title: Membership Event Content
description: The content of the event.
example: {"membership": "knock"}
properties:
membership:
type: string
description: The value `knock`.
example: "knock"
required: ['membership']
required:
- state_key
- origin
- origin_server_ts
- type
- content
- sender
examples:
application/json: {
"room_version": "7",
"event": {
"$ref": "examples/minimal_pdu.json",
"type": "m.room.member",
"state_key": "@someone:example.org",
"origin": "example.org",
"origin_server_ts": 1549041175876,
"sender": "@someone:example.org",
"content": {
"membership": "knock"
}
}
}
400:
description: |-
The request is invalid or the room the server is attempting
to knock upon has a version that is not listed in the `ver`
parameters.
The error should be passed through to clients so that they
may give better feedback to users.
schema:
allOf:
- $ref: "../client-server/definitions/errors/error.yaml"
- type: object
properties:
room_version:
type: string
description: |-
The version of the room. Required if the `errcode`
is `M_INCOMPATIBLE_ROOM_VERSION`.
examples:
application/json: {
"errcode": "M_INCOMPATIBLE_ROOM_VERSION",
"error": "Your homeserver does not support the features required to knock on this room",
"room_version": "7"
}
404:
description: |-
The room that the knocking server is attempting to knock upon is unknown
to the receiving server.
schema:
allOf:
- $ref: "../client-server/definitions/errors/error.yaml"
examples:
application/json: {
"errcode": "M_NOT_FOUND",
"error": "Unknown room",
}
403:
description: |-
The knocking server or user is not permitted to knock on the room, such as when the
server/user is banned or the room is not set up for receiving knocks.
schema:
allOf:
- $ref: "../client-server/definitions/errors/error.yaml"
examples:
application/json: {
"errcode": "M_FORBIDDEN",
"error": "You are not permitted to knock on this room",
}
"/send_knock/{roomId}/{eventId}":
put:
summary: Submit a signed knock event to a resident server.
description: |-
Submits a signed knock event to the resident server for it to
accept into the room's graph. Note that events have
a different format depending on the room version - check
the [room version specification](/#room-versions) for precise event formats.
**The request and response body here describe the common
event fields in more detail and may be missing other required
fields for a PDU.**
operationId: sendKnock
security:
- signedRequest: []
parameters:
- in: path
name: roomId
type: string
description: The room ID that is about to be knocked upon.
required: true
x-example: "!abc123:example.org"
- in: path
name: eventId
type: string
description: The event ID for the knock event.
required: true
x-example: "$abc123:example.org"
- in: body
name: body
type: object
required: true
schema:
type: object
properties:
sender:
type: string
description: The user ID of the knocking member.
example: "@someone:example.org"
origin:
type: string
description: The name of the knocking homeserver.
example: "matrix.org"
origin_server_ts:
type: integer
format: int64
description: A timestamp added by the knocking homeserver.
example: 1234567890
type:
type: string
description: The value `m.room.member`.
example: "m.room.member"
state_key:
type: string
description: The user ID of the knocking member.
example: "@someone:example.org"
content:
type: object
title: Membership Event Content
description: The content of the event.
example: {"membership": "knock"}
properties:
membership:
type: string
description: The value `knock`.
example: "knock"
required: ['membership']
required:
- state_key
- sender
- origin
- origin_server_ts
- type
- content
example: {
"$ref": "examples/minimal_pdu.json",
"type": "m.room.member",
"state_key": "@someone:example.org",
"origin": "example.org",
"origin_server_ts": 1549041175876,
"sender": "@someone:example.org",
"content": {
"membership": "knock"
}
}
responses:
200:
description: |-
Information about the room to pass along to clients regarding the
knock.
schema:
type: object
properties:
knock_room_state:
type: array
items:
$ref: "../../event-schemas/schema/stripped_state.yaml"
description: |-
A list of simplified events to help the initiator of the knock identify
the room. The recommended events to include are the join rules, canonical
alias, avatar, name, and encryption state of the room.
example:
$ref: "../../event-schemas/examples/knock_room_state.json"
required: ['knock_room_state']
examples:
application/json: {
"knock_room_state": {"$ref": "../../event-schemas/examples/knock_room_state.json"}
}
404:
description: |-
The room that the knocking server is attempting to knock upon is unknown
to the receiving server.
schema:
allOf:
- $ref: "../client-server/definitions/errors/error.yaml"
examples:
application/json: {
"errcode": "M_NOT_FOUND",
"error": "Unknown room",
}
403:
description: |-
The knocking server or user is not permitted to knock on the room, such as when the
server/user is banned or the room is not set up for receiving knocks.
schema:
allOf:
- $ref: "../client-server/definitions/errors/error.yaml"
examples:
application/json: {
"errcode": "M_FORBIDDEN",
"error": "You are not permitted to knock on this room",
}

@ -1,4 +1,4 @@
# Copyright 2018 New Vector Ltd # Copyright 2018, 2021 The Matrix.org Foundation C.I.C.
# #
# Licensed under the Apache License, Version 2.0 (the "License"); # Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License. # you may not use this file except in compliance with the License.
@ -193,6 +193,13 @@ paths:
avatar_url: avatar_url:
type: string type: string
description: The URL for the room's avatar, if one is set. description: The URL for the room's avatar, if one is set.
join_rule:
type: string
description: |-
The room's join rule. When not present, the room is assumed to
be `public`. Note that rooms with `invite` join rules are not
expected here, but rooms with `knock` rules are given their
near-public nature.
next_batch: next_batch:
type: string type: string
description: |- description: |-
@ -221,7 +228,8 @@ paths:
"num_joined_members": 37, "num_joined_members": 37,
"room_id": "!ol19s:bleecker.street", "room_id": "!ol19s:bleecker.street",
"topic": "Tasty tasty cheese", "topic": "Tasty tasty cheese",
"world_readable": true "world_readable": true,
"join_rule": "public"
} }
], ],
"next_batch": "p190q", "next_batch": "p190q",

@ -0,0 +1,18 @@
[
{
"type": "m.room.name",
"sender": "@bob:example.org",
"state_key": "",
"content": {
"name": "Example Room"
}
},
{
"type": "m.room.join_rules",
"sender": "@bob:example.org",
"state_key": "",
"content": {
"join_rule": "knock"
}
}
]

@ -0,0 +1,15 @@
{
"$ref": "m.room.member.yaml",
"content": {
"membership": "knock",
"avatar_url": "mxc://example.org/SEsfnsuifSDFSSEF",
"displayname": "Alice Margatroid",
"reason": "Looking for support"
},
"unsigned": {
"age": 1234,
"knock_room_state": {
"$ref": "knock_room_state.json"
}
}
}

@ -1,7 +1,16 @@
--- ---
allOf: allOf:
- $ref: core-event-schema/state_event.yaml - $ref: core-event-schema/state_event.yaml
description: 'A room may be `public` meaning anyone can join the room without any prior action. Alternatively, it can be `invite` meaning that a user who wishes to join the room must first receive an invite to the room from someone already inside of the room. Currently, `knock` and `private` are reserved keywords which are not implemented.' description: |
A room may be `public` meaning anyone can join the room without any prior action.
Alternatively, it can be `invite` meaning that a user who wishes to join the room
must first receive an invite to the room from someone already inside of the room.
`knock` means that users are able to ask for permission to join the room, where
they are either allowed (invited) or denied (kicked/banned) access. Join rules
of `knock` are otherwise the same as `invite`: the user needs an explicit invite
to join the room.
Currently, `private` is a reserved keyword which is not implemented.
properties: properties:
content: content:
properties: properties:

@ -14,7 +14,7 @@ description: |-
- `ban` - The user has been banned from the room, and is no longer allowed to join it until they are un-banned from the room (by having their membership state set to a value other than `ban`). - `ban` - The user has been banned from the room, and is no longer allowed to join it until they are un-banned from the room (by having their membership state set to a value other than `ban`).
- `knock` - This is a reserved word, which currently has no meaning. - `knock` - The user has knocked on the room, requesting permission to participate. They may not participate in the room until they join.
The `third_party_invite` property will be set if this invite is an `invite` event and is the successor of an `m.room.third_party_invite` event, and absent otherwise. The `third_party_invite` property will be set if this invite is an `invite` event and is the successor of an `m.room.third_party_invite` event, and absent otherwise.
@ -31,13 +31,13 @@ description: |-
from the `prev_content` object on an event. If not present, the user's previous membership must be assumed from the `prev_content` object on an event. If not present, the user's previous membership must be assumed
as `leave`. as `leave`.
| | to `invite` | to `join` | to `leave` | to `ban` | to `knock` | | | to `invite` | to `join` | to `leave` | to `ban` | to `knock` |
|-------------------|---------------------|----------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------|-----------------------------|------------------| |-------------------|----------------------|----------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------|-----------------------------|--------------------|
| **from `invite`** | No change. | User joined the room. | If the `state_key` is the same as the `sender`, the user rejected the invite. Otherwise, the `state_key` user had their invite revoked. | User was banned. | Not implemented. | | **from `invite`** | No change. | User joined the room. | If the `state_key` is the same as the `sender`, the user rejected the invite. Otherwise, the `state_key` user had their invite revoked. | User was banned. | Must never happen. |
| **from `join`** |Must never happen. | `displayname` or `avatar_url` changed. | If the `state_key` is the same as the `sender`, the user left. Otherwise, the `state_key` user was kicked. | User was kicked and banned. | Not implemented. | | **from `join`** | Must never happen. | `displayname` or `avatar_url` changed. | If the `state_key` is the same as the `sender`, the user left. Otherwise, the `state_key` user was kicked. | User was kicked and banned. | Must never happen. |
| **from `leave`** |New invitation sent. | User joined. | No change. | User was banned. | Not implemented. | | **from `leave`** | New invitation sent. | User joined. | No change. | User was banned. | User is knocking. |
| **from `ban`** |Must never happen. | Must never happen. | User was unbanned. | No change. | Not implemented. | | **from `ban`** | Must never happen. | Must never happen. | User was unbanned. | No change. | Must never happen. |
| **from `knock`** |Not implemented. | Not implemented. | Not implemented. | Not implemented. | Not implemented. | | **from `knock`** | Knock accepted. | Must never happen. | If the `state_key` is the same as the `sender`, the user retracted the knock. Otherwise, the `state_key` user had their knock denied. | User was banned. | No change. |
properties: properties:
content: content:
@ -124,7 +124,24 @@ properties:
- type: object - type: object
properties: properties:
invite_room_state: invite_room_state:
description: 'A subset of the state of the room at the time of the invite, if `membership` is `invite`. Note that this state is informational, and SHOULD NOT be trusted; once the client has joined the room, it SHOULD fetch the live state from the server and discard the invite_room_state. Also, clients must not rely on any particular state being present here; they SHOULD behave properly (with possibly a degraded but not a broken experience) in the absence of any particular events here. If they are set on the room, at least the state for `m.room.avatar`, `m.room.canonical_alias`, `m.room.join_rules`, and `m.room.name` SHOULD be included.' description: |-
A subset of the state of the room at the time of the invite, if `membership` is `invite`.
Note that this state is informational, and SHOULD NOT be trusted; once the client has
joined the room, it SHOULD fetch the live state from the server and discard the
invite_room_state. Also, clients must not rely on any particular state being present here;
they SHOULD behave properly (with possibly a degraded but not a broken experience) in
the absence of any particular events here. If they are set on the room, at least the
state for `m.room.avatar`, `m.room.canonical_alias`, `m.room.join_rules`, and `m.room.name`
SHOULD be included.
items:
$ref: "stripped_state.yaml"
type: array
knock_room_state:
description: |-
A subset of the state of the room at the time of the knock, if `membership` is `knock`.
This has the same restrictions as `invite_room_state`. If they are set on the room, at least
the state for `m.room.avatar`, `m.room.canonical_alias`, `m.room.join_rules`, `m.room.name`,
and `m.room.encryption` SHOULD be included.
items: items:
$ref: "stripped_state.yaml" $ref: "stripped_state.yaml"
type: array type: array

@ -0,0 +1,17 @@
# Spec diagrams
Non-ascii diagrams for the spec can be placed here for reference in the actual spec.
Please include source material so the diagram can be recreated by a future editor.
https://www.diagrams.net/ is a great ([open source](https://github.com/jgraph/drawio))
tool for these sorts of things - include your `.drawio` file next to your diagram.
Suggested settings for diagrams.net:
* Export as PNG.
* 100% size.
* `20` for a border width.
* No transparent background, shadow, or grid.
* Include a copy of the diagram.
To reference a diagram, use the absolute path when compiled. For example,
`![membership-flow-diagram](/diagrams/membership.png)`

@ -0,0 +1 @@
<mxfile host="app.diagrams.net" modified="2021-04-28T19:35:50.494Z" agent="5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.85 Safari/537.36" etag="-IOh23FjJiPnGlGWLseU" version="14.6.6" type="device"><diagram id="4a_pTli-mcEMNPq0ciXK" name="Page-1">3Vvdb6M4EP9rIt09NMIGA3ncZtvbh73TSn243X1ZOYmT0BIcOaZJ9q8/E0z4cggEgulVqoqHsbHHv/nwjDsyp5vDXwxv13/TBfFH0FgcRubnEYTAAa74E1GOMWViopiwYt5CMqWEF+83kURDUkNvQXY5Rk6pz71tnjinQUDmPEfDjNF9nm1J/fxXt3hFSoSXOfbL1H+9BV/HVBc6Kf0L8Vbr5MvAnsRvNjhhlivZrfGC7jMk82lkThmlPH7aHKbEj4SXyCXu93zh7XlijAS8TodvBvr96euj/xL83G+Dn2gaTvgDkMO8Yz+UKxYjvFIvkJPmx0QSZCEEI5uU8TVd0QD7Tyn1kdEwWJDoc4ZopTxfKd0KIhDEV8L5Ue4yDjkVpDXf+PJt/M3oQxfXKEk7GrI5qVqYxApmK8Ir+OB5JwSECd0Qzo6iHyM+5t57fh5YYml15pNdPzGGjxmGrZAf32VG/hYRBINUCziRmJBKYdqFrWvGLx7iGSStzFJS0gkOTaBhqaDhEyykogEb5ODx75nnH9FQY4hk8/NBDn1qHJNGIGTwPcMZtX9kX6b9Tq2kY4dIhKgmFC01FCUGjDEwHDMeqhk6S3Cy3DycoDUZ25kfx8qPGM9cDpLCrinqLVD4rG1Wot4CqIo/j/q0dzIdulzuCC/06EYzSorhBe8eL6tFHvT7teB52eITWvbCXaqM3zthnByqQXcRI9ApSBjJ9j51XcCQtHXGbSV8KgzlpNdYVHaFETEoG0VO6/nNm799cH8DO1JysRduOyW/P/4hVG3qDGuJGRK/YIxRxjOA29wC6NEt1ASMWQmYB2NsI6ulW+gBMSXAKEPMRvZySQP+jDeeH23OVIjbI5E5+Yfs72NMLUu7MXVVehcGujUvF5HVDMhAVu1kr34Uz2xnqWsrWautNks7rdrkQekLQoVDiqldX5R+KrY8hreMVhfOfG8+grYv5vI4E/KwV9FTxPOLhT7Z6XVp8Caf5ug66lg1VavtobsdKsq6Jca5EMH3sduXNq7HYKT2xqFr0QgaeigCnEEGr05W0426mq4tqVEXMBei1340vZzAUmevBuVGzWKuT7sbVUadGu1lO53RFnfCSWdG1rVdq52dPddnCgkjB+WHiFdVSv31arCNjx3F5RPW1ccjLXBraaLrIcuy6yGrs4yDsrCly82nHrtRhHdLbksHhK7mqFxogrsYLGTVg1XTWgUo1CqQWV2hs41Kfn21CqhMwA/Ae9+SNGp6tM3Fal+I/064N8cdK0lHZycRzN+iIU2BbVr5ohqYoIEAVXkuG0qlqGWV4eOitrpeJUy7A4ZffjAHFQzcEBf+L6B0NUpwEGoZJfRgpspJhbeAKozSoJMK5zBKW1IhuRXT8SWKvouC+m9YJEF6RpBPB06YMEknl1k8FP8R0JOmiN8N2cwI+7NC4qChxMsmpl1JqXCGVKXCoELY9r2EbSpzEdIAxMmIU+MBz4QwB5qQKJSVaiebu/E8r+Fmmyw0oAFJSPGsHV1JkKtxjuvCm3JuTQP0wt08YFUfPM+3EtT8Gi/Jle2S/pjrVug3O3ZmIY7ZvGNI171OejXcmvRy4ISTwoETXcmkwEr+ewBaNNNr+DF7+s8M5tN/</diagram></mxfile>

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Loading…
Cancel
Save