diff --git a/api/client-server/account-data.yaml b/api/client-server/account-data.yaml new file mode 100644 index 00000000..b634dddc --- /dev/null +++ b/api/client-server/account-data.yaml @@ -0,0 +1,105 @@ +swagger: '2.0' +info: + title: "Matrix Client-Server Client Config API" + version: "1.0.0" +host: localhost:8008 +schemes: + - https + - http +basePath: /_matrix/client/v2_alpha +consumes: + - application/json +produces: + - application/json +securityDefinitions: + accessToken: + type: apiKey + description: The user_id or application service access_token + name: access_token + in: query +paths: + "/user/{userId}/account_data/{type}": + put: + summary: Set some account_data for the user. + description: |- + Set some account_data for the client. This config is only visible to the user + that set the account_data. The config will be synced to clients in the + top-level ``account_data``. + security: + - accessToken: [] + parameters: + - in: path + type: string + name: userId + required: true + description: |- + The id of the user to set account_data for. The access token must be + authorized to make requests for this user id. + x-example: "@alice:example.com" + - in: path + type: string + name: type + required: true + description: |- + The event type of the account_data to set. Custom types should be + namespaced to avoid clashes. + x-example: "org.example.custom.config" + - in: body + name: content + required: true + description: |- + The content of the account_data + schema: + type: object + example: |- + {"custom_account_data_key": "custom_config_value"} + responses: + 200: + description: + The account_data was successfully added. + "/user/{userId}/rooms/{roomId}/account_data/{type}": + put: + summary: Set some account_data for the user. + description: |- + Set some account_data for the client on a given room. This config is only + visible to the user that set the account_data. The config will be synced to + clients in the per-room ``account_data``. + security: + - accessToken: [] + parameters: + - in: path + type: string + name: userId + required: true + description: |- + The id of the user to set account_data for. The access token must be + authorized to make requests for this user id. + x-example: "@alice:example.com" + - in: path + type: string + name: roomId + required: true + description: |- + The id of the room to set account_data on. + x-example: "!726s6s6q:example.com" + - in: path + type: string + name: type + required: true + description: |- + The event type of the account_data to set. Custom types should be + namespaced to avoid clashes. + x-example: "org.example.custom.room.config" + - in: body + name: content + required: true + description: |- + The content of the account_data + schema: + type: object + example: |- + {"custom_account_data_key": "custom_account_data_value"} + responses: + 200: + description: + The account_data was successfully added. diff --git a/api/client-server/administrative_contact.yaml b/api/client-server/administrative_contact.yaml new file mode 100644 index 00000000..68fd0fba --- /dev/null +++ b/api/client-server/administrative_contact.yaml @@ -0,0 +1,157 @@ +swagger: '2.0' +info: + title: "Matrix Client-Server v1 Account Administrative Contact API" + version: "1.0.0" +host: localhost:8008 +schemes: + - https + - http +basePath: /_matrix/client/v2_alpha +consumes: + - application/json +produces: + - application/json +securityDefinitions: + accessToken: + type: apiKey + description: The user_id or application service access_token + name: access_token + in: query +paths: + "/account/password": + post: + summary: Changes a user's password. + description: |- + This API endpoint uses the User-Interactive Authentication API. + An access token should be submitted to this endpoint if the client has + an active session. + The Home Server may change the flows available depending on whether a + valid access token is provided. + security: + - accessToken: [] + parameters: + - in: body + name: body + schema: + type: object + example: |- + { + "new_password": "ihatebananas" + } + properties: + new_password: + type: string + description: The new password for the account. + required: ["new_password"] + responses: + 200: + description: The password has been changed. + examples: + application/json: "{}" + schema: + type: object + 429: + description: This request was rate-limited. + schema: + "$ref": "definitions/error.yaml" + "/account/3pid": + get: + summary: Gets a list of a user's third party identifiers. + description: |- + Gets a list of the third party identifiers that the homeserver has + associated with the user's account. + + This is *not* the same as the list of third party identifiers bound to + the user's Matrix ID in Identity Servers. + + Identifiers in this list may be used by the Home Server as, for example, + identifiers that it will accept to reset the user's account password. + security: + - accessToken: [] + responses: + 200: + description: The lookup was successful. + examples: + application/json: |- + { + "threepids": [ + { + "medium": "email", + "address": "monkey@banana.island" + } + ] + } + schema: + type: object + properties: + threepids: + type: array + items: + type: object + title: Third party identifier + properties: + medium: + type: string + description: The medium of the third party identifier. + enum: ["email"] + address: + type: string + description: The third party identifier address. + post: + summary: Adds contact information to the user's account. + description: Adds contact information to the user's account. + security: + - accessToken: [] + parameters: + - in: body + name: body + schema: + type: object + properties: + threePidCreds: + title: "ThreePidCredentials" + type: object + description: The third party credentials to associate with the account. + properties: + client_secret: + type: string + description: The client secret used in the session with the Identity Server. + id_server: + type: string + description: The Identity Server to use. + sid: + type: string + description: The session identifier given by the Identity Server. + required: ["client_secret", "id_server", "sid"] + bind: + type: boolean + description: |- + Whether the home server should also bind this third party + identifier to the account's Matrix ID with the passed identity + server. Default: ``false``. + x-example: true + required: ["threePidCreds"] + example: |- + { + "threePidCreds": { + "id_server": "matrix.org", + "sid": "abc123987", + "client_secret": "d0n'tT3ll" + }, + "bind": false + } + responses: + 200: + description: The addition was successful. + examples: + application/json: "{}" + schema: + type: object + 403: + description: The credentials could not be verified with the identity server. + examples: + application/json: |- + { + "errcode": "M_THREEPID_AUTH_FAILED", + "error": "The third party credentials could not be verified by the identity server." + } diff --git a/api/client-server/api-docs b/api/client-server/api-docs deleted file mode 100644 index 9c8c6066..00000000 --- a/api/client-server/api-docs +++ /dev/null @@ -1,50 +0,0 @@ -{ - "apiVersion": "1.0.0", - "swaggerVersion": "1.2", - "apis": [ - { - "path": "-login", - "description": "Login operations" - }, - { - "path": "-registration", - "description": "Registration operations" - }, - { - "path": "-rooms", - "description": "Room operations" - }, - { - "path": "-profile", - "description": "Profile operations" - }, - { - "path": "-presence", - "description": "Presence operations" - }, - { - "path": "-events", - "description": "Event operations" - }, - { - "path": "-directory", - "description": "Directory operations" - }, - { - "path": "-content", - "description": "Content repository operations" - } - ], - "authorizations": { - "token": { - "scopes": [] - } - }, - "info": { - "title": "Matrix Client-Server API Reference", - "description": "This contains the client-server API for the reference implementation of the home server", - "termsOfServiceUrl": "http://matrix.org", - "license": "Apache 2.0", - "licenseUrl": "http://www.apache.org/licenses/LICENSE-2.0.html" - } -} diff --git a/api/client-server/api-docs-content b/api/client-server/api-docs-content deleted file mode 100644 index a0a31da0..00000000 --- a/api/client-server/api-docs-content +++ /dev/null @@ -1,119 +0,0 @@ -{ - "apiVersion": "1.0.0", - "swaggerVersion": "1.2", - "basePath": "http://localhost:8008/_matrix", - "resourcePath": "/media/v1/", - "apis": [ - { - "path": "/media/v1/upload", - "operations": [ - { - "method": "POST", - "summary": "Upload some content to the content repository.", - "type": "ContentUploadResponse", - "nickname": "upload_content", - "parameters": [ - { - "name": "body", - "description": "The file to upload.", - "required": true, - "type": "file", - "paramType": "body" - } - ] - } - ] - }, - { - "path": "/media/v1/download/{serverName}/{mediaId}", - "operations": [ - { - "method": "GET", - "summary": "Get the content stored at this address.", - "type": "file", - "nickname": "download_content", - "parameters": [ - { - "name": "serverName", - "description": "The serverName from the mxc:/// URI (the authority component).", - "required": true, - "type": "string", - "paramType": "path" - }, - { - "name": "mediaId", - "description": "The mediaId from the mxc:/// URI (the path component).", - "required": true, - "type": "string", - "paramType": "path" - } - ] - } - ] - }, - { - "path": "/media/v1/thumbnail/{serverName}/{mediaId}", - "operations": [ - { - "method": "GET", - "summary": "Get a thumbnail of the content stored at this address.", - "type": "file", - "nickname": "thumbnail_content", - "parameters": [ - { - "name": "serverName", - "description": "The serverName from the mxc:/// URI (the authority component).", - "required": true, - "type": "string", - "paramType": "path" - }, - { - "name": "mediaId", - "description": "The mediaId from the mxc:/// URI (the path component).", - "required": true, - "type": "string", - "paramType": "path" - }, - { - "name": "width", - "description": "The desired width of the thumbnail.", - "required": false, - "type": "integer", - "paramType": "query" - }, - { - "name": "height", - "description": "The desired height of the thumbnail.", - "required": false, - "type": "integer", - "paramType": "query" - }, - { - "name": "method", - "description": "The desired resizing method.", - "enum": [ - "crop", - "scale" - ], - "required": false, - "type": "string", - "paramType": "query" - } - ] - } - ] - } - ], - "models": { - "ContentUploadResponse": { - "id": "ContentUploadResponse", - "properties": { - "content_uri": { - "type": "string", - "description": "The mxc:// URI where this content is stored. This is of the form 'mxc://{serverName}/{mediaId}'", - "required": true - } - } - } - } -} diff --git a/api/client-server/api-docs-directory b/api/client-server/api-docs-directory deleted file mode 100644 index 5dda5806..00000000 --- a/api/client-server/api-docs-directory +++ /dev/null @@ -1,101 +0,0 @@ -{ - "apiVersion": "1.0.0", - "swaggerVersion": "1.2", - "basePath": "http://localhost:8008/_matrix/client/api/v1", - "resourcePath": "/directory", - "produces": [ - "application/json" - ], - "apis": [ - { - "path": "/directory/room/{roomAlias}", - "operations": [ - { - "method": "GET", - "summary": "Get the room ID corresponding to this room alias.", - "notes": "Volatile: This API is likely to change.", - "type": "DirectoryResponse", - "nickname": "get_room_id_for_alias", - "parameters": [ - { - "name": "roomAlias", - "description": "The room alias.", - "required": true, - "type": "string", - "paramType": "path" - } - ] - }, - { - "method": "PUT", - "summary": "Create a new mapping from room alias to room ID.", - "notes": "Volatile: This API is likely to change.", - "type": "void", - "nickname": "add_room_alias", - "parameters": [ - { - "name": "roomAlias", - "description": "The room alias to set.", - "required": true, - "type": "string", - "paramType": "path" - }, - { - "name": "body", - "description": "The room ID to set.", - "required": true, - "type": "RoomAliasRequest", - "paramType": "body" - } - ] - }, - { - "method": "DELETE", - "summary": "Removes a mapping of room alias to room ID.", - "notes": "Only privileged users can perform this action.", - "type": "void", - "nickname": "remove_room_alias", - "parameters": [ - { - "name": "roomAlias", - "description": "The room alias to remove.", - "required": true, - "type": "string", - "paramType": "path" - } - ] - } - ] - } - ], - "models": { - "DirectoryResponse": { - "id": "DirectoryResponse", - "properties": { - "room_id": { - "type": "string", - "description": "The fully-qualified room ID.", - "required": true - }, - "servers": { - "type": "array", - "items": { - "$ref": "string" - }, - "description": "A list of servers that know about this room.", - "required": true - } - } - }, - "RoomAliasRequest": { - "id": "RoomAliasRequest", - "properties": { - "room_id": { - "type": "string", - "description": "The room ID to map the alias to.", - "required": true - } - } - } - } -} diff --git a/api/client-server/api-docs-events b/api/client-server/api-docs-events deleted file mode 100644 index 1bdb9b03..00000000 --- a/api/client-server/api-docs-events +++ /dev/null @@ -1,247 +0,0 @@ -{ - "apiVersion": "1.0.0", - "swaggerVersion": "1.2", - "basePath": "http://localhost:8008/_matrix/client/api/v1", - "resourcePath": "/events", - "produces": [ - "application/json" - ], - "apis": [ - { - "path": "/events", - "operations": [ - { - "method": "GET", - "summary": "Listen on the event stream", - "notes": "This can only be done by the logged in user. This will block until an event is received, or until the timeout is reached.", - "type": "PaginationChunk", - "nickname": "get_event_stream", - "parameters": [ - { - "name": "from", - "description": "The token to stream from.", - "required": false, - "type": "string", - "paramType": "query" - }, - { - "name": "timeout", - "description": "The maximum time in milliseconds to wait for an event.", - "required": false, - "type": "integer", - "paramType": "query" - } - ] - } - ], - - "responseMessages": [ - { - "code": 400, - "message": "Bad pagination token." - } - ] - }, - { - "path": "/events/{eventId}", - "operations": [ - { - "method": "GET", - "summary": "Get information about a single event.", - "notes": "Get information about a single event.", - "type": "Event", - "nickname": "get_event", - "parameters": [ - { - "name": "eventId", - "description": "The event ID to get.", - "required": true, - "type": "string", - "paramType": "path" - } - ], - "responseMessages": [ - { - "code": 404, - "message": "Event not found." - } - ] - } - ] - }, - { - "path": "/initialSync", - "operations": [ - { - "method": "GET", - "summary": "Get this user's current state.", - "notes": "Get this user's current state.", - "type": "InitialSyncResponse", - "nickname": "initial_sync", - "parameters": [ - { - "name": "limit", - "description": "The maximum number of messages to return for each room.", - "type": "integer", - "paramType": "query", - "required": false - } - ] - } - ] - }, - { - "path": "/publicRooms", - "operations": [ - { - "method": "GET", - "summary": "Get a list of publicly visible rooms.", - "type": "PublicRoomsPaginationChunk", - "nickname": "get_public_room_list" - } - ] - } - ], - "models": { - "PaginationChunk": { - "id": "PaginationChunk", - "properties": { - "start": { - "type": "string", - "description": "A token which correlates to the first value in \"chunk\" for paginating.", - "required": true - }, - "end": { - "type": "string", - "description": "A token which correlates to the last value in \"chunk\" for paginating.", - "required": true - }, - "chunk": { - "type": "array", - "description": "An array of events.", - "required": true, - "items": { - "$ref": "Event" - } - } - } - }, - "Event": { - "id": "Event", - "properties": { - "event_id": { - "type": "string", - "description": "An ID which uniquely identifies this event.", - "required": true - }, - "room_id": { - "type": "string", - "description": "The room in which this event occurred.", - "required": true - } - } - }, - "PublicRoomInfo": { - "id": "PublicRoomInfo", - "properties": { - "aliases": { - "type": "array", - "description": "A list of room aliases for this room.", - "items": { - "$ref": "string" - } - }, - "name": { - "type": "string", - "description": "The name of the room, as given by the m.room.name state event." - }, - "room_id": { - "type": "string", - "description": "The room ID for this public room.", - "required": true - }, - "topic": { - "type": "string", - "description": "The topic of this room, as given by the m.room.topic state event." - } - } - }, - "PublicRoomsPaginationChunk": { - "id": "PublicRoomsPaginationChunk", - "properties": { - "start": { - "type": "string", - "description": "A token which correlates to the first value in \"chunk\" for paginating.", - "required": true - }, - "end": { - "type": "string", - "description": "A token which correlates to the last value in \"chunk\" for paginating.", - "required": true - }, - "chunk": { - "type": "array", - "description": "A list of public room data.", - "required": true, - "items": { - "$ref": "PublicRoomInfo" - } - } - } - }, - "InitialSyncResponse": { - "id": "InitialSyncResponse", - "properties": { - "end": { - "type": "string", - "description": "A streaming token which can be used with /events to continue from this snapshot of data.", - "required": true - }, - "presence": { - "type": "array", - "description": "A list of presence events.", - "items": { - "$ref": "Event" - }, - "required": false - }, - "rooms": { - "type": "array", - "description": "A list of initial sync room data.", - "required": false, - "items": { - "$ref": "InitialSyncRoomData" - } - } - } - }, - "InitialSyncRoomData": { - "id": "InitialSyncRoomData", - "properties": { - "membership": { - "type": "string", - "description": "This user's membership state in this room.", - "required": true - }, - "room_id": { - "type": "string", - "description": "The ID of this room.", - "required": true - }, - "messages": { - "type": "PaginationChunk", - "description": "The most recent messages for this room, governed by the limit parameter.", - "required": false - }, - "state": { - "type": "array", - "description": "A list of state events representing the current state of the room.", - "required": false, - "items": { - "$ref": "Event" - } - } - } - } - } -} diff --git a/api/client-server/api-docs-login b/api/client-server/api-docs-login deleted file mode 100644 index d6f8d84f..00000000 --- a/api/client-server/api-docs-login +++ /dev/null @@ -1,120 +0,0 @@ -{ - "apiVersion": "1.0.0", - "apis": [ - { - "operations": [ - { - "method": "GET", - "nickname": "get_login_info", - "notes": "All login stages MUST be mentioned if there is >1 login type.", - "summary": "Get the login mechanism to use when logging in.", - "type": "LoginFlows" - }, - { - "method": "POST", - "nickname": "submit_login", - "notes": "If this is part of a multi-stage login, there MUST be a 'session' key.", - "parameters": [ - { - "description": "A login submission", - "name": "body", - "paramType": "body", - "required": true, - "type": "LoginSubmission" - } - ], - "responseMessages": [ - { - "code": 400, - "message": "Bad login type" - }, - { - "code": 400, - "message": "Missing JSON keys" - } - ], - "summary": "Submit a login action.", - "type": "LoginResult" - } - ], - "path": "/login" - } - ], - "basePath": "http://localhost:8008/_matrix/client/api/v1", - "consumes": [ - "application/json" - ], - "models": { - "LoginFlows": { - "id": "LoginFlows", - "properties": { - "flows": { - "description": "A list of valid login flows.", - "type": "array", - "items": { - "$ref": "LoginInfo" - } - } - } - }, - "LoginInfo": { - "id": "LoginInfo", - "properties": { - "stages": { - "description": "Multi-stage login only: An array of all the login types required to login.", - "items": { - "$ref": "string" - }, - "type": "array" - }, - "type": { - "description": "The login type that must be used when logging in.", - "type": "string" - } - } - }, - "LoginResult": { - "id": "LoginResult", - "properties": { - "access_token": { - "description": "The access token for this user's login if this is the final stage of the login process.", - "type": "string" - }, - "user_id": { - "description": "The user's fully-qualified user ID.", - "type": "string" - }, - "next": { - "description": "Multi-stage login only: The next login type to submit.", - "type": "string" - }, - "session": { - "description": "Multi-stage login only: The session token to send when submitting the next login type.", - "type": "string" - } - } - }, - "LoginSubmission": { - "id": "LoginSubmission", - "properties": { - "type": { - "description": "The type of login being submitted.", - "type": "string" - }, - "session": { - "description": "Multi-stage login only: The session token from an earlier login stage.", - "type": "string" - }, - "_login_type_defined_keys_": { - "description": "Keys as defined by the specified login type, e.g. \"user\", \"password\"" - } - } - } - }, - "produces": [ - "application/json" - ], - "resourcePath": "/login", - "swaggerVersion": "1.2" -} - diff --git a/api/client-server/api-docs-presence b/api/client-server/api-docs-presence deleted file mode 100644 index 6b224460..00000000 --- a/api/client-server/api-docs-presence +++ /dev/null @@ -1,164 +0,0 @@ -{ - "apiVersion": "1.0.0", - "swaggerVersion": "1.2", - "basePath": "http://localhost:8008/_matrix/client/api/v1", - "resourcePath": "/presence", - "produces": [ - "application/json" - ], - "consumes": [ - "application/json" - ], - "apis": [ - { - "path": "/presence/{userId}/status", - "operations": [ - { - "method": "PUT", - "summary": "Update this user's presence state.", - "notes": "This can only be done by the logged in user.", - "type": "void", - "nickname": "update_presence", - "parameters": [ - { - "name": "body", - "description": "The new presence state", - "required": true, - "type": "PresenceUpdate", - "paramType": "body" - }, - { - "name": "userId", - "description": "The user whose presence to set.", - "required": true, - "type": "string", - "paramType": "path" - } - ] - }, - { - "method": "GET", - "summary": "Get this user's presence state.", - "notes": "Get this user's presence state.", - "type": "PresenceUpdate", - "nickname": "get_presence", - "parameters": [ - { - "name": "userId", - "description": "The user whose presence to get.", - "required": true, - "type": "string", - "paramType": "path" - } - ] - } - ] - }, - { - "path": "/presence/list/{userId}", - "operations": [ - { - "method": "GET", - "summary": "Retrieve a list of presences for all of this user's friends.", - "notes": "", - "type": "array", - "items": { - "$ref": "Presence" - }, - "nickname": "get_presence_list", - "parameters": [ - { - "name": "userId", - "description": "The user whose presence list to get.", - "required": true, - "type": "string", - "paramType": "path" - } - ] - }, - { - "method": "POST", - "summary": "Add or remove users from this presence list.", - "notes": "Add or remove users from this presence list.", - "type": "void", - "nickname": "modify_presence_list", - "parameters": [ - { - "name": "userId", - "description": "The user whose presence list is being modified.", - "required": true, - "type": "string", - "paramType": "path" - }, - { - "name": "body", - "description": "The modifications to make to this presence list.", - "required": true, - "type": "PresenceListModifications", - "paramType": "body" - } - ] - } - ] - } - ], - "models": { - "PresenceUpdate": { - "id": "PresenceUpdate", - "properties": { - "presence": { - "type": "string", - "description": "Enum: The presence state.", - "enum": [ - "offline", - "unavailable", - "online", - "free_for_chat" - ] - }, - "status_msg": { - "type": "string", - "description": "The user-defined message associated with this presence state." - } - }, - "subTypes": [ - "Presence" - ] - }, - "Presence": { - "id": "Presence", - "properties": { - "last_active_ago": { - "type": "integer", - "format": "int64", - "description": "The last time this user performed an action on their home server." - }, - "user_id": { - "type": "string", - "description": "The fully qualified user ID" - } - } - }, - "PresenceListModifications": { - "id": "PresenceListModifications", - "properties": { - "invite": { - "type": "array", - "description": "A list of user IDs to add to the list.", - "items": { - "type": "string", - "description": "A fully qualified user ID." - } - }, - "drop": { - "type": "array", - "description": "A list of user IDs to remove from the list.", - "items": { - "type": "string", - "description": "A fully qualified user ID." - } - } - } - } - } -} diff --git a/api/client-server/api-docs-profile b/api/client-server/api-docs-profile deleted file mode 100644 index d2fccaa6..00000000 --- a/api/client-server/api-docs-profile +++ /dev/null @@ -1,122 +0,0 @@ -{ - "apiVersion": "1.0.0", - "swaggerVersion": "1.2", - "basePath": "http://localhost:8008/_matrix/client/api/v1", - "resourcePath": "/profile", - "produces": [ - "application/json" - ], - "consumes": [ - "application/json" - ], - "apis": [ - { - "path": "/profile/{userId}/displayname", - "operations": [ - { - "method": "PUT", - "summary": "Set a display name.", - "notes": "This can only be done by the logged in user.", - "type": "void", - "nickname": "set_display_name", - "parameters": [ - { - "name": "body", - "description": "The new display name for this user.", - "required": true, - "type": "DisplayName", - "paramType": "body" - }, - { - "name": "userId", - "description": "The user whose display name to set.", - "required": true, - "type": "string", - "paramType": "path" - } - ] - }, - { - "method": "GET", - "summary": "Get a display name.", - "notes": "This can be done by anyone.", - "type": "DisplayName", - "nickname": "get_display_name", - "parameters": [ - { - "name": "userId", - "description": "The user whose display name to get.", - "required": true, - "type": "string", - "paramType": "path" - } - ] - } - ] - }, - { - "path": "/profile/{userId}/avatar_url", - "operations": [ - { - "method": "PUT", - "summary": "Set an avatar URL.", - "notes": "This can only be done by the logged in user.", - "type": "void", - "nickname": "set_avatar_url", - "parameters": [ - { - "name": "body", - "description": "The new avatar url for this user.", - "required": true, - "type": "AvatarUrl", - "paramType": "body" - }, - { - "name": "userId", - "description": "The user whose avatar url to set.", - "required": true, - "type": "string", - "paramType": "path" - } - ] - }, - { - "method": "GET", - "summary": "Get an avatar url.", - "notes": "This can be done by anyone.", - "type": "AvatarUrl", - "nickname": "get_avatar_url", - "parameters": [ - { - "name": "userId", - "description": "The user whose avatar url to get.", - "required": true, - "type": "string", - "paramType": "path" - } - ] - } - ] - } - ], - "models": { - "DisplayName": { - "id": "DisplayName", - "properties": { - "displayname": { - "type": "string", - "description": "The textual display name" - } - } - }, - "AvatarUrl": { - "id": "AvatarUrl", - "properties": { - "avatar_url": { - "type": "string", - "description": "A url to an image representing an avatar." - } - } - } - } -} diff --git a/api/client-server/api-docs-registration b/api/client-server/api-docs-registration deleted file mode 100644 index 11c170c3..00000000 --- a/api/client-server/api-docs-registration +++ /dev/null @@ -1,120 +0,0 @@ -{ - "apiVersion": "1.0.0", - "apis": [ - { - "operations": [ - { - "method": "GET", - "nickname": "get_registration_info", - "notes": "All login stages MUST be mentioned if there is >1 login type.", - "summary": "Get the login mechanism to use when registering.", - "type": "RegistrationFlows" - }, - { - "method": "POST", - "nickname": "submit_registration", - "notes": "If this is part of a multi-stage registration, there MUST be a 'session' key.", - "parameters": [ - { - "description": "A registration submission", - "name": "body", - "paramType": "body", - "required": true, - "type": "RegistrationSubmission" - } - ], - "responseMessages": [ - { - "code": 400, - "message": "Bad login type" - }, - { - "code": 400, - "message": "Missing JSON keys" - } - ], - "summary": "Submit a registration action.", - "type": "RegistrationResult" - } - ], - "path": "/register" - } - ], - "basePath": "http://localhost:8008/_matrix/client/api/v1", - "consumes": [ - "application/json" - ], - "models": { - "RegistrationFlows": { - "id": "RegistrationFlows", - "properties": { - "flows": { - "description": "A list of valid registration flows.", - "type": "array", - "items": { - "$ref": "RegistrationInfo" - } - } - } - }, - "RegistrationInfo": { - "id": "RegistrationInfo", - "properties": { - "stages": { - "description": "Multi-stage registration only: An array of all the login types required to registration.", - "items": { - "$ref": "string" - }, - "type": "array" - }, - "type": { - "description": "The first login type that must be used when logging in.", - "type": "string" - } - } - }, - "RegistrationResult": { - "id": "RegistrationResult", - "properties": { - "access_token": { - "description": "The access token for this user's registration if this is the final stage of the registration process.", - "type": "string" - }, - "user_id": { - "description": "The user's fully-qualified user ID.", - "type": "string" - }, - "next": { - "description": "Multi-stage registration only: The next registration type to submit.", - "type": "string" - }, - "session": { - "description": "Multi-stage registration only: The session token to send when submitting the next registration type.", - "type": "string" - } - } - }, - "RegistrationSubmission": { - "id": "RegistrationSubmission", - "properties": { - "type": { - "description": "The type of registration being submitted.", - "type": "string" - }, - "session": { - "description": "Multi-stage registration only: The session token from an earlier registration stage.", - "type": "string" - }, - "_registration_type_defined_keys_": { - "description": "Keys as defined by the specified registration type, e.g. \"user\", \"password\"" - } - } - } - }, - "produces": [ - "application/json" - ], - "resourcePath": "/register", - "swaggerVersion": "1.2" -} - diff --git a/api/client-server/api-docs-rooms b/api/client-server/api-docs-rooms deleted file mode 100644 index 0d3d9fbc..00000000 --- a/api/client-server/api-docs-rooms +++ /dev/null @@ -1,1128 +0,0 @@ -{ - "apiVersion": "1.0.0", - "swaggerVersion": "1.2", - "basePath": "http://localhost:8008/_matrix/client/api/v1", - "resourcePath": "/rooms", - "produces": [ - "application/json" - ], - "consumes": [ - "application/json" - ], - "authorizations": { - "token": [] - }, - "apis": [ - { - "path": "/rooms/{roomId}/send/{eventType}", - "operations": [ - { - "method": "POST", - "summary": "Send a generic non-state event to this room.", - "notes": "This operation can also be done as a PUT by suffixing /{txnId}.", - "type": "EventId", - "nickname": "send_non_state_event", - "consumes": [ - "application/json" - ], - "parameters": [ - { - "name": "body", - "description": "The event contents", - "required": true, - "type": "EventContent", - "paramType": "body" - }, - { - "name": "roomId", - "description": "The room to send the message in.", - "required": true, - "type": "string", - "paramType": "path" - }, - { - "name": "eventType", - "description": "The type of event to send.", - "required": true, - "type": "string", - "paramType": "path" - } - ] - } - ] - }, - { - "path": "/rooms/{roomId}/state/{eventType}/{stateKey}", - "operations": [ - { - "method": "PUT", - "summary": "Send a generic state event to this room.", - "notes": "The state key can be omitted, such that you can PUT to /rooms/{roomId}/state/{eventType}. The state key defaults to a 0 length string in this case.", - "type": "void", - "nickname": "send_state_event", - "consumes": [ - "application/json" - ], - "parameters": [ - { - "name": "body", - "description": "The event contents", - "required": true, - "type": "EventContent", - "paramType": "body" - }, - { - "name": "roomId", - "description": "The room to send the message in.", - "required": true, - "type": "string", - "paramType": "path" - }, - { - "name": "eventType", - "description": "The type of event to send.", - "required": true, - "type": "string", - "paramType": "path" - }, - { - "name": "stateKey", - "description": "An identifier used to specify clobbering semantics. State events with the same (roomId, eventType, stateKey) will be replaced.", - "required": true, - "type": "string", - "paramType": "path" - } - ] - } - ] - }, - { - "path": "/rooms/{roomId}/send/m.room.message", - "operations": [ - { - "method": "POST", - "summary": "Send a message in this room.", - "notes": "This operation can also be done as a PUT by suffixing /{txnId}.", - "type": "EventId", - "nickname": "send_message", - "consumes": [ - "application/json" - ], - "parameters": [ - { - "name": "body", - "description": "The message contents", - "required": true, - "type": "Message", - "paramType": "body" - }, - { - "name": "roomId", - "description": "The room to send the message in.", - "required": true, - "type": "string", - "paramType": "path" - } - ] - } - ] - }, - { - "path": "/rooms/{roomId}/state/m.room.topic", - "operations": [ - { - "method": "PUT", - "summary": "Set the topic for this room.", - "notes": "Set the topic for this room.", - "type": "void", - "nickname": "set_topic", - "consumes": [ - "application/json" - ], - "parameters": [ - { - "name": "body", - "description": "The topic contents", - "required": true, - "type": "Topic", - "paramType": "body" - }, - { - "name": "roomId", - "description": "The room to set the topic in.", - "required": true, - "type": "string", - "paramType": "path" - } - ] - }, - { - "method": "GET", - "summary": "Get the topic for this room.", - "notes": "Get the topic for this room.", - "type": "Topic", - "nickname": "get_topic", - "parameters": [ - { - "name": "roomId", - "description": "The room to get topic in.", - "required": true, - "type": "string", - "paramType": "path" - } - ], - "responseMessages": [ - { - "code": 404, - "message": "Topic not found." - } - ] - } - ] - }, - { - "path": "/rooms/{roomId}/state/m.room.name", - "operations": [ - { - "method": "PUT", - "summary": "Set the name of this room.", - "notes": "Set the name of this room.", - "type": "void", - "nickname": "set_room_name", - "consumes": [ - "application/json" - ], - "parameters": [ - { - "name": "body", - "description": "The name contents", - "required": true, - "type": "RoomName", - "paramType": "body" - }, - { - "name": "roomId", - "description": "The room to set the name of.", - "required": true, - "type": "string", - "paramType": "path" - } - ] - }, - { - "method": "GET", - "summary": "Get the room's name.", - "notes": "", - "type": "RoomName", - "nickname": "get_room_name", - "parameters": [ - { - "name": "roomId", - "description": "The room to get the name of.", - "required": true, - "type": "string", - "paramType": "path" - } - ], - "responseMessages": [ - { - "code": 404, - "message": "Name not found." - } - ] - } - ] - }, - { - "path": "/rooms/{roomId}/state/m.room.power_levels", - "operations": [ - { - "method": "PUT", - "summary": "Set the power levels for this room.", - "notes": "This has to be set atomically. The levels set will clobber what was previously there.", - "type": "void", - "nickname": "set_room_power_levels", - "consumes": [ - "application/json" - ], - "parameters": [ - { - "name": "body", - "description": "The power levels", - "required": true, - "type": "RoomPowerLevels", - "paramType": "body" - }, - { - "name": "roomId", - "description": "The room to set the power levels for.", - "required": true, - "type": "string", - "paramType": "path" - } - ] - } - ] - }, - { - "path": "/rooms/{roomId}/send/m.room.message.feedback", - "operations": [ - { - "method": "POST", - "summary": "Send feedback to a message.", - "notes": "This operation can also be done as a PUT by suffixing /{txnId}.", - "type": "EventId", - "nickname": "send_feedback", - "consumes": [ - "application/json" - ], - "parameters": [ - { - "name": "body", - "description": "The feedback contents", - "required": true, - "type": "Feedback", - "paramType": "body" - }, - { - "name": "roomId", - "description": "The room to send the feedback in.", - "required": true, - "type": "string", - "paramType": "path" - } - ], - "responseMessages": [ - { - "code": 400, - "message": "Bad feedback type." - } - ] - } - ] - }, - { - "path": "/rooms/{roomId}/invite", - "operations": [ - { - "method": "POST", - "summary": "Invite a user to this room.", - "notes": "This operation can also be done as a PUT by suffixing /{txnId}.", - "type": "void", - "nickname": "invite", - "consumes": [ - "application/json" - ], - "parameters": [ - { - "name": "roomId", - "description": "The room which has this user.", - "required": true, - "type": "string", - "paramType": "path" - }, - { - "name": "body", - "description": "The user to invite.", - "required": true, - "type": "InviteRequest", - "paramType": "body" - } - ] - } - ] - }, - { - "path": "/rooms/{roomId}/join", - "operations": [ - { - "method": "POST", - "summary": "Join this room.", - "notes": "This operation can also be done as a PUT by suffixing /{txnId}.", - "type": "void", - "nickname": "join_room", - "consumes": [ - "application/json" - ], - "parameters": [ - { - "name": "roomId", - "description": "The room to join.", - "required": true, - "type": "string", - "paramType": "path" - }, - { - "name": "body", - "required": true, - "type": "JoinRequest", - "paramType": "body" - } - ] - } - ] - }, - { - "path": "/rooms/{roomId}/leave", - "operations": [ - { - "method": "POST", - "summary": "Leave this room.", - "notes": "This operation can also be done as a PUT by suffixing /{txnId}.", - "type": "void", - "nickname": "leave", - "consumes": [ - "application/json" - ], - "parameters": [ - { - "name": "roomId", - "description": "The room to leave.", - "required": true, - "type": "string", - "paramType": "path" - }, - { - "name": "body", - "required": true, - "type": "LeaveRequest", - "paramType": "body" - } - ] - } - ] - }, - { - "path": "/rooms/{roomId}/ban", - "operations": [ - { - "method": "POST", - "summary": "Ban a user in the room.", - "notes": "This operation can also be done as a PUT by suffixing /{txnId}. The caller must have the required power level to do this operation.", - "type": "void", - "nickname": "ban", - "consumes": [ - "application/json" - ], - "parameters": [ - { - "name": "roomId", - "description": "The room which has the user to ban.", - "required": true, - "type": "string", - "paramType": "path" - }, - { - "name": "body", - "description": "The user to ban.", - "required": true, - "type": "BanRequest", - "paramType": "body" - } - ] - } - ] - }, - { - "path": "/rooms/{roomId}/state/m.room.member/{userId}", - "operations": [ - { - "method": "PUT", - "summary": "Change the membership state for a user in a room.", - "notes": "Change the membership state for a user in a room.", - "type": "void", - "nickname": "set_membership", - "consumes": [ - "application/json" - ], - "parameters": [ - { - "name": "body", - "description": "The new membership state", - "required": true, - "type": "Member", - "paramType": "body" - }, - { - "name": "userId", - "description": "The user whose membership is being changed.", - "required": true, - "type": "string", - "paramType": "path" - }, - { - "name": "roomId", - "description": "The room which has this user.", - "required": true, - "type": "string", - "paramType": "path" - } - ], - "responseMessages": [ - { - "code": 400, - "message": "No membership key." - }, - { - "code": 400, - "message": "Bad membership value." - }, - { - "code": 403, - "message": "When inviting: You are not in the room." - }, - { - "code": 403, - "message": "When inviting: is already in the room." - }, - { - "code": 403, - "message": "When joining: Cannot force another user to join." - }, - { - "code": 403, - "message": "When joining: You are not invited to this room." - } - ] - }, - { - "method": "GET", - "summary": "Get the membership state of a user in a room.", - "notes": "Get the membership state of a user in a room.", - "type": "Member", - "nickname": "get_membership", - "parameters": [ - { - "name": "userId", - "description": "The user whose membership state you want to get.", - "required": true, - "type": "string", - "paramType": "path" - }, - { - "name": "roomId", - "description": "The room which has this user.", - "required": true, - "type": "string", - "paramType": "path" - } - ], - "responseMessages": [ - { - "code": 404, - "message": "Member not found." - } - ] - } - ] - }, - { - "path": "/join/{roomAliasOrId}", - "operations": [ - { - "method": "POST", - "summary": "Join a room via a room alias or room ID.", - "notes": "This endpoint, unlike /rooms/{roomId}/join allows the client to join a room by it's alias. This operation can also be done as a PUT by suffixing /{txnId}.", - "type": "JoinRoomInfo", - "nickname": "join", - "consumes": [ - "application/json" - ], - "parameters": [ - { - "name": "roomAliasOrId", - "description": "The room alias or room ID to join.", - "required": true, - "type": "string", - "paramType": "path" - } - ], - "responseMessages": [ - { - "code": 400, - "message": "Bad room alias." - } - ] - } - ] - }, - { - "path": "/createRoom", - "operations": [ - { - "method": "POST", - "summary": "Create a room.", - "notes": "Create a room. This operation can also be done as a PUT by suffixing /{txnId}.", - "type": "RoomInfo", - "nickname": "create_room", - "consumes": [ - "application/json" - ], - "parameters": [ - { - "name": "body", - "description": "The desired configuration for the room.", - "required": true, - "type": "RoomConfig", - "paramType": "body" - } - ], - "responseMessages": [ - { - "code": 400, - "message": "Body must be JSON." - }, - { - "code": 400, - "message": "Room alias already taken." - } - ] - } - ] - }, - { - "path": "/rooms/{roomId}/messages", - "operations": [ - { - "method": "GET", - "summary": "Get a list of messages for this room.", - "notes": "Get a list of messages for this room.", - "type": "MessagePaginationChunk", - "nickname": "get_messages", - "parameters": [ - { - "name": "roomId", - "description": "The room to get messages in.", - "required": true, - "type": "string", - "paramType": "path" - }, - { - "name": "from", - "description": "The token to start getting results from.", - "required": false, - "type": "string", - "paramType": "query" - }, - { - "name": "to", - "description": "The token to stop getting results at.", - "required": false, - "type": "string", - "paramType": "query" - }, - { - "name": "limit", - "description": "The maximum number of messages to return.", - "required": false, - "type": "integer", - "paramType": "query" - } - ] - } - ] - }, - { - "path": "/rooms/{roomId}/members", - "operations": [ - { - "method": "GET", - "summary": "Get a list of members for this room.", - "notes": "Get a list of members for this room.", - "type": "MemberPaginationChunk", - "nickname": "get_members", - "parameters": [ - { - "name": "roomId", - "description": "The room to get a list of members from.", - "required": true, - "type": "string", - "paramType": "path" - }, - { - "name": "from", - "description": "The token to start getting results from.", - "required": false, - "type": "string", - "paramType": "query" - }, - { - "name": "to", - "description": "The token to stop getting results at.", - "required": false, - "type": "string", - "paramType": "query" - }, - { - "name": "limit", - "description": "The maximum number of members to return.", - "required": false, - "type": "integer", - "paramType": "query" - } - ] - } - ] - }, - { - "path": "/rooms/{roomId}/state", - "operations": [ - { - "method": "GET", - "summary": "Get a list of all the current state events for this room.", - "notes": "This is equivalent to the events returned under the 'state' key for this room in /initialSync.", - "type": "array", - "items": { - "$ref": "Event" - }, - "nickname": "get_state_events", - "parameters": [ - { - "name": "roomId", - "description": "The room to get a list of current state events from.", - "required": true, - "type": "string", - "paramType": "path" - } - ] - } - ] - }, - { - "path": "/rooms/{roomId}/typing/{userId}", - "operations": [ - { - "method": "PUT", - "summary": "Inform the server that this user is typing.", - "notes": "Only the authorised user can send typing notifications.", - "type": "void", - "nickname": "put_typing", - "parameters": [ - { - "name": "roomId", - "description": "The room to send the m.typing event into.", - "required": true, - "type": "string", - "paramType": "path" - }, - { - "name": "userId", - "description": "The user ID of the typer.", - "required": true, - "type": "string", - "paramType": "path" - }, - { - "name": "body", - "description": "The typing information.", - "required": true, - "type": "Typing", - "paramType": "body" - } - ] - } - ] - }, - { - "path": "/rooms/{roomId}/initialSync", - "operations": [ - { - "method": "GET", - "summary": "Get all the current information for this room, including messages and state events.", - "type": "InitialSyncRoomData", - "nickname": "get_room_sync_data", - "parameters": [ - { - "name": "roomId", - "description": "The room to get information for.", - "required": true, - "type": "string", - "paramType": "path" - } - ] - } - ] - } - ], - "models": { - "Topic": { - "id": "Topic", - "properties": { - "topic": { - "type": "string", - "description": "The topic text" - } - } - }, - "RoomName": { - "id": "RoomName", - "properties": { - "name": { - "type": "string", - "description": "The human-readable name for the room. Can contain spaces." - } - } - }, - "UserPowerLevels": { - "id": "UserPowerLevels", - "properties": { - "__user_id__": { - "type": "integer", - "description": "The power level for __user_id__" - } - } - }, - "EventPowerLevels": { - "id": "UserPowerLevels", - "properties": { - "__event_type__": { - "type": "integer", - "description": "The power level required in order to send __event_type__ events." - } - } - }, - "RoomPowerLevels": { - "id": "RoomPowerLevels", - "properties": { - "ban": { - "type": "integer", - "description": "The minimum level required in order to ban someone." - }, - "kick": { - "type": "integer", - "description": "The minimum level required in order to kick someone." - }, - "redact": { - "type": "integer", - "description": "The minimum level required in order to redact a message." - }, - "users_default": { - "type": "integer", - "description": "The default power level of a user who is not in the 'users' list." - }, - "state_default": { - "type": "integer", - "description": "The default power level required in order to send state events." - }, - "events_default": { - "type": "integer", - "description": "The default power level required in order to send non-state events." - }, - "users": { - "type": "UserPowerLevels", - "description": "Mappings of user ID to power level." - }, - "events": { - "type": "EventPowerLevels", - "description": "Mappings of event type to power level." - } - } - }, - "Message": { - "id": "Message", - "properties": { - "msgtype": { - "type": "string", - "description": "The type of message being sent, e.g. \"m.text\"", - "required": true - }, - "_msgtype_defined_keys_": { - "description": "Additional keys as defined by the msgtype, e.g. \"body\"" - } - } - }, - "Feedback": { - "id": "Feedback", - "properties": { - "target_event_id": { - "type": "string", - "description": "The event ID being acknowledged.", - "required": true - }, - "type": { - "type": "string", - "description": "The type of feedback. Either 'delivered' or 'read'.", - "required": true - } - } - }, - "Member": { - "id": "Member", - "properties": { - "membership": { - "type": "string", - "description": "Enum: The membership state of this member.", - "enum": [ - "invite", - "join", - "leave", - "ban" - ] - } - } - }, - "RoomInfo": { - "id": "RoomInfo", - "properties": { - "room_id": { - "type": "string", - "description": "The allocated room ID.", - "required": true - }, - "room_alias": { - "type": "string", - "description": "The alias for the room.", - "required": false - } - } - }, - "JoinRoomInfo": { - "id": "JoinRoomInfo", - "properties": { - "room_id": { - "type": "string", - "description": "The room ID joined, if joined via a room alias only.", - "required": true - } - } - }, - "Typing": { - "id": "Typing", - "properties": { - "typing": { - "type": "boolean", - "description": "True if the user is currently typing.", - "required": true - }, - "timeout": { - "type": "integer", - "description": "The length of time until the user should be treated as no longer typing, in milliseconds. Can be omitted if they are no longer typing.", - "required": true - } - } - }, - "RoomConfig": { - "id": "RoomConfig", - "properties": { - "visibility": { - "type": "string", - "description": "Enum: The room visibility. The room_alias_name is required if the visibility is public; without it the room will remain private.", - "required": false, - "enum": [ - "public", - "private" - ] - }, - "room_alias_name": { - "type": "string", - "description": "Localpart of the alias to give the new room. The home server will attach its domain name after this.", - "required": false - }, - "name": { - "type": "string", - "description": "Sets the name of the room. Send a m.room.name event after creating the room with the 'name' key specified.", - "required": false - }, - "topic": { - "type": "string", - "description": "Sets the topic for the room. Send a m.room.topic event after creating the room with the 'topic' key specified.", - "required": false - }, - "invite": { - "type": "array", - "description": "The list of user IDs to invite. Sends m.room.member events after creating the room.", - "items": { - "$ref": "string" - }, - "required": false - } - } - }, - "PaginationRequest": { - "id": "PaginationRequest", - "properties": { - "from": { - "type": "string", - "description": "The token to start getting results from." - }, - "to": { - "type": "string", - "description": "The token to stop getting results at." - }, - "limit": { - "type": "integer", - "description": "The maximum number of entries to return." - } - } - }, - "PaginationChunk": { - "id": "PaginationChunk", - "properties": { - "start": { - "type": "string", - "description": "A token which correlates to the first value in \"chunk\" for paginating.", - "required": true - }, - "end": { - "type": "string", - "description": "A token which correlates to the last value in \"chunk\" for paginating.", - "required": true - } - }, - "subTypes": [ - "MessagePaginationChunk" - ] - }, - "MessagePaginationChunk": { - "id": "MessagePaginationChunk", - "properties": { - "chunk": { - "type": "array", - "description": "A list of message events.", - "items": { - "$ref": "MessageEvent" - }, - "required": true - } - } - }, - "MemberPaginationChunk": { - "id": "MemberPaginationChunk", - "properties": { - "chunk": { - "type": "array", - "description": "A list of member events.", - "items": { - "$ref": "MemberEvent" - }, - "required": true - } - } - }, - "Event": { - "id": "Event", - "properties": { - "event_id": { - "type": "string", - "description": "An ID which uniquely identifies this event. This is automatically set by the server.", - "required": true - }, - "room_id": { - "type": "string", - "description": "The room in which this event occurred. This is automatically set by the server.", - "required": true - }, - "type": { - "type": "string", - "description": "The event type.", - "required": true - } - }, - "subTypes": [ - "MessageEvent" - ] - }, - "EventId": { - "id": "EventId", - "properties": { - "event_id": { - "type": "string", - "description": "The allocated event ID for this event.", - "required": true - } - } - }, - "EventContent": { - "id": "EventContent", - "properties": { - "__event_content_keys__": { - "type": "string", - "description": "Event-specific content keys and values.", - "required": false - } - } - }, - "MessageEvent": { - "id": "MessageEvent", - "properties": { - "content": { - "type": "Message" - } - } - }, - "MemberEvent": { - "id": "MemberEvent", - "properties": { - "content": { - "type": "Member" - } - } - }, - "InviteRequest": { - "id": "InviteRequest", - "properties": { - "user_id": { - "type": "string", - "description": "The fully-qualified user ID." - } - } - }, - "JoinRequest": { - "id": "JoinRequest", - "properties": {} - }, - "LeaveRequest": { - "id": "LeaveRequest", - "properties": {} - }, - "BanRequest": { - "id": "BanRequest", - "properties": { - "user_id": { - "type": "string", - "description": "The fully-qualified user ID." - }, - "reason": { - "type": "string", - "description": "The reason for the ban." - } - } - }, - "InitialSyncRoomData": { - "id": "InitialSyncRoomData", - "properties": { - "membership": { - "type": "string", - "description": "This user's membership state in this room.", - "required": true - }, - "room_id": { - "type": "string", - "description": "The ID of this room.", - "required": true - }, - "messages": { - "type": "MessagePaginationChunk", - "description": "The most recent messages for this room, governed by the limit parameter.", - "required": false - }, - "state": { - "type": "array", - "description": "A list of state events representing the current state of the room.", - "required": false, - "items": { - "$ref": "Event" - } - }, - "presence": { - "type": "array", - "description": "A list of m.presence events representing the current presence state of the room members.", - "required": false, - "items": { - "$ref": "Event" - } - } - } - } - } -} diff --git a/api/client-server/v1/application_service.yaml b/api/client-server/application_service.yaml similarity index 100% rename from api/client-server/v1/application_service.yaml rename to api/client-server/application_service.yaml diff --git a/api/client-server/banning.yaml b/api/client-server/banning.yaml new file mode 100644 index 00000000..e841050f --- /dev/null +++ b/api/client-server/banning.yaml @@ -0,0 +1,76 @@ +swagger: '2.0' +info: + title: "Matrix Client-Server v1 Room Banning API" + version: "1.0.0" +host: localhost:8008 +schemes: + - https + - http +basePath: /_matrix/client/api/v1 +consumes: + - application/json +produces: + - application/json +securityDefinitions: + accessToken: + type: apiKey + description: The user_id or application service access_token + name: access_token + in: query +paths: + "/rooms/{roomId}/ban": + post: + summary: Ban a user in the room. + description: |- + Ban a user in the room. If the user is currently in the room, also kick them. + + When a user is banned from a room, they may not join it until they are unbanned. + + The caller must have the required power level in order to perform this operation. + security: + - accessToken: [] + parameters: + - in: path + type: string + name: roomId + description: The room identifier (not alias) from which the user should be banned. + required: true + x-example: "!e42d8c:matrix.org" + - in: body + name: body + required: true + schema: + type: object + example: |- + { + "reason": "Telling unfunny jokes", + "user_id": "@cheeky_monkey:matrix.org" + } + properties: + user_id: + type: string + description: The fully qualified user ID of the user being banned. + reason: + type: string + description: The reason the user has been banned. + required: ["user_id"] + responses: + 200: + description: The user has been kicked and banned from the room. + examples: + application/json: |- + {} + schema: + type: object + 403: + description: |- + You do not have permission to ban the user from the room. A meaningful ``errcode`` and description error text will be returned. Example reasons for rejections are: + + - The banner is not currently in the room. + - The banner's power level is insufficient to ban users from the room. + examples: + application/json: |- + { + "errcode": "M_FORBIDDEN", + "error": "You do not have a high enough power level to ban from this room." + } diff --git a/api/client-server/v1/content-repo.yaml b/api/client-server/content-repo.yaml similarity index 100% rename from api/client-server/v1/content-repo.yaml rename to api/client-server/content-repo.yaml diff --git a/api/client-server/v1/create_room.yaml b/api/client-server/create_room.yaml similarity index 100% rename from api/client-server/v1/create_room.yaml rename to api/client-server/create_room.yaml diff --git a/api/client-server/v1/definitions/error.yaml b/api/client-server/definitions/error.yaml similarity index 100% rename from api/client-server/v1/definitions/error.yaml rename to api/client-server/definitions/error.yaml diff --git a/api/client-server/definitions/event.json b/api/client-server/definitions/event.json new file mode 100644 index 00000000..5a8f52f6 --- /dev/null +++ b/api/client-server/definitions/event.json @@ -0,0 +1,53 @@ +{ + "type": "object", + "title": "Event", + "properties": { + "content": { + "type": "object", + "title": "EventContent", + "description": "The content of this event. The fields in this object will vary depending on the type of event." + }, + "origin_server_ts": { + "type": "integer", + "format": "int64", + "description": "Timestamp in milliseconds on originating homeserver when this event was sent." + }, + "sender": { + "type": "string", + "description": "The MXID of the user who sent this event." + }, + "state_key": { + "type": "string", + "description": "Optional. This key will only be present for state events. A unique key which defines the overwriting semantics for this piece of room state." + }, + "type": { + "type": "string", + "description": "The type of event." + }, + "unsigned": { + "type": "object", + "title": "Unsigned", + "description": "Information about this event which was not sent by the originating homeserver", + "properties": { + "age": { + "type": "integer", + "format": "int64", + "description": "Time in milliseconds since the event was sent." + }, + "prev_content": { + "title": "EventContent", + "type": "object", + "description": "Optional. The previous ``content`` for this state. This will be present only for state events appearing in the ``timeline``. If this is not a state event, or there is no previous content, this key will be missing." + }, + "replaces_state": { + "type": "string", + "description": "Optional. The event_id of the previous event for this state. This will be present only for state events appearing in the ``timeline``. If this is not a state event, or there is no previous content, this key will be missing." + }, + "transaction_id": { + "type": "string", + "description": "Optional. The transaction ID set when this message was sent. This key will only be present for message events sent by the device calling this API." + } + } + } + } +} diff --git a/api/client-server/v2_alpha/definitions/event_batch.json b/api/client-server/definitions/event_batch.json similarity index 69% rename from api/client-server/v2_alpha/definitions/event_batch.json rename to api/client-server/definitions/event_batch.json index 395aed13..7f489423 100644 --- a/api/client-server/v2_alpha/definitions/event_batch.json +++ b/api/client-server/definitions/event_batch.json @@ -5,8 +5,8 @@ "type": "array", "description": "List of events", "items": { - "title": "Event", - "type": "object" + "type": "object", + "allOf": [{"$ref": "event.json" }] } } } diff --git a/api/client-server/v2_alpha/definitions/event_filter.json b/api/client-server/definitions/event_filter.json similarity index 100% rename from api/client-server/v2_alpha/definitions/event_filter.json rename to api/client-server/definitions/event_filter.json diff --git a/api/client-server/v1/definitions/push_condition.json b/api/client-server/definitions/push_condition.json similarity index 100% rename from api/client-server/v1/definitions/push_condition.json rename to api/client-server/definitions/push_condition.json diff --git a/api/client-server/v1/definitions/push_rule.json b/api/client-server/definitions/push_rule.json similarity index 100% rename from api/client-server/v1/definitions/push_rule.json rename to api/client-server/definitions/push_rule.json diff --git a/api/client-server/v1/definitions/push_ruleset.json b/api/client-server/definitions/push_ruleset.json similarity index 100% rename from api/client-server/v1/definitions/push_ruleset.json rename to api/client-server/definitions/push_ruleset.json diff --git a/api/client-server/v2_alpha/definitions/room_event_filter.json b/api/client-server/definitions/room_event_filter.json similarity index 92% rename from api/client-server/v2_alpha/definitions/room_event_filter.json rename to api/client-server/definitions/room_event_filter.json index 86375781..02e5d0e0 100644 --- a/api/client-server/v2_alpha/definitions/room_event_filter.json +++ b/api/client-server/definitions/room_event_filter.json @@ -1,6 +1,6 @@ { "type": "object", - "allOf": [{"$ref": "definitions/event_filter.json"}], + "allOf": [{"$ref": "event_filter.json"}], "properties": { "rooms": { "type": "array", diff --git a/api/client-server/v2_alpha/definitions/sync_filter.json b/api/client-server/definitions/sync_filter.json similarity index 84% rename from api/client-server/v2_alpha/definitions/sync_filter.json rename to api/client-server/definitions/sync_filter.json index 0cd6a798..defc318a 100644 --- a/api/client-server/v2_alpha/definitions/sync_filter.json +++ b/api/client-server/definitions/sync_filter.json @@ -7,24 +7,24 @@ "state": { "description": "The state events to include for rooms.", - "allOf": [{"$ref": "definitions/room_event_filter.json"}] + "allOf": [{"$ref": "room_event_filter.json"}] }, "timeline": { "description": "The message and state update events to include for rooms.", - "allOf": [{"$ref": "definitions/room_event_filter.json"}] + "allOf": [{"$ref": "room_event_filter.json"}] }, "ephemeral": { "description": "The events that aren't recorded in the room history, e.g. typing and receipts, to include for rooms.", - "allOf": [{"$ref": "definitions/room_event_filter.json"}] + "allOf": [{"$ref": "room_event_filter.json"}] } } }, "presence": { "description": "The presence updates to include.", - "allOf": [{"$ref": "definitions/event_filter.json"}] + "allOf": [{"$ref": "event_filter.json"}] }, "event_format": { "description": diff --git a/api/client-server/definitions/timeline_batch.json b/api/client-server/definitions/timeline_batch.json new file mode 100644 index 00000000..6f7e714a --- /dev/null +++ b/api/client-server/definitions/timeline_batch.json @@ -0,0 +1,14 @@ +{ + "type": "object", + "allOf": [{"$ref":"event_batch.json"}], + "properties": { + "limited": { + "type": "boolean", + "description": "True if the number of events returned was limited by the ``limit`` on the filter" + }, + "prev_batch": { + "type": "string", + "description": "If the batch was limited then this is a token that can be supplied to the server to retrieve earlier events" + } + } +} diff --git a/api/client-server/v1/directory.yaml b/api/client-server/directory.yaml similarity index 60% rename from api/client-server/v1/directory.yaml rename to api/client-server/directory.yaml index c70b9f6b..4966a920 100644 --- a/api/client-server/v1/directory.yaml +++ b/api/client-server/directory.yaml @@ -29,6 +29,7 @@ paths: name: roomAlias description: The room alias to set. required: true + x-example: "#monkeys:matrix.org" - in: body name: roomInfo description: Information about this room alias. @@ -39,11 +40,18 @@ paths: room_id: type: string description: The room ID to set. + example: |- + { + "room_id": "!abnjk1jdasj98:capuchins.com" + } responses: 200: description: The mapping was created. + examples: + application/json: |- + {} schema: - type: object # empty json object + type: object get: summary: Get the room ID corresponding to this room alias. parameters: @@ -52,6 +60,7 @@ paths: name: roomAlias description: The room alias. required: true + x-example: "#monkeys:matrix.org" responses: 200: description: The room ID and other information for this alias. @@ -67,10 +76,38 @@ paths: items: type: string description: A server which is aware of this room ID. + examples: + application/json: |- + { + "room_id": "!abnjk1jdasj98:capuchins.com", + "servers": [ + "capuchins.com", + "matrix.org", + "another.com" + ] + } 404: description: There is no mapped room ID for this room alias. + examples: + application/json: |- + { + "errcode": "M_NOT_FOUND", + "error": "Room ID !abnjk1jdasj98:capuchins.com not found." + } + 409: + description: A room alias with that name already exists. + examples: + application/json: |- + { + "errcode": "M_UNKNOWN", + "error": "Room alias #monkeys:matrix.org already exists." + } delete: summary: Remove a mapping of room alias to room ID. + description: |- + Remove a mapping of room alias to room ID. + + Servers may choose to implement additional access control checks here, for instance that room aliases can only be deleted by their creator or a server administrator. security: - accessToken: [] parameters: @@ -79,9 +116,12 @@ paths: name: roomAlias description: The room alias to remove. required: true + x-example: "#monkeys:matrix.org" responses: 200: - description: The mapping was removed. + description: The mapping was deleted. + examples: + application/json: |- + {} schema: - type: object # empty json object - + type: object diff --git a/api/client-server/v2_alpha/filter.yaml b/api/client-server/filter.yaml similarity index 100% rename from api/client-server/v2_alpha/filter.yaml rename to api/client-server/filter.yaml diff --git a/api/client-server/v1/guest_events.yaml b/api/client-server/guest_events.yaml similarity index 97% rename from api/client-server/v1/guest_events.yaml rename to api/client-server/guest_events.yaml index bbb5799a..671b355a 100644 --- a/api/client-server/v1/guest_events.yaml +++ b/api/client-server/guest_events.yaml @@ -98,6 +98,6 @@ paths: type: object title: Event allOf: - - "$ref": "core-event-schema/room_event.json" + - "$ref": "../../event-schemas/schema/core-event-schema/room_event.json" 400: description: "Bad pagination ``from`` parameter." diff --git a/api/client-server/v1/membership.yaml b/api/client-server/inviting.yaml similarity index 61% rename from api/client-server/v1/membership.yaml rename to api/client-server/inviting.yaml index c089e154..0a028710 100644 --- a/api/client-server/v1/membership.yaml +++ b/api/client-server/inviting.yaml @@ -1,6 +1,6 @@ swagger: '2.0' info: - title: "Matrix Client-Server v1 Room Membership API" + title: "Matrix Client-Server v1 Room Joining API" version: "1.0.0" host: localhost:8008 schemes: @@ -18,55 +18,6 @@ securityDefinitions: name: access_token in: query paths: - "/rooms/{roomId}/join": - post: - summary: Start the requesting user participating in a particular room. - description: |- - This API starts a user participating in a particular room, if that user - is allowed to participate in that room. After this call, the client is - allowed to see all current state events in the room, and all subsequent - events associated with the room until the user leaves the room. - - After a user has joined a room, the room will appear as an entry in the - response of the |initialSync| API. - security: - - accessToken: [] - parameters: - - in: path - type: string - name: roomId - description: The room identifier or room alias to join. - required: true - x-example: "#monkeys:matrix.org" - responses: - 200: - description: |- - The room has been joined. - - The joined room ID must be returned in the ``room_id`` field. - examples: - application/json: |- - {"room_id": "!d41d8cd:matrix.org"} - schema: - type: object - 403: - description: |- - You do not have permission to join the room. A meaningful ``errcode`` and description error text will be returned. Example reasons for rejection are: - - - The room is invite-only and the user was not invited. - - The user has been banned from the room. - examples: - application/json: |- - {"errcode": "M_FORBIDDEN", "error": "You are not invited to this room."} - 429: - description: This request was rate-limited. - schema: - "$ref": "definitions/error.yaml" - x-alias: - canonical-link: "post-matrix-client-api-v1-rooms-roomid-join" - aliases: - - /join/{roomId} - # With an extra " " to disambiguate from the 3pid invite endpoint # The extra space makes it sort first for what I'm sure is a good reason. "/rooms/{roomId}/invite ": diff --git a/api/client-server/joining.yaml b/api/client-server/joining.yaml new file mode 100644 index 00000000..b6d2df18 --- /dev/null +++ b/api/client-server/joining.yaml @@ -0,0 +1,68 @@ +swagger: '2.0' +info: + title: "Matrix Client-Server v1 Room Inviting API" + version: "1.0.0" +host: localhost:8008 +schemes: + - https + - http +basePath: /_matrix/client/api/v1 +consumes: + - application/json +produces: + - application/json +securityDefinitions: + accessToken: + type: apiKey + description: The user_id or application service access_token + name: access_token + in: query +paths: + "/rooms/{roomId}/join": + post: + summary: Start the requesting user participating in a particular room. + description: |- + This API starts a user participating in a particular room, if that user + is allowed to participate in that room. After this call, the client is + allowed to see all current state events in the room, and all subsequent + events associated with the room until the user leaves the room. + + After a user has joined a room, the room will appear as an entry in the + response of the |initialSync| API. + security: + - accessToken: [] + parameters: + - in: path + type: string + name: roomId + description: The room identifier or room alias to join. + required: true + x-example: "#monkeys:matrix.org" + responses: + 200: + description: |- + The room has been joined. + + The joined room ID must be returned in the ``room_id`` field. + examples: + application/json: |- + {"room_id": "!d41d8cd:matrix.org"} + schema: + type: object + 403: + description: |- + You do not have permission to join the room. A meaningful ``errcode`` and description error text will be returned. Example reasons for rejection are: + + - The room is invite-only and the user was not invited. + - The user has been banned from the room. + examples: + application/json: |- + {"errcode": "M_FORBIDDEN", "error": "You are not invited to this room."} + 429: + description: This request was rate-limited. + schema: + "$ref": "definitions/error.yaml" + x-alias: + canonical-link: "post-matrix-client-api-v1-rooms-roomid-join" + aliases: + - /_matrix/client/api/v1/join/{roomId} diff --git a/api/client-server/leaving.yaml b/api/client-server/leaving.yaml new file mode 100644 index 00000000..e81d812b --- /dev/null +++ b/api/client-server/leaving.yaml @@ -0,0 +1,92 @@ +swagger: '2.0' +info: + title: "Matrix Client-Server v1 Room Leaving API" + version: "1.0.0" +host: localhost:8008 +schemes: + - https + - http +basePath: /_matrix/client/api/v1 +consumes: + - application/json +produces: + - application/json +securityDefinitions: + accessToken: + type: apiKey + description: The user_id or application service access_token + name: access_token + in: query +paths: + "/rooms/{roomId}/leave": + post: + summary: Stop the requesting user participating in a particular room. + description: |- + This API stops a user participating in a particular room. + + If the user was already in the room, they will no longer be able to see + new events in the room. If the room requires an invite to join, they + will need to be re-invited before they can re-join. + + If the user was invited to the room, but had not joined, this call + serves to reject the invite. + + The user will still be allowed to retrieve history from the room which + they were previously allowed to see. + security: + - accessToken: [] + parameters: + - in: path + type: string + name: roomId + description: The room identifier to leave. + required: true + x-example: "!nkl290a:matrix.org" + responses: + 200: + description: |- + The room has been left. + examples: + application/json: |- + {} + schema: + type: object + 429: + description: This request was rate-limited. + schema: + "$ref": "definitions/error.yaml" + "/rooms/{roomId}/forget": + post: + summary: Stop the requesting user remembering about a particular room. + description: |- + This API stops a user remembering about a particular room. + + In general, history is a first class citizen in Matrix. After this API + is called, however, a user will no longer be able to retrieve history + for this room. If all users on a homeserver forget a room, the room is + eligible for deletion from that homeserver. + + If the user is currently joined to the room, they will implicitly leave + the room as part of this API call. + security: + - accessToken: [] + parameters: + - in: path + type: string + name: roomId + description: The room identifier to forget. + required: true + x-example: "!au1ba7o:matrix.org" + responses: + 200: + description: |- + The room has been forgotten. + examples: + application/json: |- + {} + schema: + type: object + 429: + description: This request was rate-limited. + schema: + "$ref": "definitions/error.yaml" diff --git a/api/client-server/v1/list_public_rooms.yaml b/api/client-server/list_public_rooms.yaml similarity index 100% rename from api/client-server/v1/list_public_rooms.yaml rename to api/client-server/list_public_rooms.yaml diff --git a/api/client-server/v1/login.yaml b/api/client-server/login.yaml similarity index 100% rename from api/client-server/v1/login.yaml rename to api/client-server/login.yaml diff --git a/api/client-server/v1/message_pagination.yaml b/api/client-server/message_pagination.yaml similarity index 100% rename from api/client-server/v1/message_pagination.yaml rename to api/client-server/message_pagination.yaml diff --git a/api/client-server/v1/sync.yaml b/api/client-server/old_sync.yaml similarity index 89% rename from api/client-server/v1/sync.yaml rename to api/client-server/old_sync.yaml index d07e9399..6dbe7e5e 100644 --- a/api/client-server/v1/sync.yaml +++ b/api/client-server/old_sync.yaml @@ -84,7 +84,7 @@ paths: type: object title: Event allOf: - - "$ref": "core-event-schema/room_event.json" + - "$ref": "../../event-schemas/schema/core-event-schema/room_event.json" 400: description: "Bad pagination ``from`` parameter." "/initialSync": @@ -131,6 +131,14 @@ paths: "type": "m.presence" } ], + "account_data": [ + { + "type": "org.example.custom.config", + "content": { + "custom_config_key": "custom_config_value" + } + } + ], "rooms": [ { "membership": "join", @@ -245,7 +253,19 @@ paths: "user_id": "@alice:localhost" } ], - "visibility": "private" + "visibility": "private", + "account_data": [ + { + "type": "m.tag", + "content": {"tags": {"work": {"order": 1}}} + }, + { + "type": "org.example.custom.room.config", + "content": { + "custom_config_key": "custom_config_value" + } + } + ] } ] } @@ -265,7 +285,7 @@ paths: type: object title: Event allOf: - - "$ref": "core-event-schema/event.json" + - "$ref": "../../event-schemas/schema/core-event-schema/event.json" rooms: type: array items: @@ -284,7 +304,7 @@ paths: title: "InviteEvent" description: "The invite event if ``membership`` is ``invite``" allOf: - - "$ref": "v1-event-schema/m.room.member" + - "$ref": "../../event-schemas/schema/m.room.member" messages: type: object title: PaginationChunk @@ -312,7 +332,7 @@ paths: type: object title: RoomEvent allOf: - - "$ref": "core-event-schema/room_event.json" + - "$ref": "../../event-schemas/schema/core-event-schema/room_event.json" required: ["start", "end", "chunk"] state: type: array @@ -325,13 +345,23 @@ paths: title: StateEvent type: object allOf: - - "$ref": "core-event-schema/state_event.json" + - "$ref": "../../event-schemas/schema/core-event-schema/state_event.json" visibility: type: string enum: ["private", "public"] description: |- Whether this room is visible to the ``/publicRooms`` API or not." + account_data: + type: array + description: |- + The private data that this user has attached to + this room. + items: + title: Event + type: object + allOf: + - "$ref": "../../event-schemas/schema/core-event-schema/event.json" required: ["room_id", "membership"] required: ["end", "rooms", "presence"] 404: @@ -368,6 +398,6 @@ paths: } schema: allOf: - - "$ref": "core-event-schema/event.json" + - "$ref": "../../event-schemas/schema/core-event-schema/event.json" 404: description: The event was not found or you do not have permission to read this event. diff --git a/api/client-server/v1/presence.yaml b/api/client-server/presence.yaml similarity index 98% rename from api/client-server/v1/presence.yaml rename to api/client-server/presence.yaml index 5684398b..33df17d2 100644 --- a/api/client-server/v1/presence.yaml +++ b/api/client-server/presence.yaml @@ -205,4 +205,4 @@ paths: type: object title: PresenceEvent allOf: - - "$ref": "core-event-schema/event.json" + - "$ref": "../../event-schemas/schema/core-event-schema/event.json" diff --git a/api/client-server/v1/profile.yaml b/api/client-server/profile.yaml similarity index 100% rename from api/client-server/v1/profile.yaml rename to api/client-server/profile.yaml diff --git a/api/client-server/v1/push_notifier.yaml b/api/client-server/push_notifier.yaml similarity index 100% rename from api/client-server/v1/push_notifier.yaml rename to api/client-server/push_notifier.yaml diff --git a/api/client-server/v1/pusher.yaml b/api/client-server/pusher.yaml similarity index 100% rename from api/client-server/v1/pusher.yaml rename to api/client-server/pusher.yaml diff --git a/api/client-server/v1/pushrules.yaml b/api/client-server/pushrules.yaml similarity index 100% rename from api/client-server/v1/pushrules.yaml rename to api/client-server/pushrules.yaml diff --git a/api/client-server/v2_alpha/receipts.yaml b/api/client-server/receipts.yaml similarity index 100% rename from api/client-server/v2_alpha/receipts.yaml rename to api/client-server/receipts.yaml diff --git a/api/client-server/v2_alpha/registration.yaml b/api/client-server/registration.yaml similarity index 100% rename from api/client-server/v2_alpha/registration.yaml rename to api/client-server/registration.yaml diff --git a/api/client-server/v1/room_send.yaml b/api/client-server/room_send.yaml similarity index 60% rename from api/client-server/v1/room_send.yaml rename to api/client-server/room_send.yaml index 9c9273df..3ab1ac40 100644 --- a/api/client-server/v1/room_send.yaml +++ b/api/client-server/room_send.yaml @@ -76,50 +76,3 @@ paths: type: string description: |- A unique identifier for the event. - "/rooms/{roomId}/send/{eventType}": - post: - summary: Send a message event to the given room. - description: |- - This endpoint can be used to send a message event to a room; however - the lack of a transaction ID means that it is possible to cause message - duplication if events are resent on error, so it is preferable to use - `PUT /_matrix/client/api/v1/rooms/{roomId}/send/{eventType}/{txnId}`_. - security: - - accessToken: [] - parameters: - - in: path - type: string - name: roomId - description: The room to send the event to. - required: true - x-example: "!636q39766251:example.com" - - in: path - type: string - name: eventType - description: The type of event to send. - required: true - x-example: "m.room.message" - - in: body - name: body - schema: - type: object - example: |- - { - "msgtype": "m.text", - "body": "hello" - } - responses: - 200: - description: "An ID for the sent event." - examples: - application/json: |- - { - "event_id": "YUwRidLecu" - } - schema: - type: object - properties: - event_id: - type: string - description: |- - A unique identifier for the event. diff --git a/api/client-server/v1/room_state.yaml b/api/client-server/room_state.yaml similarity index 100% rename from api/client-server/v1/room_state.yaml rename to api/client-server/room_state.yaml diff --git a/api/client-server/v1/rooms.yaml b/api/client-server/rooms.yaml similarity index 94% rename from api/client-server/v1/rooms.yaml rename to api/client-server/rooms.yaml index 90b51290..f7654f14 100644 --- a/api/client-server/v1/rooms.yaml +++ b/api/client-server/rooms.yaml @@ -173,7 +173,7 @@ paths: title: StateEvent type: object allOf: - - "$ref": "core-event-schema/state_event.json" + - "$ref": "../../event-schemas/schema/core-event-schema/state_event.json" 403: description: > You aren't a member of the room and weren't previously a @@ -311,7 +311,11 @@ paths: "user_id": "@alice:example.com" } ], - "visibility": "private" + "visibility": "private", + "account_data": [{ + "type": "m.tag", + "content": {"tags": {"work": {"order": "1"}}} + }] } schema: title: RoomInfo @@ -351,7 +355,7 @@ paths: type: object title: RoomEvent allOf: - - "$ref": "core-event-schema/room_event.json" + - "$ref": "../../event-schemas/schema/core-event-schema/room_event.json" required: ["start", "end", "chunk"] state: type: array @@ -364,13 +368,22 @@ paths: title: StateEvent type: object allOf: - - "$ref": "core-event-schema/state_event.json" + - "$ref": "../../event-schemas/schema/core-event-schema/state_event.json" visibility: type: string enum: ["private", "public"] description: |- Whether this room is visible to the ``/publicRooms`` API or not." + account_data: + type: array + description: |- + The private data that this user has attached to this room. + items: + title: Event + type: object + allOf: + - "$ref": "../../event-schemas/schema/core-event-schema/event.json" required: ["room_id"] 403: description: > @@ -440,7 +453,7 @@ paths: title: MemberEvent type: object allOf: - - "$ref": "v1-event-schema/m.room.member" + - "$ref": "../../event-schemas/schema/m.room.member" 403: description: > You aren't a member of the room and weren't previously a diff --git a/api/client-server/v1/search.yaml b/api/client-server/search.yaml similarity index 96% rename from api/client-server/v1/search.yaml rename to api/client-server/search.yaml index a5cc9422..a9eaa085 100644 --- a/api/client-server/v1/search.yaml +++ b/api/client-server/search.yaml @@ -178,7 +178,7 @@ paths: title: Event description: The event that matched. allOf: - - "$ref": "core-event-schema/room_event.json" + - "$ref": "../../event-schemas/schema/core-event-schema/room_event.json" context: type: object title: Event Context @@ -218,7 +218,7 @@ paths: type: object title: Event allOf: - - "$ref": "core-event-schema/room_event.json" + - "$ref": "../../event-schemas/schema/core-event-schema/room_event.json" events_after: type: array title: Events After @@ -227,7 +227,7 @@ paths: type: object title: Event allOf: - - "$ref": "core-event-schema/room_event.json" + - "$ref": "../../event-schemas/schema/core-event-schema/room_event.json" state: type: object title: Current state @@ -238,7 +238,7 @@ paths: type: object title: Event allOf: - - "$ref": "core-event-schema/room_event.json" + - "$ref": "../../event-schemas/schema/core-event-schema/room_event.json" groups: type: object title: Groups diff --git a/api/client-server/v2_alpha/sync.yaml b/api/client-server/sync.yaml similarity index 72% rename from api/client-server/v2_alpha/sync.yaml rename to api/client-server/sync.yaml index a2d5a2b8..675eda5d 100644 --- a/api/client-server/v2_alpha/sync.yaml +++ b/api/client-server/sync.yaml @@ -95,33 +95,26 @@ paths: description: |- Updates to rooms. properties: - joined: - title: Joined + join: + title: Joined Rooms type: object + description: |- + The rooms that the user has joined. additionalProperties: title: Joined Room type: object properties: - event_map: - title: EventMap - type: object - description: |- - A map from event ID to events for this room. The - events are referenced from the ``timeline`` and - ``state`` keys for this room. - additionalProperties: - title: Event - description: An event object. - type: object - allOf: - - $ref: "core-event-schema/event.json" state: title: State type: object description: |- - The state updates for the room. + Updates to the state, between the time indicated by + the ``since`` parameter, and the start of the + ``timeline`` (or all state up to the start of the + ``timeline``, if ``since`` is not given, or + ``full_state`` is true). allOf: - - $ref: "definitions/room_event_batch.json" + - $ref: "definitions/event_batch.json" timeline: title: Timeline type: object @@ -139,8 +132,16 @@ paths: e.g. typing. allOf: - $ref: "definitions/event_batch.json" - invited: - title: Invited + account_data: + title: Account Data + type: object + description: |- + The private data that this user has attached to + this room. + allOf: + - $ref: "definitions/event_batch.json" + invite: + title: Invited Rooms type: object description: |- The rooms that the user has been invited to. @@ -166,37 +167,22 @@ paths: ``invite_state``. allOf: - $ref: "definitions/event_batch.json" - archived: - title: Archived + leave: + title: Left rooms type: object description: |- - The rooms that the user has left or been banned from. The - entries in the room_map will lack an ``ephemeral`` key. + The rooms that the user has left or been banned from. additionalProperties: - title: Archived Room + title: Left Room type: object properties: - event_map: - title: EventMap - type: object - description: |- - A map from event ID to events for this room. The - events are referenced from the ``timeline`` and - ``state`` keys for this room. - additionalProperties: - title: Event - description: An event object. - type: object - allOf: - - $ref: "core-event-schema/event.json" state: title: State type: object description: |- - The state updates for the room up to the point when - the user left. + The state updates for the room up to the start of the timeline. allOf: - - $ref: "definitions/room_event_batch.json" + - $ref: "definitions/event_batch.json" timeline: title: Timeline type: object @@ -225,48 +211,54 @@ paths: } ] }, + "account_data": { + "events": [ + { + "type": "org.example.custom.config", + "content": { + "custom_config_key": "custom_config_value" + } + } + ] + }, "rooms": { - "joined": { + "join": { "!726s6s6q:example.com": { - "event_map": { - "$66697273743031:example.com": { - "sender": "@alice:example.com", - "type": "m.room.member", - "state_key": "@alice:example.com", - "content": {"membership": "join"}, - "origin_server_ts": 1417731086795 - }, - "$7365636s6r6432:example.com": { - "sender": "@bob:example.com", - "type": "m.room.member", - "state_key": "@bob:example.com", - "content": {"membership": "join"}, - "unsigned": { - "prev_content": {"membership": "invite"} - }, - "origin_server_ts": 1417731086795 - }, - "$74686972643033:example.com": { - "sender": "@alice:example.com", - "type": "m.room.message", - "unsigned": {"age": "124524", "txn_id": "1234"}, - "content": { - "body": "I am a fish", - "msgtype": "m.text" - }, - "origin_server_ts": 1417731086797 - } - }, "state": { "events": [ - "$66697273743031:example.com", - "$7365636s6r6432:example.com" + { + "sender": "@alice:example.com", + "type": "m.room.member", + "state_key": "@alice:example.com", + "content": {"membership": "join"}, + "origin_server_ts": 1417731086795, + "event_id": "$66697273743031:example.com" + } ] }, "timeline": { "events": [ - "$7365636s6r6432:example.com", - "$74686972643033:example.com" + { + "sender": "@bob:example.com", + "type": "m.room.member", + "state_key": "@bob:example.com", + "content": {"membership": "join"}, + "prev_content": {"membership": "invite"}, + "origin_server_ts": 1417731086795, + "event_id": "$7365636s6r6432:example.com" + }, + { + "sender": "@alice:example.com", + "type": "m.room.message", + "age": 124524, + "txn_id": "1234", + "content": { + "body": "I am a fish", + "msgtype": "m.text" + }, + "origin_server_ts": 1417731086797, + "event_id": "$74686972643033:example.com" + } ], "limited": true, "prev_batch": "t34-23535_0_0" @@ -274,15 +266,28 @@ paths: "ephemeral": { "events": [ { - "room_id": "!726s6s6q:example.com", "type": "m.typing", "content": {"user_ids": ["@alice:example.com"]} } ] + }, + "account_data": { + "events": [ + { + "type": "m.tag", + "content": {"tags": {"work": {"order": 1}}} + }, + { + "type": "org.example.custom.room.config", + "content": { + "custom_config_key": "custom_config_value" + } + } + ] } } }, - "invited": { + "invite": { "!696r7674:example.com": { "invite_state": { "events": [ @@ -302,6 +307,6 @@ paths: } } }, - "archived": {} + "leave": {} } } diff --git a/api/client-server/tags.yaml b/api/client-server/tags.yaml new file mode 100644 index 00000000..91945dca --- /dev/null +++ b/api/client-server/tags.yaml @@ -0,0 +1,147 @@ +swagger: '2.0' +info: + title: "Matrix Client-Server tag API" + version: "1.0.0" +host: localhost:8008 +schemes: + - https + - http +basePath: /_matrix/client/v2_alpha +consumes: + - application/json +produces: + - application/json +securityDefinitions: + accessToken: + type: apiKey + description: The user_id or application service access_token + name: access_token + in: query +paths: + "/user/{userId}/rooms/{roomId}/tags": + get: + summary: List the tags for a room. + description: |- + List the tags set by a user on a room. + security: + - accessToken: [] + parameters: + - in: path + type: string + name: userId + required: true + description: |- + The id of the user to get tags for. The access token must be + authorized to make requests for this user id. + x-example: "@alice:example.com" + - in: path + type: string + name: roomId + required: true + description: |- + The id of the room to get tags for. + x-example: "!726s6s6q:example.com" + responses: + 200: + description: + The list of tags for the user for the room. + schema: + type: object + properties: + tags: + title: Tags + type: object + examples: + application/json: |- + { + "tags": { + "work": {"order": "1"}, + "pinned": {} + } + } + "/user/{userId}/rooms/{roomId}/tags/{tag}": + put: + summary: Add a tag to a room. + description: |- + Add a tag to the room. + security: + - accessToken: [] + parameters: + - in: path + type: string + name: userId + required: true + description: |- + The id of the user to add a tag for. The access token must be + authorized to make requests for this user id. + x-example: "@alice:example.com" + - in: path + type: string + name: roomId + required: true + description: |- + The id of the room to add a tag to. + x-example: "!726s6s6q:example.com" + - in: path + type: string + name: tag + required: true + description: |- + The tag to add. + x-example: "work" + - in: body + name: body + required: true + description: |- + Extra data for the tag, e.g. ordering. + schema: + type: object + example: |- + {"order": "1"} + responses: + 200: + description: + The tag was successfully added. + schema: + type: object + examples: + application/json: |- + {} + delete: + summary: Remove a tag from the room. + description: |- + Remove a tag from the room. + security: + - access_token: [] + parameters: + - in: path + type: string + name: userId + required: true + description: |- + The id of the user to remove a tag for. The access token must be + authorized to make requests for this user id. + x-example: "@alice:example.com" + - in: path + type: string + name: roomId + required: true + description: |- + The id of the room to remove a tag from. + x-example: "!726s6s6q:example.com" + - in: path + type: string + name: tag + required: true + description: |- + The tag to remove. + x-example: "work" + responses: + 200: + description: + The tag was successfully removed + schema: + type: object + examples: + application/json: |- + {} diff --git a/api/client-server/v1/third_party_membership.yaml b/api/client-server/third_party_membership.yaml similarity index 100% rename from api/client-server/v1/third_party_membership.yaml rename to api/client-server/third_party_membership.yaml diff --git a/api/client-server/v1/typing.yaml b/api/client-server/typing.yaml similarity index 100% rename from api/client-server/v1/typing.yaml rename to api/client-server/typing.yaml diff --git a/api/client-server/v1/core-event-schema b/api/client-server/v1/core-event-schema deleted file mode 120000 index 045aecb0..00000000 --- a/api/client-server/v1/core-event-schema +++ /dev/null @@ -1 +0,0 @@ -v1-event-schema/core-event-schema \ No newline at end of file diff --git a/api/client-server/v1/v1-event-schema b/api/client-server/v1/v1-event-schema deleted file mode 120000 index 7a0d0326..00000000 --- a/api/client-server/v1/v1-event-schema +++ /dev/null @@ -1 +0,0 @@ -../../../event-schemas/schema/v1 \ No newline at end of file diff --git a/api/client-server/v2_alpha/core-event-schema b/api/client-server/v2_alpha/core-event-schema deleted file mode 120000 index b020e6da..00000000 --- a/api/client-server/v2_alpha/core-event-schema +++ /dev/null @@ -1 +0,0 @@ -../../../event-schemas/schema/v1/core-event-schema \ No newline at end of file diff --git a/api/client-server/v2_alpha/definitions/definitions b/api/client-server/v2_alpha/definitions/definitions deleted file mode 120000 index 945c9b46..00000000 --- a/api/client-server/v2_alpha/definitions/definitions +++ /dev/null @@ -1 +0,0 @@ -. \ No newline at end of file diff --git a/api/client-server/v2_alpha/definitions/error.yaml b/api/client-server/v2_alpha/definitions/error.yaml deleted file mode 100644 index 20312ae4..00000000 --- a/api/client-server/v2_alpha/definitions/error.yaml +++ /dev/null @@ -1,10 +0,0 @@ -type: object -description: A Matrix-level Error -properties: - errcode: - type: string - description: An error code. - error: - type: string - description: A human-readable error message. -required: ["errcode"] \ No newline at end of file diff --git a/api/client-server/v2_alpha/definitions/room_event_batch.json b/api/client-server/v2_alpha/definitions/room_event_batch.json deleted file mode 100644 index fcf148f3..00000000 --- a/api/client-server/v2_alpha/definitions/room_event_batch.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "type": "object", - "properties": { - "events": { - "type": "array", - "description": "List of event ids", - "items": { - "type": "string" - } - } - } -} diff --git a/api/client-server/v2_alpha/definitions/timeline_batch.json b/api/client-server/v2_alpha/definitions/timeline_batch.json deleted file mode 100644 index ddf8d341..00000000 --- a/api/client-server/v2_alpha/definitions/timeline_batch.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "type": "object", - "allOf": [{"$ref":"definitions/room_event_batch.json"}], - "properties": { - "limited": { - "type": "boolean", - "description": "Whether there are more events on the server" - }, - "prev_batch": { - "type": "string", - "description": "If the batch was limited then this is a token that can be supplied to the server to retrieve more events" - } - } -} diff --git a/api/client-server/v1/voip.yaml b/api/client-server/voip.yaml similarity index 100% rename from api/client-server/v1/voip.yaml rename to api/client-server/voip.yaml diff --git a/drafts/markjh_end_to_end.rst b/drafts/markjh_end_to_end.rst new file mode 100644 index 00000000..07390b5e --- /dev/null +++ b/drafts/markjh_end_to_end.rst @@ -0,0 +1,146 @@ +Goals of Key-Distribution in Matrix +=================================== + +* No Central Authority: Users should not need to trust a central authority + when determining the authenticity of keys. + +* Easy to Add New Devices: It should be easy for a user to start using a + new device. + +* Possible to discover MITM: It should be possible for a user to determine if + they are being MITM. + +* Lost Devices: It should be possible for a user to recover if they lose all + their devices. + +* No Copying Keys: Keys should be per device and shouldn't leave the device + they were created on. + +A Possible Mechanism for Key Distribution +========================================= + +Basic API for setting up keys on a server: + +https://github.com/matrix-org/matrix-doc/pull/24 + +Client shouldn't trust the keys unless they have been verified, e.g by +comparing fingerprints. + +If a user adds a new device it should some yet to be specified protocol +communicate with an old device and obtain a cross-signature from the old +device for its public key. + +The new device can then present the cross-signed key to all the devices +that the user is in conversations with. Those devices should then include +the new device into those conversations. + +If the user cannot cross-sign the new key, e.g. because their old device +is lost or stolen. Then they will need to reauthenticate their conversations +out of band, e.g by comparing fingerprints. + + +Goals of End-to-end encryption in Matrix +======================================== + +* Access to Chat History: Users should be able to see the history of a + conversation on a new device. User should be able to control who can + see their chat history and how much of the chat history they can see. + +* Forward Secrecy of Discarded Chat History: Users should be able to discard + history from their device, once they have discarded the history it should be + impossible for an adversary to recover that history. + +* Forward Secrecy of Future Messages: Users should be able to recover from + disclosure of the chat history on their device. + +* Deniablity of Chat History: It should not be possible to prove to a third + party that a given user sent a message. + +* Authenticity of Chat History: It should be possible to prove amoungst + the members of a chat that a message sent by a user was authored by that + user. + + +Bonus Goals: + +* Traffic Analysis: It would be nice if the protocol was resilient to traffic + or metadata analysis. However it's not something we want to persue if it + harms the usability of the protocol. It might be cool if there was a + way for the user to could specify the trade off between performance and + resilience to traffic analysis that they wanted. + + +A Possible Design for Group Chat using Olm +========================================== + +Protecting the secrecy of history +--------------------------------- + +Each message sent by a client has a 32-bit counter. This counter increments +by one for each message sent by the client. This counter is used to advance a +ratchet. The ratchet is split into a vector four 256-bit values, +:math:`R_{n,j}` for :math:`j \in {0,1,2,3}`. The ratchet can be advanced as +follows: + +.. math:: + \begin{align} + R_{2^24n,0} &= H_0\left(R_{2^24(i-1),0}\right) \\ + R_{2^24n,1} &= H_1\left(R_{2^24(i-1),0}\right) \\ + R_{2^24n,2} &= H_2\left(R_{2^24(i-1),0}\right) \\ + R_{2^24n,3} &= H_3\left(R_{2^24(i-1),0}\right) \\ + R_{2^16n,1} &= H_1\left(R_{2^16(i-1),1}\right) \\ + R_{2^16n,2} &= H_2\left(R_{2^16(i-1),1}\right) \\ + R_{2^16n,3} &= H_3\left(R_{2^16(i-1),1}\right) \\ + R_{2^8i,2} &= H_2\left(R_{2^8(i-1),2}\right) \\ + R_{2^8i,3} &= H_3\left(R_{2^8(i-1),2}\right) \\ + R_{i,3} &= H_3\left(R_{(i-1),3}\right) + \end{align} + +Where :math:`H_0`, :math:`H_1`, :math:`H_2`, and :math:`H_3` +are different hash functions. For example +:math:`H_0` could be :math:`HMAC\left(X,\text{"\textbackslash x00"}\right)` and +:math:`H_1` could be :math:`HMAC\left(X,\text{"\textbackslash x01"}\right)`. + +So every :math:`2^24` iterations :math:`R_{n,1}` is reseeded from :math:`R_{n,0}`. +Every :math:`2^16` iterations :math:`R_{n,2}` is reseeded from :math:`R_{n,1}`. +Every :math:`2^8` iterations :math:`R_{n,3}` is reseeded from :math:`R_{n,2}`. + +This scheme allows the ratchet to be advanced an arbitrary amount forwards +while needing only 1024 hash computations. + +This the value of the ratchet is hashed to generate the keys used to encrypt +each mesage. + +A client can decrypt chat history onwards from the earliest value of the +ratchet it is aware of. But cannot decrypt history from before that point +without reversing the hash function. + +This allows a client to share its ability to decrypt chat history with another +from a point in the conversation onwards by giving a copy of the ratchet at +that point in the conversation. + +A client can discard history by advancing a ratchet to beyond the last message +they want to discard and then forgetting all previous values of the ratchet. + +Proving and denying the authenticity of history +----------------------------------------------- + +Client sign the messages they send using a Ed25519 key generated per +conversation. That key, along with the ratchet key, is distributed +to other clients using 1:1 olm ratchets. Those 1:1 ratchets are started using +Triple Diffie-Hellman which provides authenticity of the messages to the +participants and deniability of the messages to third parties. Therefore +any keys shared over those keys inherit the same levels of deniability and +authenticity. + +Protecting the secrecy of future messages +----------------------------------------- + +A client would need to generate new keys if it wanted to prevent access to +messages beyond a given point in the conversation. It must generate new keys +whenever someone leaves the room. It should generate new keys periodically +anyway. + +The frequency of key generation in a large room may need to be restricted to +keep the frequency of messages broadcast over the individual 1:1 channels +low. diff --git a/drafts/websockets.rst b/drafts/websockets.rst new file mode 100644 index 00000000..bd0ff081 --- /dev/null +++ b/drafts/websockets.rst @@ -0,0 +1,285 @@ +WebSockets API +============== + +Introduction +------------ +This document is a proposal for a WebSockets-based client-server API. It is not +intended to replace the REST API, but rather to complement it and provide an +alternative interface for certain operations. + +The primary goal is to offer a more efficient interface than the REST API: by +using a bidirectional protocol such as WebSockets we can avoid the overheads +involved in long-polling (SSL negotiation, HTTP headers, etc). In doing so we +will reduce the latency between server and client by allowing the server to +send events as soon as they arrive, rather than having to wait for a poll from +the client. + +Handshake +--------- +1. Instead of calling ``/sync``, the client makes a websocket request to + ``/_matrix/client/rN/stream``, passing the query parameters ``access_token`` + and ``since``, and optionally ``filter`` - all of which have the same + meaning as for ``/sync``. + + * The client sets the ``Sec-WebSocket-Protocol`` to ``m.json``. (Servers may + offer alternative encodings; at present only the JSON encoding is + specified but in future we will specify alternative encodings.) + +#. The server returns the websocket handshake; the socket is then connected. + +If the server does not return a valid websocket handshake, this indicates that +the server or an intermediate proxy does not support WebSockets. In this case, +the client should fall back to polling the ``/sync`` REST endpoint. + +Example +~~~~~~~ + +Client request: + +.. code:: http + + GET /_matrix/client/v2_alpha/stream?access_token=123456&since=s72594_4483_1934 HTTP/1.1 + Host: matrix.org + Upgrade: websocket + Connection: Upgrade + Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw== + Sec-WebSocket-Protocol: m.json + Sec-WebSocket-Version: 13 + Origin: https://matrix.org + +Server response: + +.. code:: http + + HTTP/1.1 101 Switching Protocols + Upgrade: websocket + Connection: Upgrade + Sec-WebSocket-Accept: HSmrc0sMlYUkAGmm5OPpG2HaGWk= + Sec-WebSocket-Protocol: m.json + + +Update Notifications +-------------------- +Once the socket is connected, the server begins streaming updates over the +websocket. The server sends Update notifications about new messages or state +changes. To make it easy for clients to parse, Update notifications have the +same structure as the response to ``/sync``: an object with the following +members: + +============= ========== =================================================== +Key Type Description +============= ========== =================================================== +next_batch string The batch token to supply in the ``since`` param of + the next /sync request. This is not required for + streaming of events over the WebSocket, but is + provided so that clients can reconnect if the + socket is disconnected. +presence Presence The updates to the presence status of other users. +rooms Rooms Updates to rooms. +============= ========== =================================================== + +Example +~~~~~~~ +Message from the server: + +.. code:: json + + { + "next_batch": "s72595_4483_1934", + "presence": { + "events": [] + }, + "rooms": { + "join": {}, + "invite": {}, + "leave": {} + } + } + + +Client-initiated operations +--------------------------- + +The client can perform certain operations by sending a websocket message to +the server. Such a "Request" message should be a JSON-encoded object with +the following members: + +============= ========== =================================================== +Key Type Description +============= ========== =================================================== +id string A unique identifier for this request +method string Specifies the name of the operation to be + performed; see below for available operations +param object The parameters for the requested operation. +============= ========== =================================================== + +The server responds to a client Request with a Response message. This is a +JSON-encoded object with the following members: + +============= ========== =================================================== +Key Type Description +============= ========== =================================================== +id string The same as the value in the corresponding Request + object. The presence of the ``id`` field + distinguishes a Response message from an Update + notification. +result object On success, the results of the request. +error object On error, an object giving the resons for the + error. This has the same structure as the "standard + error response" for the Matrix API: an object with + the fields ``errcode`` and ``error``. +============= ========== =================================================== + +Request methods +~~~~~~~~~~~~~~~ +It is not intended that all operations which are available via the REST API +will be available via the WebSockets API, but a few simple, common operations +will be exposed. The initial operations will be as follows. + +``ping`` +^^^^^^^^ +This is a no-op which clients may use to keep their connection alive. + +The request ``params`` and the response ``result`` should be empty. + +``send`` +^^^^^^^^ +Send a message event to a room. The parameters are as follows: + +============= ========== =================================================== +Parameter Type Description +============= ========== =================================================== +room_id string **Required.** The room to send the event to +event_type string **Required.** The type of event to send. +content object **Required.** The content of the event. +============= ========== =================================================== + +The result is as follows: + +============= ========== =================================================== +Key Type Description +============= ========== =================================================== +event_id string A unique identifier for the event. +============= ========== =================================================== + +The ``id`` from the Request message is used as the transaction ID by the +server. + +``state`` +^^^^^^^^^ +Update the state on a room. + +============= ========== =================================================== +Parameter Type Description +============= ========== =================================================== +room_id string **Required.** The room to set the state in +event_type string **Required.** The type of event to send. +state_key string **Required.** The state_key for the state to send. +content object **Required.** The content of the event. +============= ========== =================================================== + +The result is as follows: + +============= ========== =================================================== +Key Type Description +============= ========== =================================================== +event_id string A unique identifier for the event. +============= ========== =================================================== + + +Example +~~~~~~~ +Client request: + +.. code:: json + + { + "id": "12345", + "method": "send", + "params": { + "room_id": "!d41d8cd:matrix.org", + "event_type": "m.room.message", + "content": { + "msgtype": "m.text", + "body": "hello" + } + } + } + +Server response: + +.. code:: json + + { + "id": "12345", + "result": { + "event_id": "$66697273743031:matrix.org" + } + } + +Alternative server response, in case of error: + +.. code:: json + + { + "id": "12345", + "error": { + "errcode": "M_MISSING_PARAM", + "error": "Missing parameter: event_type" + } + } + + +Rationale +--------- +Alternatives to WebSockets include HTTP/2, CoAP, and simply rolling our own +protocol over raw TCP sockets. However, the need to implement browser-based +clients essentially reduces our choice to WebSockets. HTTP/2 streams will +probably provide an interesting alternative in the future, but current browsers +do not appear to give javascript applications low-level access to the protocol. + +Concerning the continued use of the JSON encoding: we prefer to focus on the +transition to WebSockets initially. Replacing JSON with a compact +representation such as CBOR, MessagePack, or even just compressed JSON will be +a likely extension for the future. The support for negotiation of subprotocols +within WebSockets should make this a simple transition once time permits. + +The number of methods available for client requests is deliberately limited, as +each method requires code to be written to map it onto the equivalent REST +implementation. Some REST methods - for instance, user registration and login - +would be pointless to expose via WebSockets. It is likely, however, that we +will increate the number of methods available via the WebSockets API as it +becomes clear which would be most useful. + +Open questions +-------------- + +Throttling +~~~~~~~~~~ +At least in v2 sync, clients are inherently self-throttling - if they do not +poll quickly enough, events will be dropped from the next result. This proposal +raises the possibility that events will be produced more quickly than they can +be sent to the client; backlogs will build up on the server and/or in the +intermediate network, which will not only lead to high latency on events being +delivered, but will lead to responses to client requests also being delayed. + +We may need to implement some sort of throttling mechanism by which the server +can start to drop events. The difficulty is in knowing when to start dropping +events. A few ideas: + +* Use websocket pings to measure the RTT; if it starts to increase, start + dropping events. But this requires knowledge of the base RTT, and a useful + model of what constitutes an excessive increase. + +* Have the client acknowledge each batch of events, and use a window to ensure + the number of outstanding batches is limited. This is annoying as it requires + the client to have to acknowledge batches - and it's not clear what the right + window size is: we want a big window for long fat networks (think of mobile + clients), but a small one for one with lower latency. + +* Start dropping events if the server's TCP buffer starts filling up. This has + the advantage of delegating the congestion-detection to TCP (which already + has a number of algorithms to deal with it, to greater or lesser + effectiveness), but relies on homeservers being hosted on OSes which use + sensible TCP congestion-avoidance algorithms, and more critically, an ability + to read the fill level of the TCP send buffer. diff --git a/event-schemas/README.md b/event-schemas/README.md index 1030a04c..950c3018 100644 --- a/event-schemas/README.md +++ b/event-schemas/README.md @@ -7,7 +7,7 @@ resolved correctly. For basic CLI testing, we recommend and have verified they work with the Node.js package [z-schema](https://github.com/zaggino/z-schema): ``` $ npm install -g z-schema - $ z-schema schema/v1/m.room.message examples/v1/m.room.message_m.text + $ z-schema schema/m.room.message examples/m.room.message_m.text schema validation passed json #1 validation passed ``` diff --git a/event-schemas/check.sh b/event-schemas/check.sh deleted file mode 100755 index a6d03b5a..00000000 --- a/event-schemas/check.sh +++ /dev/null @@ -1,22 +0,0 @@ -#!/bin/bash -e -# Runs z-schema over all of the schema files (looking for matching examples) - -if ! which z-schema; then - echo >&2 "Need to install z-schema; run: sudo npm install -g z-schema" - exit 1 -fi - -find schema/v1/m.* | while read line -do - split_path=(${line///// }) - event_type=(${split_path[2]}) - echo "Checking $event_type" - echo "--------------------" - # match exact name or exact name with a # - find examples/v1 -name $event_type -o -name "$event_type#*" | while read exline - do - echo " against $exline" - # run z-schema: because of bash -e if this fails we bail with exit code 1 - z-schema schema/v1/$event_type $exline - done -done diff --git a/event-schemas/check_examples.py b/event-schemas/check_examples.py index e54d3a1c..5a409407 100755 --- a/event-schemas/check_examples.py +++ b/event-schemas/check_examples.py @@ -1,4 +1,4 @@ -#! /usr/bin/env python +#!/usr/bin/env python import sys import json @@ -60,6 +60,8 @@ def check_example_dir(exampledir, schemadir): continue examplepath = os.path.join(root, filename) schemapath = examplepath.replace(exampledir, schemadir) + if schemapath.find("#") >= 0: + schemapath = schemapath[:schemapath.find("#")] try: check_example_file(examplepath, schemapath) except Exception as e: diff --git a/event-schemas/examples/v1/m.call.answer b/event-schemas/examples/m.call.answer similarity index 100% rename from event-schemas/examples/v1/m.call.answer rename to event-schemas/examples/m.call.answer diff --git a/event-schemas/examples/v1/m.call.candidates b/event-schemas/examples/m.call.candidates similarity index 100% rename from event-schemas/examples/v1/m.call.candidates rename to event-schemas/examples/m.call.candidates diff --git a/event-schemas/examples/v1/m.call.hangup b/event-schemas/examples/m.call.hangup similarity index 100% rename from event-schemas/examples/v1/m.call.hangup rename to event-schemas/examples/m.call.hangup diff --git a/event-schemas/examples/v1/m.call.invite b/event-schemas/examples/m.call.invite similarity index 100% rename from event-schemas/examples/v1/m.call.invite rename to event-schemas/examples/m.call.invite diff --git a/event-schemas/examples/v1/m.presence b/event-schemas/examples/m.presence similarity index 100% rename from event-schemas/examples/v1/m.presence rename to event-schemas/examples/m.presence diff --git a/event-schemas/examples/v1/m.receipt b/event-schemas/examples/m.receipt similarity index 100% rename from event-schemas/examples/v1/m.receipt rename to event-schemas/examples/m.receipt diff --git a/event-schemas/examples/v1/m.room.aliases b/event-schemas/examples/m.room.aliases similarity index 100% rename from event-schemas/examples/v1/m.room.aliases rename to event-schemas/examples/m.room.aliases diff --git a/event-schemas/examples/v1/m.room.avatar b/event-schemas/examples/m.room.avatar similarity index 100% rename from event-schemas/examples/v1/m.room.avatar rename to event-schemas/examples/m.room.avatar diff --git a/event-schemas/examples/v1/m.room.canonical_alias b/event-schemas/examples/m.room.canonical_alias similarity index 100% rename from event-schemas/examples/v1/m.room.canonical_alias rename to event-schemas/examples/m.room.canonical_alias diff --git a/event-schemas/examples/v1/m.room.create b/event-schemas/examples/m.room.create similarity index 100% rename from event-schemas/examples/v1/m.room.create rename to event-schemas/examples/m.room.create diff --git a/event-schemas/examples/v1/m.room.guest_access b/event-schemas/examples/m.room.guest_access similarity index 100% rename from event-schemas/examples/v1/m.room.guest_access rename to event-schemas/examples/m.room.guest_access diff --git a/event-schemas/examples/v1/m.room.history_visibility b/event-schemas/examples/m.room.history_visibility similarity index 100% rename from event-schemas/examples/v1/m.room.history_visibility rename to event-schemas/examples/m.room.history_visibility diff --git a/event-schemas/examples/v1/m.room.join_rules b/event-schemas/examples/m.room.join_rules similarity index 100% rename from event-schemas/examples/v1/m.room.join_rules rename to event-schemas/examples/m.room.join_rules diff --git a/event-schemas/examples/v1/m.room.member b/event-schemas/examples/m.room.member similarity index 100% rename from event-schemas/examples/v1/m.room.member rename to event-schemas/examples/m.room.member diff --git a/event-schemas/examples/m.room.member#invite_room_state b/event-schemas/examples/m.room.member#invite_room_state new file mode 100644 index 00000000..e2ca5668 --- /dev/null +++ b/event-schemas/examples/m.room.member#invite_room_state @@ -0,0 +1,30 @@ +{ + "age": 242352, + "content": { + "membership": "join", + "avatar_url": "mxc://localhost/SEsfnsuifSDFSSEF#auto", + "displayname": "Alice Margatroid" + }, + "invite_room_state": [ + { + "type": "m.room.name", + "state_key": "", + "content": { + "name": "Forest of Magic" + } + }, + { + "type": "m.room.join_rules", + "state_key": "", + "content": { + "join_rules": "invite" + } + } + ], + "state_key": "@alice:localhost", + "origin_server_ts": 1431961217939, + "event_id": "$WLGTSEFSEF:localhost", + "type": "m.room.member", + "room_id": "!Cuyf34gef24t:localhost", + "user_id": "@example:localhost" +} diff --git a/event-schemas/examples/m.room.member#third_party_invite b/event-schemas/examples/m.room.member#third_party_invite new file mode 100644 index 00000000..2457302a --- /dev/null +++ b/event-schemas/examples/m.room.member#third_party_invite @@ -0,0 +1,25 @@ +{ + "age": 242352, + "content": { + "membership": "join", + "avatar_url": "mxc://localhost/SEsfnsuifSDFSSEF#auto", + "displayname": "Alice Margatroid", + "third_party_invite": { + "signed": { + "mxid": "@alice:localhost", + "signatures": { + "magic.forest": { + "ed25519:3": "fQpGIW1Snz+pwLZu6sTy2aHy/DYWWTspTJRPyNp0PKkymfIsNffysMl6ObMMFdIJhk6g6pwlIqZ54rxo8SLmAg" + } + }, + "token": "abc123" + } + } + }, + "state_key": "@alice:localhost", + "origin_server_ts": 1431961217939, + "event_id": "$WLGTSEFSEF:localhost", + "type": "m.room.member", + "room_id": "!Cuyf34gef24t:localhost", + "user_id": "@example:localhost" +} diff --git a/event-schemas/examples/v1/m.room.message#m.audio b/event-schemas/examples/m.room.message#m.audio similarity index 100% rename from event-schemas/examples/v1/m.room.message#m.audio rename to event-schemas/examples/m.room.message#m.audio diff --git a/event-schemas/examples/v1/m.room.message#m.emote b/event-schemas/examples/m.room.message#m.emote similarity index 100% rename from event-schemas/examples/v1/m.room.message#m.emote rename to event-schemas/examples/m.room.message#m.emote diff --git a/event-schemas/examples/v1/m.room.message#m.file b/event-schemas/examples/m.room.message#m.file similarity index 100% rename from event-schemas/examples/v1/m.room.message#m.file rename to event-schemas/examples/m.room.message#m.file diff --git a/event-schemas/examples/v1/m.room.message#m.image b/event-schemas/examples/m.room.message#m.image similarity index 100% rename from event-schemas/examples/v1/m.room.message#m.image rename to event-schemas/examples/m.room.message#m.image diff --git a/event-schemas/examples/v1/m.room.message#m.location b/event-schemas/examples/m.room.message#m.location similarity index 100% rename from event-schemas/examples/v1/m.room.message#m.location rename to event-schemas/examples/m.room.message#m.location diff --git a/event-schemas/examples/v1/m.room.message#m.notice b/event-schemas/examples/m.room.message#m.notice similarity index 100% rename from event-schemas/examples/v1/m.room.message#m.notice rename to event-schemas/examples/m.room.message#m.notice diff --git a/event-schemas/examples/v1/m.room.message#m.text b/event-schemas/examples/m.room.message#m.text similarity index 100% rename from event-schemas/examples/v1/m.room.message#m.text rename to event-schemas/examples/m.room.message#m.text diff --git a/event-schemas/examples/v1/m.room.message#m.video b/event-schemas/examples/m.room.message#m.video similarity index 100% rename from event-schemas/examples/v1/m.room.message#m.video rename to event-schemas/examples/m.room.message#m.video diff --git a/event-schemas/examples/v1/m.room.message.feedback b/event-schemas/examples/m.room.message.feedback similarity index 100% rename from event-schemas/examples/v1/m.room.message.feedback rename to event-schemas/examples/m.room.message.feedback diff --git a/event-schemas/examples/v1/m.room.name b/event-schemas/examples/m.room.name similarity index 100% rename from event-schemas/examples/v1/m.room.name rename to event-schemas/examples/m.room.name diff --git a/event-schemas/examples/v1/m.room.power_levels b/event-schemas/examples/m.room.power_levels similarity index 100% rename from event-schemas/examples/v1/m.room.power_levels rename to event-schemas/examples/m.room.power_levels diff --git a/event-schemas/examples/v1/m.room.redaction b/event-schemas/examples/m.room.redaction similarity index 100% rename from event-schemas/examples/v1/m.room.redaction rename to event-schemas/examples/m.room.redaction diff --git a/event-schemas/examples/v1/m.room.third_party_invite b/event-schemas/examples/m.room.third_party_invite similarity index 100% rename from event-schemas/examples/v1/m.room.third_party_invite rename to event-schemas/examples/m.room.third_party_invite diff --git a/event-schemas/examples/v1/m.room.topic b/event-schemas/examples/m.room.topic similarity index 100% rename from event-schemas/examples/v1/m.room.topic rename to event-schemas/examples/m.room.topic diff --git a/event-schemas/examples/m.tag b/event-schemas/examples/m.tag new file mode 100644 index 00000000..00e81060 --- /dev/null +++ b/event-schemas/examples/m.tag @@ -0,0 +1,8 @@ +{ + "type": "m.tag", + "content": { + "tags": { + "work": {"order": 1} + } + } +} diff --git a/event-schemas/examples/v1/m.typing b/event-schemas/examples/m.typing similarity index 100% rename from event-schemas/examples/v1/m.typing rename to event-schemas/examples/m.typing diff --git a/event-schemas/schema/v1/core-event-schema/event.json b/event-schemas/schema/core-event-schema/event.json similarity index 100% rename from event-schemas/schema/v1/core-event-schema/event.json rename to event-schemas/schema/core-event-schema/event.json diff --git a/event-schemas/schema/v1/core-event-schema/msgtype_infos/image_info.json b/event-schemas/schema/core-event-schema/msgtype_infos/image_info.json similarity index 100% rename from event-schemas/schema/v1/core-event-schema/msgtype_infos/image_info.json rename to event-schemas/schema/core-event-schema/msgtype_infos/image_info.json diff --git a/event-schemas/schema/v1/core-event-schema/room_event.json b/event-schemas/schema/core-event-schema/room_event.json similarity index 93% rename from event-schemas/schema/v1/core-event-schema/room_event.json rename to event-schemas/schema/core-event-schema/room_event.json index d5413f8a..80f7d265 100644 --- a/event-schemas/schema/v1/core-event-schema/room_event.json +++ b/event-schemas/schema/core-event-schema/room_event.json @@ -3,7 +3,7 @@ "title": "Room Event", "description": "In addition to the Event fields, Room Events MUST have the following additional field.", "allOf":[{ - "$ref": "core-event-schema/event.json" + "$ref": "event.json" }], "properties": { "room_id": { diff --git a/event-schemas/schema/v1/core-event-schema/state_event.json b/event-schemas/schema/core-event-schema/state_event.json similarity index 93% rename from event-schemas/schema/v1/core-event-schema/state_event.json rename to event-schemas/schema/core-event-schema/state_event.json index 5809cf7f..cfcb212c 100644 --- a/event-schemas/schema/v1/core-event-schema/state_event.json +++ b/event-schemas/schema/core-event-schema/state_event.json @@ -3,7 +3,7 @@ "title": "State Event", "description": "In addition to the Room Event fields, State Events have the following additional fields.", "allOf":[{ - "$ref": "core-event-schema/room_event.json" + "$ref": "room_event.json" }], "properties": { "state_key": { diff --git a/event-schemas/schema/v1/m.call.answer b/event-schemas/schema/m.call.answer similarity index 100% rename from event-schemas/schema/v1/m.call.answer rename to event-schemas/schema/m.call.answer diff --git a/event-schemas/schema/v1/m.call.candidates b/event-schemas/schema/m.call.candidates similarity index 100% rename from event-schemas/schema/v1/m.call.candidates rename to event-schemas/schema/m.call.candidates diff --git a/event-schemas/schema/v1/m.call.hangup b/event-schemas/schema/m.call.hangup similarity index 100% rename from event-schemas/schema/v1/m.call.hangup rename to event-schemas/schema/m.call.hangup diff --git a/event-schemas/schema/v1/m.call.invite b/event-schemas/schema/m.call.invite similarity index 100% rename from event-schemas/schema/v1/m.call.invite rename to event-schemas/schema/m.call.invite diff --git a/event-schemas/schema/v1/m.presence b/event-schemas/schema/m.presence similarity index 100% rename from event-schemas/schema/v1/m.presence rename to event-schemas/schema/m.presence diff --git a/event-schemas/schema/v1/m.receipt b/event-schemas/schema/m.receipt similarity index 100% rename from event-schemas/schema/v1/m.receipt rename to event-schemas/schema/m.receipt diff --git a/event-schemas/schema/v1/m.room.aliases b/event-schemas/schema/m.room.aliases similarity index 100% rename from event-schemas/schema/v1/m.room.aliases rename to event-schemas/schema/m.room.aliases diff --git a/event-schemas/schema/v1/m.room.avatar b/event-schemas/schema/m.room.avatar similarity index 100% rename from event-schemas/schema/v1/m.room.avatar rename to event-schemas/schema/m.room.avatar diff --git a/event-schemas/schema/v1/m.room.canonical_alias b/event-schemas/schema/m.room.canonical_alias similarity index 94% rename from event-schemas/schema/v1/m.room.canonical_alias rename to event-schemas/schema/m.room.canonical_alias index 25cd00c0..49e0e669 100644 --- a/event-schemas/schema/v1/m.room.canonical_alias +++ b/event-schemas/schema/m.room.canonical_alias @@ -13,8 +13,7 @@ "type": "string", "description": "The canonical alias." } - }, - "required": ["alias"] + } }, "state_key": { "type": "string", diff --git a/event-schemas/schema/v1/m.room.create b/event-schemas/schema/m.room.create similarity index 100% rename from event-schemas/schema/v1/m.room.create rename to event-schemas/schema/m.room.create diff --git a/event-schemas/schema/v1/m.room.guest_access b/event-schemas/schema/m.room.guest_access similarity index 100% rename from event-schemas/schema/v1/m.room.guest_access rename to event-schemas/schema/m.room.guest_access diff --git a/event-schemas/schema/v1/m.room.history_visibility b/event-schemas/schema/m.room.history_visibility similarity index 100% rename from event-schemas/schema/v1/m.room.history_visibility rename to event-schemas/schema/m.room.history_visibility diff --git a/event-schemas/schema/v1/m.room.join_rules b/event-schemas/schema/m.room.join_rules similarity index 100% rename from event-schemas/schema/v1/m.room.join_rules rename to event-schemas/schema/m.room.join_rules diff --git a/event-schemas/schema/v1/m.room.member b/event-schemas/schema/m.room.member similarity index 94% rename from event-schemas/schema/v1/m.room.member rename to event-schemas/schema/m.room.member index 81057049..25b30109 100644 --- a/event-schemas/schema/v1/m.room.member +++ b/event-schemas/schema/m.room.member @@ -1,7 +1,7 @@ { "type": "object", "title": "The current membership state of a user in the room.", - "description": "Adjusts the membership state for a user in a room. It is preferable to use the membership APIs (``/rooms//invite`` etc) when performing membership actions rather than adjusting the state directly as there are a restricted set of valid transformations. For example, user A cannot force user B to join a room, and trying to force this state change directly will fail. \n\nThe ``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.\n\nThis event also includes an ``invite_room_state`` key **outside the** ``content`` **key**. This contains an array of ``StrippedState`` Events. These events provide information on a few select state events such as the room name.", + "description": "Adjusts the membership state for a user in a room. It is preferable to use the membership APIs (``/rooms//invite`` etc) when performing membership actions rather than adjusting the state directly as there are a restricted set of valid transformations. For example, user A cannot force user B to join a room, and trying to force this state change directly will fail. \n\nThe ``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.\n\nThis event may also include an ``invite_room_state`` key **outside the** ``content`` **key**. If present, this contains an array of ``StrippedState`` Events. These events provide information on a few select state events such as the room name.", "allOf": [{ "$ref": "core-event-schema/state_event.json" }], diff --git a/event-schemas/schema/v1/m.room.message b/event-schemas/schema/m.room.message similarity index 100% rename from event-schemas/schema/v1/m.room.message rename to event-schemas/schema/m.room.message diff --git a/event-schemas/schema/v1/m.room.message#m.audio b/event-schemas/schema/m.room.message#m.audio similarity index 100% rename from event-schemas/schema/v1/m.room.message#m.audio rename to event-schemas/schema/m.room.message#m.audio diff --git a/event-schemas/schema/v1/m.room.message#m.emote b/event-schemas/schema/m.room.message#m.emote similarity index 100% rename from event-schemas/schema/v1/m.room.message#m.emote rename to event-schemas/schema/m.room.message#m.emote diff --git a/event-schemas/schema/v1/m.room.message#m.file b/event-schemas/schema/m.room.message#m.file similarity index 100% rename from event-schemas/schema/v1/m.room.message#m.file rename to event-schemas/schema/m.room.message#m.file diff --git a/event-schemas/schema/v1/m.room.message#m.image b/event-schemas/schema/m.room.message#m.image similarity index 100% rename from event-schemas/schema/v1/m.room.message#m.image rename to event-schemas/schema/m.room.message#m.image diff --git a/event-schemas/schema/v1/m.room.message#m.location b/event-schemas/schema/m.room.message#m.location similarity index 100% rename from event-schemas/schema/v1/m.room.message#m.location rename to event-schemas/schema/m.room.message#m.location diff --git a/event-schemas/schema/v1/m.room.message#m.notice b/event-schemas/schema/m.room.message#m.notice similarity index 100% rename from event-schemas/schema/v1/m.room.message#m.notice rename to event-schemas/schema/m.room.message#m.notice diff --git a/event-schemas/schema/v1/m.room.message#m.text b/event-schemas/schema/m.room.message#m.text similarity index 100% rename from event-schemas/schema/v1/m.room.message#m.text rename to event-schemas/schema/m.room.message#m.text diff --git a/event-schemas/schema/v1/m.room.message#m.video b/event-schemas/schema/m.room.message#m.video similarity index 100% rename from event-schemas/schema/v1/m.room.message#m.video rename to event-schemas/schema/m.room.message#m.video diff --git a/event-schemas/schema/v1/m.room.message.feedback b/event-schemas/schema/m.room.message.feedback similarity index 100% rename from event-schemas/schema/v1/m.room.message.feedback rename to event-schemas/schema/m.room.message.feedback diff --git a/event-schemas/schema/v1/m.room.name b/event-schemas/schema/m.room.name similarity index 100% rename from event-schemas/schema/v1/m.room.name rename to event-schemas/schema/m.room.name diff --git a/event-schemas/schema/v1/m.room.power_levels b/event-schemas/schema/m.room.power_levels similarity index 100% rename from event-schemas/schema/v1/m.room.power_levels rename to event-schemas/schema/m.room.power_levels diff --git a/event-schemas/schema/v1/m.room.redaction b/event-schemas/schema/m.room.redaction similarity index 100% rename from event-schemas/schema/v1/m.room.redaction rename to event-schemas/schema/m.room.redaction diff --git a/event-schemas/schema/v1/m.room.third_party_invite b/event-schemas/schema/m.room.third_party_invite similarity index 100% rename from event-schemas/schema/v1/m.room.third_party_invite rename to event-schemas/schema/m.room.third_party_invite diff --git a/event-schemas/schema/v1/m.room.topic b/event-schemas/schema/m.room.topic similarity index 100% rename from event-schemas/schema/v1/m.room.topic rename to event-schemas/schema/m.room.topic diff --git a/event-schemas/schema/m.tag b/event-schemas/schema/m.tag new file mode 100644 index 00000000..4c5b4fa5 --- /dev/null +++ b/event-schemas/schema/m.tag @@ -0,0 +1,25 @@ +{ + "type": "object", + "title": "Tag Event", + "description": "Informs the client of tags on a room.", + "properties": { + "type": { + "type": "string", + "enum": ["m.tag"] + }, + "content": { + "type": "object", + "properties": { + "tags": { + "type": "object", + "description": "The tags on the room and their contents.", + "additionalProperties": { + "title": "Tag", + "type": "object" + } + } + } + } + }, + "required": ["type", "content"] +} diff --git a/event-schemas/schema/v1/m.typing b/event-schemas/schema/m.typing similarity index 100% rename from event-schemas/schema/v1/m.typing rename to event-schemas/schema/m.typing diff --git a/event-schemas/schema/v1/core-event-schema/core-event-schema b/event-schemas/schema/v1/core-event-schema/core-event-schema deleted file mode 120000 index 945c9b46..00000000 --- a/event-schemas/schema/v1/core-event-schema/core-event-schema +++ /dev/null @@ -1 +0,0 @@ -. \ No newline at end of file diff --git a/event-schemas/schema/v1/v1-event-schema b/event-schemas/schema/v1/v1-event-schema deleted file mode 120000 index 945c9b46..00000000 --- a/event-schemas/schema/v1/v1-event-schema +++ /dev/null @@ -1 +0,0 @@ -. \ No newline at end of file diff --git a/jenkins.sh b/jenkins.sh index 0b217e58..c58ad473 100755 --- a/jenkins.sh +++ b/jenkins.sh @@ -5,5 +5,13 @@ set -ex (cd event-schemas/ && ./check_examples.py) (cd api && ./check_examples.py) (cd scripts && ./gendoc.py -v) -(cd api && npm install && node validator.js -s "client-server/v1" && node validator.js -s "client-server/v2_alpha") -(cd event-schemas/ && ./check.sh) +(cd api && npm install && node validator.js -s "client-server") + +: ${GOPATH:=${WORKSPACE}/.gopath} +mkdir -p "${GOPATH}" +export GOPATH +go get github.com/hashicorp/golang-lru +go get gopkg.in/fsnotify.v1 + +(cd scripts/continuserv && go build) +(cd scripts/speculator && go build) diff --git a/scripts/matrix-org-gendoc.sh b/scripts/add-matrix-org-stylings.sh similarity index 53% rename from scripts/matrix-org-gendoc.sh rename to scripts/add-matrix-org-stylings.sh index 5716786d..9a9256b2 100755 --- a/scripts/matrix-org-gendoc.sh +++ b/scripts/add-matrix-org-stylings.sh @@ -1,45 +1,26 @@ -#! /bin/bash +#!/bin/bash -eu -if [ -z "$1" ]; then - echo "Expected /includes/head.html file as 1st arg." - exit 1 +if [[ $# != 1 || ! -d $1 ]]; then + echo >&2 "Usage: $0 include_dir" + exit 1 fi -if [ -z "$2" ]; then - echo "Expected /includes/nav.html file as 2nd arg." +HEADER="$1/head.html" +NAV_BAR="$1/nav.html" +FOOTER="$1/footer.html" + +for f in "$1"/{head,nav,footer}.html; do + if [[ ! -e "${f}" ]]; then + echo >&2 "Need ${f} to exist" exit 1 -fi + fi +done -if [ -z "$3" ]; then - echo "Expected /includes/footer.html file as 3rd arg." - exit 1 -fi - - -HEADER=$1 -NAV_BAR=$2 -FOOTER=$3 - -if [ ! -f $HEADER ]; then - echo $HEADER " does not exist" - exit 1 -fi - -if [ ! -f $NAV_BAR ]; then - echo $NAV_BAR " does not exist" - exit 1 -fi - -if [ ! -f $FOOTER ]; then - echo $FOOTER " does not exist" - exit 1 -fi - -python gendoc.py +files=gen/*.html perl -MFile::Slurp -pi -e 'BEGIN { $header = read_file("'$HEADER'") } s##$header -#' gen/specification.html gen/howtos.html +#' ${files} perl -MFile::Slurp -pi -e 'BEGIN { $nav = read_file("'$NAV_BAR'") } s##
@@ -48,7 +29,7 @@ perl -MFile::Slurp -pi -e 'BEGIN { $nav = read_file("'$NAV_BAR'") } s## <
-#' gen/specification.html gen/howtos.html +#' ${files} perl -MFile::Slurp -pi -e 'BEGIN { $footer = read_file("'$FOOTER'") } s##
@@ -60,4 +41,4 @@ perl -MFile::Slurp -pi -e 'BEGIN { $footer = read_file("'$FOOTER'") } s## $footer
- #' gen/specification.html gen/howtos.html + #' ${files} diff --git a/scripts/gendoc.py b/scripts/gendoc.py index ed36a5a7..9b87cb30 100755 --- a/scripts/gendoc.py +++ b/scripts/gendoc.py @@ -238,6 +238,18 @@ def rst2html(i, o): ) +def addAnchors(path): + with open(path, "r") as f: + lines = f.readlines() + + replacement = replacement = r'

\n\1' + with open(path, "w") as f: + for line in lines: + line = re.sub(r'()', replacement, line.rstrip()) + line = re.sub(r'(
)', replacement, line.rstrip()) + f.write(line + "\n") + + def run_through_template(input, set_verbose): tmpfile = './tmp/output' try: @@ -264,6 +276,12 @@ def run_through_template(input, set_verbose): raise +def get_build_targets(targets_listing): + with open(targets_listing, "r") as targ_file: + all_targets = yaml.load(targ_file.read()) + return all_targets["targets"].keys() + + """ Extract and resolve groups for the given target in the given targets listing. Args: @@ -374,20 +392,34 @@ def cleanup_env(): shutil.rmtree("./tmp") -def main(target_name, keep_intermediates): +def main(requested_target_name, keep_intermediates): prepare_env() - log("Building spec [target=%s]" % target_name) - target = get_build_target("../specification/targets.yaml", target_name) - build_spec(target=target, out_filename="tmp/templated_spec.rst") - run_through_template("tmp/templated_spec.rst", VERBOSE) - fix_relative_titles( - target=target, filename="tmp/templated_spec.rst", - out_filename="tmp/full_spec.rst" - ) - shutil.copy("../supporting-docs/howtos/client-server.rst", "tmp/howto.rst") - run_through_template("tmp/howto.rst", False) # too spammy to mark -v on this - rst2html("tmp/full_spec.rst", "gen/specification.html") - rst2html("tmp/howto.rst", "gen/howtos.html") + log("Building spec [target=%s]" % requested_target_name) + + targets = [requested_target_name] + if requested_target_name == "all": + targets = get_build_targets("../specification/targets.yaml") + + for target_name in targets: + templated_file = "tmp/templated_%s.rst" % (target_name,) + rst_file = "tmp/spec_%s.rst" % (target_name,) + html_file = "gen/%s.html" % (target_name,) + + target = get_build_target("../specification/targets.yaml", target_name) + build_spec(target=target, out_filename=templated_file) + run_through_template(templated_file, VERBOSE) + fix_relative_titles( + target=target, filename=templated_file, + out_filename=rst_file, + ) + rst2html(rst_file, html_file) + addAnchors(html_file) + + if requested_target_name == "all": + shutil.copy("../supporting-docs/howtos/client-server.rst", "tmp/howto.rst") + run_through_template("tmp/howto.rst", False) # too spammy to mark -v on this + rst2html("tmp/howto.rst", "gen/howtos.html") + if not keep_intermediates: cleanup_env() @@ -401,8 +433,9 @@ if __name__ == '__main__': help="Do not delete intermediate files. They will be found in tmp/" ) parser.add_argument( - "--target", "-t", default="main", - help="Specify the build target to build from specification/targets.yaml" + "--target", "-t", default="all", + help="Specify the build target to build from specification/targets.yaml. " + + "The value 'all' will build all of the targets therein." ) parser.add_argument( "--verbose", "-v", action="store_true", diff --git a/scripts/generate-http-docs.sh b/scripts/generate-http-docs.sh new file mode 100755 index 00000000..b3894a2c --- /dev/null +++ b/scripts/generate-http-docs.sh @@ -0,0 +1,27 @@ +#!/bin/bash -eu + +# This script generates an HTML page containing all of the client-server API docs. +# It takes all of the swagger YAML files for the client-server API, and turns +# them into API docs, with none of the narrative found in the rst files which +# normally wrap these API docs. + +cd "$(dirname $0)" + +mkdir -p tmp gen + +cat >tmp/http_apis <> tmp/http_apis +done + +(cd ../templating ; python build.py -i matrix_templates -o ../scripts/gen ../scripts/tmp/http_apis) +rst2html.py --stylesheet-path=$(echo css/*.css | tr ' ' ',') gen/http_apis > gen/http_apis.html diff --git a/scripts/speculator/main.go b/scripts/speculator/main.go index 97e67c8c..602cdb57 100644 --- a/scripts/speculator/main.go +++ b/scripts/speculator/main.go @@ -23,6 +23,7 @@ import ( "path" "strconv" "strings" + "sync" "syscall" "time" @@ -53,9 +54,11 @@ type User struct { } var ( - port = flag.Int("port", 9000, "Port on which to listen for HTTP") - allowedMembers map[string]bool - specCache *lru.Cache // string -> []byte + port = flag.Int("port", 9000, "Port on which to listen for HTTP") + includesDir = flag.String("includes_dir", "", "Directory containing include files for styling like matrix.org") + allowedMembers map[string]bool + specCache *lru.Cache // string -> map[string][]byte filename -> contents + styledSpecCache *lru.Cache // string -> map[string][]byte filename -> contents ) func (u *User) IsTrusted() bool { @@ -63,19 +66,22 @@ func (u *User) IsTrusted() bool { } const ( - pullsPrefix = "https://api.github.com/repos/matrix-org/matrix-doc/pulls" - matrixDocCloneURL = "https://github.com/matrix-org/matrix-doc.git" + pullsPrefix = "https://api.github.com/repos/matrix-org/matrix-doc/pulls" + matrixDocCloneURL = "https://github.com/matrix-org/matrix-doc.git" + permissionsOwnerFull = 0700 ) func gitClone(url string, shared bool) (string, error) { directory := path.Join("/tmp/matrix-doc", strconv.FormatInt(rand.Int63(), 10)) - cmd := exec.Command("git", "clone", url, directory) - if shared { - cmd.Args = append(cmd.Args, "--shared") + if err := os.MkdirAll(directory, permissionsOwnerFull); err != nil { + return "", fmt.Errorf("error making directory %s: %v", directory, err) } - - if err := cmd.Run(); err != nil { - return "", fmt.Errorf("error cloning repo: %v", err) + args := []string{"clone", url, directory} + if shared { + args = append(args, "--shared") + } + if err := runGitCommand(directory, args); err != nil { + return "", err } return directory, nil } @@ -84,15 +90,13 @@ func gitCheckout(path, sha string) error { return runGitCommand(path, []string{"checkout", sha}) } -func gitFetch(path string) error { - return runGitCommand(path, []string{"fetch"}) -} - func runGitCommand(path string, args []string) error { cmd := exec.Command("git", args...) cmd.Dir = path + var b bytes.Buffer + cmd.Stderr = &b if err := cmd.Run(); err != nil { - return fmt.Errorf("error running %q: %v", strings.Join(cmd.Args, " "), err) + return fmt.Errorf("error running %q: %v (stderr: %s)", strings.Join(cmd.Args, " "), err, b.String()) } return nil } @@ -101,10 +105,7 @@ func lookupPullRequest(url url.URL, pathPrefix string) (*PullRequest, error) { if !strings.HasPrefix(url.Path, pathPrefix+"/") { return nil, fmt.Errorf("invalid path passed: %s expect %s/123", url.Path, pathPrefix) } - prNumber := url.Path[len(pathPrefix)+1:] - if strings.Contains(prNumber, "/") { - return nil, fmt.Errorf("invalid path passed: %s expect %s/123", url.Path, pathPrefix) - } + prNumber := strings.Split(url.Path[len(pathPrefix)+1:], "/")[0] resp, err := http.Get(fmt.Sprintf("%s/%s", pullsPrefix, prNumber)) defer resp.Body.Close() @@ -131,22 +132,41 @@ func generate(dir string) error { } func writeError(w http.ResponseWriter, code int, err error) { + w.Header().Set("Content-Type", "text/plain") w.WriteHeader(code) io.WriteString(w, fmt.Sprintf("%v\n", err)) } type server struct { + mu sync.Mutex // Must be locked around any git command on matrixDocCloneURL matrixDocCloneURL string } +func (s *server) updateBase() error { + s.mu.Lock() + defer s.mu.Unlock() + return runGitCommand(s.matrixDocCloneURL, []string{"fetch"}) +} + +// canCheckout returns whether a given sha can currently be checked out from s.matrixDocCloneURL. +func (s *server) canCheckout(sha string) bool { + s.mu.Lock() + defer s.mu.Unlock() + return runGitCommand(s.matrixDocCloneURL, []string{"cat-file", "-e", sha + "^{commit}"}) == nil +} + // generateAt generates spec from repo at sha. // Returns the path where the generation was done. func (s *server) generateAt(sha string) (dst string, err error) { - err = gitFetch(s.matrixDocCloneURL) - if err != nil { - return + if !s.canCheckout(sha) { + err = s.updateBase() + if err != nil { + return + } } + s.mu.Lock() dst, err = gitClone(s.matrixDocCloneURL, true) + s.mu.Unlock() if err != nil { return } @@ -164,26 +184,61 @@ func (s *server) getSHAOf(ref string) (string, error) { cmd.Dir = path.Join(s.matrixDocCloneURL) var b bytes.Buffer cmd.Stdout = &b - if err := cmd.Run(); err != nil { + s.mu.Lock() + err := cmd.Run() + s.mu.Unlock() + if err != nil { return "", fmt.Errorf("error generating spec: %v\nOutput from gendoc:\n%v", err, b.String()) } return strings.TrimSpace(b.String()), nil } +// extractPath extracts the file path within the gen directory which should be served for the request. +// Returns one of (file to serve, path to redirect to). +// path is the actual path being requested, e.g. "/spec/head/client_server.html". +// base is the base path of the handler, including a trailing slash, before the PR number, e.g. "/spec/". +func extractPath(path, base string) (string, string) { + // Assumes exactly one flat directory + + // Count slashes in /spec/head/client_server.html + // base is /spec/ + // +1 for the PR number - /spec/head + // +1 for the path-part after the slash after the PR number + max := strings.Count(base, "/") + 2 + parts := strings.SplitN(path, "/", max) + + if len(parts) < max { + // Path is base/pr - redirect to base/pr/index.html + return "", path + "/index.html" + } + if parts[max-1] == "" { + // Path is base/pr/ - serve index.html + return "index.html", "" + } + + // Path is base/pr/file.html - serve file + return parts[max-1], "" +} + func (s *server) serveSpec(w http.ResponseWriter, req *http.Request) { var sha string - if strings.ToLower(req.URL.Path) == "/spec/head" { - if err := gitFetch(s.matrixDocCloneURL); err != nil { + var styleLikeMatrixDotOrg = req.URL.Query().Get("matrixdotorgstyle") != "" + + if styleLikeMatrixDotOrg && *includesDir == "" { + writeError(w, 500, fmt.Errorf("Cannot style like matrix.org - no include dir specified")) + return + } + + if strings.HasPrefix(strings.ToLower(req.URL.Path), "/spec/head") { + // err may be non-nil here but if headSha is non-empty we will serve a possibly-stale result in favour of erroring. + // This is to deal with cases like where github is down but we still want to serve the spec. + if headSha, err := s.lookupHeadSHA(); headSha == "" { writeError(w, 500, err) return + } else { + sha = headSha } - originHead, err := s.getSHAOf("origin/master") - if err != nil { - writeError(w, 500, err) - return - } - sha = originHead } else { pr, err := lookupPullRequest(*req.URL, "/spec") if err != nil { @@ -199,25 +254,94 @@ func (s *server) serveSpec(w http.ResponseWriter, req *http.Request) { } sha = pr.Head.SHA } - if cached, ok := specCache.Get(sha); ok { - w.Write(cached.([]byte)) - return + + var cache = specCache + if styleLikeMatrixDotOrg { + cache = styledSpecCache } - dst, err := s.generateAt(sha) - defer os.RemoveAll(dst) - if err != nil { - writeError(w, 500, err) - return + var pathToContent map[string][]byte + + if cached, ok := cache.Get(sha); ok { + pathToContent = cached.(map[string][]byte) + } else { + dst, err := s.generateAt(sha) + defer os.RemoveAll(dst) + if err != nil { + writeError(w, 500, err) + return + } + + if styleLikeMatrixDotOrg { + cmd := exec.Command("./add-matrix-org-stylings.sh", *includesDir) + cmd.Dir = path.Join(dst, "scripts") + var b bytes.Buffer + cmd.Stderr = &b + if err := cmd.Run(); err != nil { + writeError(w, 500, fmt.Errorf("error styling spec: %v\nOutput:\n%v", err, b.String())) + return + } + } + + fis, err := ioutil.ReadDir(path.Join(dst, "scripts", "gen")) + if err != nil { + writeError(w, 500, fmt.Errorf("Error reading directory: %v", err)) + } + pathToContent = make(map[string][]byte) + for _, fi := range fis { + b, err := ioutil.ReadFile(path.Join(dst, "scripts", "gen", fi.Name())) + if err != nil { + writeError(w, 500, fmt.Errorf("Error reading spec: %v", err)) + return + } + pathToContent[fi.Name()] = b + } + cache.Add(sha, pathToContent) } - b, err := ioutil.ReadFile(path.Join(dst, "scripts/gen/specification.html")) - if err != nil { - writeError(w, 500, fmt.Errorf("Error reading spec: %v", err)) + requestedPath, redirect := extractPath(req.URL.Path, "/spec/") + if redirect != "" { + s.redirectTo(w, req, redirect) return } - w.Write(b) - specCache.Add(sha, b) + if b, ok := pathToContent[requestedPath]; ok { + w.Write(b) + return + } + if requestedPath == "index.html" { + // Fall back to single-page spec for old PRs + if b, ok := pathToContent["specification.html"]; ok { + w.Write(b) + return + } + } + w.WriteHeader(404) + w.Write([]byte("Not found")) +} + +func (s *server) redirectTo(w http.ResponseWriter, req *http.Request, path string) { + req.URL.Path = path + w.Header().Set("Location", req.URL.String()) + w.WriteHeader(302) +} + +// lookupHeadSHA looks up what origin/master's HEAD SHA is. +// It attempts to `git fetch` before doing so. +// If this fails, it may still return a stale sha, but will also return an error. +func (s *server) lookupHeadSHA() (sha string, retErr error) { + retErr = s.updateBase() + if retErr != nil { + log.Printf("Error fetching: %v, attempting to fall back to current known value", retErr) + } + originHead, err := s.getSHAOf("origin/master") + if err != nil { + retErr = err + } + sha = originHead + if retErr != nil && originHead != "" { + log.Printf("Successfully fell back to possibly stale sha: %s", sha) + } + return } func checkAuth(pr *PullRequest) error { @@ -255,7 +379,7 @@ func (s *server) serveRSTDiff(w http.ResponseWriter, req *http.Request) { return } - diffCmd := exec.Command("diff", "-u", path.Join(base, "scripts", "tmp", "full_spec.rst"), path.Join(head, "scripts", "tmp", "full_spec.rst")) + diffCmd := exec.Command("diff", "-r", "-u", path.Join(base, "scripts", "tmp"), path.Join(head, "scripts", "tmp")) var diff bytes.Buffer diffCmd.Stdout = &diff if err := ignoreExitCodeOne(diffCmd.Run()); err != nil { @@ -299,7 +423,12 @@ func (s *server) serveHTMLDiff(w http.ResponseWriter, req *http.Request) { return } - cmd := exec.Command(htmlDiffer, path.Join(base, "scripts", "gen", "specification.html"), path.Join(head, "scripts", "gen", "specification.html")) + requestedPath, redirect := extractPath(req.URL.Path, "/diff/spec/") + if redirect != "" { + s.redirectTo(w, req, redirect) + return + } + cmd := exec.Command(htmlDiffer, path.Join(base, "scripts", "gen", requestedPath), path.Join(head, "scripts", "gen", requestedPath)) var b bytes.Buffer cmd.Stdout = &b if err := cmd.Run(); err != nil { @@ -344,6 +473,10 @@ func listPulls(w http.ResponseWriter, req *http.Request) { pull.Number, pull.User.HTMLURL, pull.User.Login, pull.HTMLURL, pull.Title, pull.Number, pull.Number, pull.Number) } s += `` + if *includesDir != "" { + s += `` + } + io.WriteString(w, s) } @@ -383,7 +516,7 @@ func main() { if err != nil { log.Fatal(err) } - s := server{masterCloneDir} + s := server{matrixDocCloneURL: masterCloneDir} http.HandleFunc("/spec/", forceHTML(s.serveSpec)) http.HandleFunc("/diff/rst/", s.serveRSTDiff) http.HandleFunc("/diff/html/", forceHTML(s.serveHTMLDiff)) @@ -408,7 +541,10 @@ func serveText(s string) func(http.ResponseWriter, *http.Request) { } func initCache() error { - c, err := lru.New(50) // Evict after 50 entries (i.e. 50 sha1s) - specCache = c + c1, err := lru.New(50) // Evict after 50 entries (i.e. 50 sha1s) + specCache = c1 + + c2, err := lru.New(50) // Evict after 50 entries (i.e. 50 sha1s) + styledSpecCache = c2 return err } diff --git a/specification/application_service_api.rst b/specification/application_service_api.rst index cf2f9d57..bdf8a52e 100644 --- a/specification/application_service_api.rst +++ b/specification/application_service_api.rst @@ -12,6 +12,9 @@ irrespective of the underlying homeserver implementation. Add in Client-Server services? Overview of bots? Seems weird to be in the spec given it is VERY implementation specific. +.. contents:: Table of Contents +.. sectnum:: + Application Services -------------------- Application services are passive and can only observe events from a given diff --git a/specification/client_server_api.rst b/specification/client_server_api.rst index 05b6ff3c..67973c6b 100644 --- a/specification/client_server_api.rst +++ b/specification/client_server_api.rst @@ -17,6 +17,118 @@ shortly. Documentation for the old `V1 authentication <../attic/v1_registration_login.rst>`_ is still available separately. +.. contents:: Table of Contents +.. sectnum:: + +API Standards +------------- + +.. TODO + Need to specify any HMAC or access_token lifetime/ratcheting tricks + We need to specify capability negotiation for extensible transports + +The mandatory baseline for communication in Matrix is exchanging JSON objects +over HTTP APIs. HTTPS is mandated as the baseline for server-server +(federation) communication. HTTPS is recommended for client-server +communication, although HTTP may be supported as a fallback to support basic +HTTP clients. More efficient optional transports for client-server +communication will in future be supported as optional extensions - e.g. a +packed binary encoding over stream-cipher encrypted TCP socket for +low-bandwidth/low-roundtrip mobile usage. For the default HTTP transport, all +API calls use a Content-Type of ``application/json``. In addition, all strings +MUST be encoded as UTF-8. Clients are authenticated using opaque +``access_token`` strings (see `Client Authentication`_ for details), passed as a +query string parameter on all requests. + +Any errors which occur at the Matrix API level MUST return a "standard error +response". This is a JSON object which looks like:: + + { + "errcode": "", + "error": "" + } + +The ``error`` string will be a human-readable error message, usually a sentence +explaining what went wrong. The ``errcode`` string will be a unique string +which can be used to handle an error message e.g. ``M_FORBIDDEN``. These error +codes should have their namespace first in ALL CAPS, followed by a single _ to +ease separating the namespace from the error code. For example, if there was a +custom namespace ``com.mydomain.here``, and a +``FORBIDDEN`` code, the error code should look like +``COM.MYDOMAIN.HERE_FORBIDDEN``. There may be additional keys depending on the +error, but the keys ``error`` and ``errcode`` MUST always be present. + +Some standard error codes are below: + +:``M_FORBIDDEN``: + Forbidden access, e.g. joining a room without permission, failed login. + +:``M_UNKNOWN_TOKEN``: + The access token specified was not recognised. + +:``M_BAD_JSON``: + Request contained valid JSON, but it was malformed in some way, e.g. missing + required keys, invalid values for keys. + +:``M_NOT_JSON``: + Request did not contain valid JSON. + +:``M_NOT_FOUND``: + No resource was found for this request. + +:``M_LIMIT_EXCEEDED``: + Too many requests have been sent in a short period of time. Wait a while then + try again. + +Some requests have unique error codes: + +:``M_USER_IN_USE``: + Encountered when trying to register a user ID which has been taken. + +:``M_ROOM_IN_USE``: + Encountered when trying to create a room which has been taken. + +:``M_BAD_PAGINATION``: + Encountered when specifying bad pagination query parameters. + +.. _sect:txn_ids: + +The Client-Server API typically uses ``HTTP POST`` to submit requests. This +means these requests are not idempotent. The C-S API also allows ``HTTP PUT`` to +make requests idempotent. In order to use a ``PUT``, paths should be suffixed +with ``/{txnId}``. ``{txnId}`` is a unique client-generated transaction ID which +identifies the request, and is scoped to a given Client (identified by that +client's ``access_token``). Crucially, it **only** serves to identify new +requests from retransmits. After the request has finished, the ``{txnId}`` +value should be changed (how is not specified; a monotonically increasing +integer is recommended). It is preferable to use ``HTTP PUT`` to make sure +requests to send messages do not get sent more than once should clients need to +retransmit requests. + +Valid requests look like:: + + POST /some/path/here?access_token=secret + { + "key": "This is a post." + } + + PUT /some/path/here/11?access_token=secret + { + "key": "This is a put with a txnId of 11." + } + +In contrast, these are invalid requests:: + + POST /some/path/here/11?access_token=secret + { + "key": "This is a post, but it has a txnId." + } + + PUT /some/path/here?access_token=secret + { + "key": "This is a put but it is missing a txnId." + } + Client Authentication --------------------- Most API endpoints require the user to identify themselves by presenting @@ -356,7 +468,7 @@ This section refers to API Version 2. These API calls currently use the prefix .. _User-Interactive Authentication: `sect:auth-api`_ -{{v2_registration_http_api}} +{{registration_http_api}} Old V1 API docs: |register|_ @@ -398,58 +510,11 @@ database. Adding Account Administrative Contact Information +++++++++++++++++++++++++++++++++++++++++++++++++ -Request:: - POST $V2PREFIX/account/3pid +A homeserver may keep some contact information for administrative use. +This is independent of any information kept by any Identity Servers. -Used to add contact information to the user's account. - -The body of the POST request is a JSON object containing: - -threePidCreds - An object containing contact information. -bind - Optional. A boolean indicating whether the Home Server should also bind this - third party identifier to the account's matrix ID with the Identity Server. If - supplied and true, the Home Server must bind the 3pid accordingly. - -The contact information object comprises: - -id_server - The colon-separated hostname and port of the Identity Server used to - authenticate the third party identifier. If the port is the default, it and the - colon should be omitted. -sid - The session ID given by the Identity Server -client_secret - The client secret used in the session with the Identity Server. - -On success, the empty JSON object is returned. - -May also return error codes: - -M_THREEPID_AUTH_FAILED - If the credentials provided could not be verified with the ID Server. - -Fetching Currently Associated Contact Information -+++++++++++++++++++++++++++++++++++++++++++++++++ -Request:: - - GET $V2PREFIX/account/3pid - -This returns a list of third party identifiers that the Home Server has -associated with the user's account. This is *not* the same as the list of third -party identifiers bound to the user's Matrix ID in Identity Servers. Identifiers -in this list may be used by the Home Server as, for example, identifiers that it -will accept to reset the user's account password. - -Returns a JSON object with the key ``threepids`` whose contents is an array of -objects with the following keys: - -medium - The medium of the 3pid (eg, ``email``) -address - The textual address of the 3pid, eg. the email address +{{administrative_contact_http_api}} Pagination ---------- @@ -657,9 +722,9 @@ When the client first logs in, they will need to initially synchronise with their home server. This is achieved via the initial sync API described below. This API also returns an ``end`` token which can be used with the event stream. -{{sync_http_api}} +{{old_sync_http_api}} -{{v2_sync_http_api}} +{{sync_http_api}} Getting events for a room @@ -783,19 +848,9 @@ client has to use the the following API. Room aliases ~~~~~~~~~~~~ -.. NOTE:: - This section is a work in progress. -Room aliases can be created by sending a ``PUT /directory/room/``:: - - { - "room_id": - } - -They can be deleted by sending a ``DELETE /directory/room/`` with -no content. Only some privileged users may be able to delete room aliases, e.g. -server admins, the creator of the room alias, etc. This specification does not -outline the privilege level required for deleting room aliases. +Servers may host aliases for rooms with human-friendly names. Aliases take the +form ``#friendlyname:server.name``. As room aliases are scoped to a particular home server domain name, it is likely that a home server will reject attempts to maintain aliases on other @@ -812,17 +867,11 @@ appears to have a room alias of ``#alias:example.com``, this SHOULD be checked to make sure that the room's ID matches the ``room_id`` returned from the request. -Room aliases can be checked in the same way they are resolved; by sending a -``GET /directory/room/``:: - - { - "room_id": , - "servers": [ , , ] - } - Home servers can respond to resolve requests for aliases on other domains than their own by using the federation API to ask other domain name home servers. +{{directory_http_api}} + Permissions ~~~~~~~~~~~ @@ -870,42 +919,24 @@ following values: ``invite`` This room can only be joined if you were invited. -{{membership_http_api}} +{{inviting_http_api}} + +{{joining_http_api}} + +{{banning_http_api}} Leaving rooms ~~~~~~~~~~~~~ -.. TODO-spec - HS deleting rooms they are no longer a part of. Not implemented. - - This is actually Very Tricky. If all clients a HS is serving leave a room, - the HS will no longer get any new events for that room, because the servers - who get the events are determined on the *membership list*. There should - probably be a way for a HS to lurk on a room even if there are 0 of their - members in the room. - - Grace period before deletion? - - Under what conditions should a room NOT be purged? - - 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 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 appear on the |initialSync|_ API. Whether or not they actually joined the room, if the room is an "invite-only" room they will need to be re-invited before they can re-join -the room. To leave a room, a request should be made to -|/rooms//leave|_ with:: +the room. - {} - -Alternatively, the membership state for this user in this room can be modified -directly by sending the following request to -``/rooms//state/m.room.member/``:: - - { - "membership": "leave" - } - -See the `Room events`_ section for more information on ``m.room.member``. Once a -user has left a room, that room will no longer appear on the |initialSync|_ API. -If all members in a room leave, that room becomes eligible for deletion. +{{leaving_http_api}} Banning users in a room ~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/specification/events.rst b/specification/events.rst index a1aece1c..5a003115 100644 --- a/specification/events.rst +++ b/specification/events.rst @@ -30,12 +30,14 @@ formatted for federation by: ``auth_events``, ``prev_events``, ``hashes``, ``signatures``, ``depth``, ``origin``, ``prev_state``. * Adding an ``age`` to the ``unsigned`` object which gives the time in - milliseconds that has ellapsed since the event was sent. -* Adding a ``prev_content`` to the ``unsigned`` object if the event is - a ``state event`` which gives previous content of that state key. + milliseconds that has elapsed since the event was sent. +* Adding ``prev_content`` and ``prev_sender`` to the ``unsigned`` object if the + event is a ``state event``, which give the previous content and previous + sender of that state key * Adding a ``redacted_because`` to the ``unsigned`` object if the event was redacted which gives the event that redacted it. -* Adding a ``transaction_id`` if the event was sent by the client requesting it. +* Adding a ``transaction_id`` to the ``unsigned`` object if the event was sent + by the client requesting it. Events in responses for APIs with the /v1 prefix are generated from an event formatted for the /v2 prefix by: diff --git a/specification/intro.rst b/specification/intro.rst index 64a21108..8c08bf24 100644 --- a/specification/intro.rst +++ b/specification/intro.rst @@ -8,6 +8,16 @@ https://github.com/matrix-org/matrix-doc using https://github.com/matrix-org/matrix-doc/blob/master/scripts/gendoc.py as of revision ``{{git_version}}`` - https://github.com/matrix-org/matrix-doc/tree/{{git_rev}} +APIs +~~~~ +The following APIs are documented in this specification: + +- `Client-Server API `_ for writing Matrix clients. +- `Server-Server API `_ for writing servers which can federate with Matrix. +- `Application Service API `_ for writing privileged plugins to servers. + +There are also some `appendices `_. + Changelog ~~~~~~~~~ {{spec_changelog}} @@ -358,112 +368,3 @@ dedicated API. The API is symmetrical to managing Profile data. Would it really be overengineered to use the same API for both profile & private user data, but with different ACLs? -API Standards -------------- - -.. TODO - Need to specify any HMAC or access_token lifetime/ratcheting tricks - We need to specify capability negotiation for extensible transports - -The mandatory baseline for communication in Matrix is exchanging JSON objects -over HTTP APIs. HTTPS is mandated as the baseline for server-server -(federation) communication. HTTPS is recommended for client-server -communication, although HTTP may be supported as a fallback to support basic -HTTP clients. More efficient optional transports for client-server -communication will in future be supported as optional extensions - e.g. a -packed binary encoding over stream-cipher encrypted TCP socket for -low-bandwidth/low-roundtrip mobile usage. For the default HTTP transport, all -API calls use a Content-Type of ``application/json``. In addition, all strings -MUST be encoded as UTF-8. Clients are authenticated using opaque -``access_token`` strings (see `Client Authentication`_ for details), passed as a -query string parameter on all requests. - -Any errors which occur at the Matrix API level MUST return a "standard error -response". This is a JSON object which looks like:: - - { - "errcode": "", - "error": "" - } - -The ``error`` string will be a human-readable error message, usually a sentence -explaining what went wrong. The ``errcode`` string will be a unique string -which can be used to handle an error message e.g. ``M_FORBIDDEN``. These error -codes should have their namespace first in ALL CAPS, followed by a single _ to -ease separating the namespace from the error code. For example, if there was a -custom namespace ``com.mydomain.here``, and a -``FORBIDDEN`` code, the error code should look like -``COM.MYDOMAIN.HERE_FORBIDDEN``. There may be additional keys depending on the -error, but the keys ``error`` and ``errcode`` MUST always be present. - -Some standard error codes are below: - -:``M_FORBIDDEN``: - Forbidden access, e.g. joining a room without permission, failed login. - -:``M_UNKNOWN_TOKEN``: - The access token specified was not recognised. - -:``M_BAD_JSON``: - Request contained valid JSON, but it was malformed in some way, e.g. missing - required keys, invalid values for keys. - -:``M_NOT_JSON``: - Request did not contain valid JSON. - -:``M_NOT_FOUND``: - No resource was found for this request. - -:``M_LIMIT_EXCEEDED``: - Too many requests have been sent in a short period of time. Wait a while then - try again. - -Some requests have unique error codes: - -:``M_USER_IN_USE``: - Encountered when trying to register a user ID which has been taken. - -:``M_ROOM_IN_USE``: - Encountered when trying to create a room which has been taken. - -:``M_BAD_PAGINATION``: - Encountered when specifying bad pagination query parameters. - -.. _sect:txn_ids: - -The Client-Server API typically uses ``HTTP POST`` to submit requests. This -means these requests are not idempotent. The C-S API also allows ``HTTP PUT`` to -make requests idempotent. In order to use a ``PUT``, paths should be suffixed -with ``/{txnId}``. ``{txnId}`` is a unique client-generated transaction ID which -identifies the request, and is scoped to a given Client (identified by that -client's ``access_token``). Crucially, it **only** serves to identify new -requests from retransmits. After the request has finished, the ``{txnId}`` -value should be changed (how is not specified; a monotonically increasing -integer is recommended). It is preferable to use ``HTTP PUT`` to make sure -requests to send messages do not get sent more than once should clients need to -retransmit requests. - -Valid requests look like:: - - POST /some/path/here?access_token=secret - { - "key": "This is a post." - } - - PUT /some/path/here/11?access_token=secret - { - "key": "This is a put with a txnId of 11." - } - -In contrast, these are invalid requests:: - - POST /some/path/here/11?access_token=secret - { - "key": "This is a post, but it has a txnId." - } - - PUT /some/path/here?access_token=secret - { - "key": "This is a put but it is missing a txnId." - } - diff --git a/specification/modules/account_data.rst b/specification/modules/account_data.rst new file mode 100644 index 00000000..f3fc72b6 --- /dev/null +++ b/specification/modules/account_data.rst @@ -0,0 +1,27 @@ +Client Config +============= + +.. _module:account_data: + +Clients can store custom config data for their account on their homeserver. +This account data will be synced between different devices and can persist +across installations on a particular device. Users may only view the account +data for their own account + +The account_data may be either global or scoped to a particular rooms. + +Events +------ + +The client recieves the account data as events in the ``account_data`` sections +of a v2 /sync. + +These events can also be received in a v1 /events response or in the +``account_data`` section of a room in v1 /initialSync. ``m.tag`` +events appearing in v1 /events will have a ``room_id`` with the room +the tags are for. + +Client Behaviour +---------------- + +{{account_data_http_api}} diff --git a/specification/modules/end_to_end_encryption.rst b/specification/modules/end_to_end_encryption.rst index e3a52613..8b38ab93 100644 --- a/specification/modules/end_to_end_encryption.rst +++ b/specification/modules/end_to_end_encryption.rst @@ -15,3 +15,329 @@ participating homeservers. End-to-end crypto is still being designed and prototyped - notes on the design may be found at https://lwn.net/Articles/634144/ + +Overview +-------- + +.. code:: + + 1) Bob publishes the public keys and supported algorithms for his device. + + +----------+ +--------------+ + | Bob's HS | | Bob's Device | + +----------+ +--------------+ + | | + |<=============| + /keys/upload + + 2) Alice requests Bob's public key and supported algorithms. + + +----------------+ +------------+ +----------+ + | Alice's Device | | Alice's HS | | Bob's HS | + +----------------+ +------------+ +----------+ + | | | + |=================>|==============>| + /keys/query + + 3) Alice selects an algorithm and claims any one-time keys needed. + + +----------------+ +------------+ +----------+ + | Alice's Device | | Alice's HS | | Bob's HS | + +----------------+ +------------+ +----------+ + | | | + |=================>|==============>| + /keys/claim + + 4) Alice sends an encrypted message to Bob. + + +----------------+ +------------+ +----------+ +--------------+ + | Alice's Device | | Alice's HS | | Bob's HS | | Bob's Device | + +----------------+ +------------+ +----------+ +--------------+ + | | | | + |----------------->|-------------->|------------->| + /send/ + + +Algorithms +---------- + +There are two kinds of algorithms: messaging algorithms and key algorithms. +Messaging algorithms are used to securely send messages between devices. +Key algorithms are used for key agreement and digital signatures. + +Messaging Algorithm Names +~~~~~~~~~~~~~~~~~~~~~~~~~ + +Messaging algorithm names use the extensible naming scheme used throughout this +specification. Algorithm names that start with ``m.`` are reserved for +algorithms defined by this specification. Implementations wanting to experiment +with new algorithms are encouraged to pick algorithm names that start with +their domain to reduce the risk of collisions. + +Algorithm names should be short and meaningful, and should list the primitives +used by the algorithm so that it is easier to see if the algorithm is using a +broken primitive. + +The name ``m.olm.v1.curve25519-aes-sha2`` corresponds to version 1 of the Olm +ratchet using Curve25519 for the initial key agreement, HKDF-SHA-256 for +ratchet key derivation, Curve25519 for the DH ratchet, HMAC-SHA-256 for the +hash ratchet, and HKDF-SHA-256, AES-256 in CBC mode, and 8 byte truncated +HMAC-SHA-256 for authenticated encryption. + +A name of ``m.olm.v1`` is too short: it gives no information about the primitives +in use, and is difficult to extend for different primitives. However a name of +``m.olm.v1.ecdh-curve25519-hdkfsha256.hmacsha256.hkdfsha256-aes256-cbc-hmac64sha256`` +is too long despite giving a more precise description of the algorithm: it adds +to the data transfer overhead and sacrifices clarity for human readers without +adding any useful extra information. + +Key Algorithms +~~~~~~~~~~~~~~ + +The name ``ed25519`` corresponds to the Ed25519 signature algorithm. The key is +a Base64 encoded 32-byte Ed25519 public key. + +The name ``curve25519`` corresponds to the Curve25519 ECDH algorithm. The key is +a Base64 encoded 32-byte Curve25519 public key. + +Client Behaviour +---------------- + +Uploading Keys +~~~~~~~~~~~~~~ + +Keys are uploaded as a signed JSON object. The JSON object must include an +ed25519 key and must be signed by that key. A device may only have one ed25519 +signing key. This key is used as the fingerprint for a device by other clients. + +The JSON object is signed using the process given by `Signing JSON`_. + + +.. code:: http + + POST /_matrix/client/v2_alpha/keys/upload/ HTTP/1.1 + Content-Type: application/json + + { + "device_keys": { + "user_id": "", + "device_id": "", + "valid_after_ts": 1234567890123, + "valid_until_ts": 2345678901234, + "algorithms": [ + "", + ], + "keys": { + ":": "", + }, + "signatures": { + "": { + ":": "" + } } }, + "one_time_keys": { + ":": "" + } } + +.. code:: http + + HTTP/1.1 200 OK + Content-Type: application/json + + { + "one_time_key_counts": { + "": 50 + } + } + + +Downloading Keys +~~~~~~~~~~~~~~~~ + +Keys are downloaded as a collection of signed JSON objects. There +will be one JSON object per device per user. If one of the user's +devices doesn't support end-to-end encryption then their +homeserver must synthesise a JSON object without any device keys +for that device. + +The JSON must be signed by both the homeserver of +the user querying the keys and by the homeserver of the device +being queried. This provides an audit trail if either homeserver +lies about the keys a user owns. + +.. code:: http + + POST /keys/query HTTP/1.1 + Content-Type: application/json + + { + "device_keys": { + "": [""] + } } + + +.. code:: http + + HTTP/1.1 200 OK + Content-Type: application/json + + { + "device_keys": { + "": { + "": { + "user_id": "", + "device_id": "", + "valid_after_ts": 1234567890123, + "valid_until_ts": 2345678901234, + "algorithms": [ + "", + ], + "keys": { + ":": "", + }, + "signatures": { + "": { + ":": "" + }, + "": { + ":": "" + }, + "": { + ":": "" + } } } } } } + + +Clients use ``/_matrix/client/v2_alpha/keys/query`` on their own homeservers to +query keys for any user they wish to contact. Homeservers will respond with the +keys for their local users and forward requests for remote users to +``/_matrix/federation/v1/user/keys/query`` over federation to the remote +server. + + +Claiming One Time Keys +~~~~~~~~~~~~~~~~~~~~~~ + +Some algorithms require one-time keys to improve their secrecy and deniability. +These keys are used once during session establishment, and are then thrown +away. In order for these keys to be useful for improving deniability they +must not be signed using the ed25519 key for a device. + +A device must generate a number of these keys and publish them onto their +homeserver. A device must periodically check how many one-time keys their +homeserver still has. If the number has become too small then the device must +generate new one-time keys and upload them to the homeserver. + +Devices must store the private part of each one-time key they upload. They can +discard the private part of the one-time key when they receive a message using +that key. However it's possible that a one-time key given out by a homeserver +will never be used, so the device that generates the key will never know that +it can discard the key. Therefore a device could end up trying to store too +many private keys. A device that is trying to store too many private keys may +discard keys starting with the oldest. + +A homeserver should rate-limit the number of one-time keys that a given user or +remote server can claim. A homeserver should discard the public part of a one +time key once it has given that key to another user. + + +.. code:: http + + POST /keys/claim HTTP/1.1 + Content-Type: application/json + + { + "one_time_keys": { + "": { + "": "" + } } } + +.. code:: http + + HTTP/1.1 200 OK + Content-Type: application/json + + { + "one_time_keys": { + "": { + "": { + ":": "" + } } } } + + +Clients use ``/_matrix/client/v2_alpha/keys/claim`` on their own homeservers to +claim keys for any user they wish to contact. Homeservers will respond with the +keys for their local users and forward requests for remote users to +``/_matrix/federation/v1/user/keys/claim`` over federation to the remote +server. + +Sending a Message +~~~~~~~~~~~~~~~~~ + +Encrypted messages are sent in the form. + +.. code:: json + + { + "type": "m.room.encrypted", + "content": { + "algorithm": "", + "": "" + } } + + +Using Olm ++++++++++ + +Devices that support olm must include "m.olm.v1.curve25519-aes-sha2" in their +list of supported chat algorithms, must list a Curve25519 device key, and +must publish Curve25519 one-time keys. + +.. code:: json + + { + "type": "m.room.encrypted", + "content": { + "algorithm": "m.olm.v1.curve25519-aes-sha2", + "sender_key": "", + "ciphertext": { + "": { + "type": 0, + "body": "" + } } } } + +The ciphertext is a mapping from device curve25519 key to an encrypted payload +for that device. The ``body`` is a base64 encoded message body. The type is an +integer indicating the type of the message body: 0 for the initial pre-key +message, 1 for ordinary messages. + +Olm sessions will generate messages with a type of 0 until they receive a +message. Once a session has decrypted a message it will produce messages with +a type of 1. + +When a client receives a message with a type of 0 it must first check if it +already has a matching session. If it does then it will use that session to +try to decrypt the message. If there is no existing session then the client +must create a new session and use the new session to decrypt the message. A +client must not persist a session or remove one-time keys used by a session +until it has successfully decrypted a message using that session. + +The plaintext payload is of the form: + +.. code:: json + + { + "type": "", + "content": "", + "room_id": "", + "fingerprint": "" + } + +The type and content of the plaintext message event are given in the payload. +Encrypting state events is not supported. + +We include the room ID in the payload, because otherwise the homeserver would +be able to change the room a message was sent in. We include a hash of the +participating keys so that clients can detect if another device is unexpectedly +included in the conversation. + +Clients must confirm that the ``sender_key`` belongs to the user that sent the +message. diff --git a/specification/modules/instant_messaging.rst b/specification/modules/instant_messaging.rst index a58c762f..cd385001 100644 --- a/specification/modules/instant_messaging.rst +++ b/specification/modules/instant_messaging.rst @@ -62,10 +62,8 @@ resulting ``mxc://`` URI can then be used in the ``url`` key. Recommendations when sending messages ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Clients can send messages using ``POST`` or ``PUT`` requests. Clients SHOULD use -``PUT`` requests with `transaction IDs`_ to make requests idempotent. This -ensures that messages are sent exactly once even under poor network conditions. -Clients SHOULD retry requests using an exponential-backoff algorithm for a +In the event of send failure, clients SHOULD retry requests using an +exponential-backoff algorithm for a certain amount of time T. It is recommended that T is no longer than 5 minutes. After this time, the client should stop retrying and mark the message as "unsent". Users should be able to manually resend unsent messages. @@ -78,8 +76,6 @@ reduce the impact of head-of-line blocking, clients should use a queue per room rather than a global queue, as ordering is only relevant within a single room rather than between rooms. -.. _`transaction IDs`: `sect:txn_ids`_ - Local echo ~~~~~~~~~~ diff --git a/specification/modules/receipts.rst b/specification/modules/receipts.rst index a8ad3cd3..702a7275 100644 --- a/specification/modules/receipts.rst +++ b/specification/modules/receipts.rst @@ -52,7 +52,7 @@ dismissing a notification in order for the event to count as "read". A client can update the markers for its user by interacting with the following HTTP APIs. -{{v2_receipts_http_api}} +{{receipts_http_api}} Server behaviour ---------------- diff --git a/specification/modules/tags.rst b/specification/modules/tags.rst new file mode 100644 index 00000000..f8c28c55 --- /dev/null +++ b/specification/modules/tags.rst @@ -0,0 +1,48 @@ +Room Tagging +============ + +.. _module:tagging: + +Users can add tags to rooms. Tags are short strings used to label rooms, e.g. +"work", "family". A room may have multiple tags. Tags are only visible to the +user that set them but are shared across all their devices. + +Events +------ + +The tags on a room are received as single ``m.tag`` event in the +``account_data`` section of a room in a v2 /sync. + +The ``m.tag`` can also be received in a v1 /events response or in the +``account_data`` section of a room in v1 /initialSync. ``m.tag`` +events appearing in v1 /events will have a ``room_id`` with the room +the tags are for. + +Each tag has an associated JSON object with information about the tag, e.g how +to order the rooms with a given tag. + +Ordering information is given under the ``order`` key as a string. The string +are compared lexicographically by unicode codepoint to determine which should +displayed first. So a room with a tag with an ``order`` key of ``"apples"`` +would appear before a room with a tag with an ``order`` key of ``"oranges"``. +If a room has a tag without an ``order`` key then it should appear after the +rooms with that tag that have an ``order`` key. + +The name of a tag MUST not exceed 255 bytes. + +The name of a tag should be human readable. When displaying tags for a room a +client should display this human readable name. When adding a tag for a room +a client may offer a list to choose from that includes all the tags that the +user has previously set on any of their rooms. + +Two special names are listed in the specification: + +* ``m.favourite`` +* ``m.lowpriority`` + +{{m_tag_event}} + +Client Behaviour +---------------- + +{{tags_http_api}} diff --git a/specification/modules/typing_notifications.rst b/specification/modules/typing_notifications.rst index d2253632..da383e73 100644 --- a/specification/modules/typing_notifications.rst +++ b/specification/modules/typing_notifications.rst @@ -5,10 +5,8 @@ Typing Notifications Users may wish to be informed when another user is typing in a room. This can be achieved using typing notifications. These are ephemeral events scoped to a -``room_id``. This means they do not form part of the `Event Graph`_ but still -have a ``room_id`` key. - -.. _Event Graph: `sect:event-graph`_ +``room_id``. This means they do not form part of the +`Event Graph `_ but still have a ``room_id`` key. Events ------ diff --git a/specification/server_server_api.rst b/specification/server_server_api.rst index 26e040ca..012c2ac9 100644 --- a/specification/server_server_api.rst +++ b/specification/server_server_api.rst @@ -41,6 +41,9 @@ EDUs and PDUs are further wrapped in an envelope called a Transaction, which is transferred from the origin to the destination home server using an HTTPS PUT request. +.. contents:: Table of Contents +.. sectnum:: + Server Discovery ---------------- @@ -533,6 +536,164 @@ part of the path specifies the kind of query being made, and its query arguments have a meaning specific to that kind of query. The response is a JSON-encoded object whose meaning also depends on the kind of query. + +To join a room:: + + GET .../make_join// + Response: JSON encoding of a join proto-event + + PUT .../send_join// + Response: JSON encoding of the state of the room at the time of the event + +Performs the room join handshake. For more information, see "Joining Rooms" +below. + +Joining Rooms +------------- + +When a new user wishes to join room that the user's homeserver already knows +about, the homeserver can immediately determine if this is allowable by +inspecting the state of the room, and if it is acceptable, it can generate, +sign, and emit a new ``m.room.member`` state event adding the user into that +room. When the homeserver does not yet know about the room it cannot do this +directly. Instead, it must take a longer multi-stage handshaking process by +which it first selects a remote homeserver which is already participating in +that room, and uses it to assist in the joining process. This is the remote +join handshake. + +This handshake involves the homeserver of the new member wishing to join +(referred to here as the "joining" server), the directory server hosting the +room alias the user is requesting to join with, and a homeserver where existing +room members are already present (referred to as the "resident" server). + +In summary, the remote join handshake consists of the joining server querying +the directory server for information about the room alias; receiving a room ID +and a list of join candidates. The joining server then requests information +about the room from one of the residents. It uses this information to construct +a ``m.room.member`` event which it finally sends to a resident server. + +Conceptually these are three different roles of homeserver. In practice the +directory server is likely to be resident in the room, and so may be selected +by the joining server to be the assisting resident. Likewise, it is likely that +the joining server picks the same candidate resident for both phases of event +construction, though in principle any valid candidate may be used at each time. +Thus, any join handshake can potentially involve anywhere from two to four +homeservers, though most in practice will use just two. + +:: + + Client Joining Directory Resident + Server Server Server + + join request --> + | + directory request -------> + <---------- directory response + | + make_join request -----------------------> + <------------------------------- make_join response + | + send_join request -----------------------> + <------------------------------- send_join response + | + <---------- join response + +The first part of the handshake usually involves using the directory server to +request the room ID and join candidates. This is covered in more detail on the +directory server documentation, below. In the case of a new user joining a +room as a result of a received invite, the joining user's homeserver could +optimise this step away by picking the origin server of that invite message as +the join candidate. However, the joining server should be aware that the origin +server of the invite might since have left the room, so should be prepared to +fall back on the regular join flow if this optimisation fails. + +Once the joining server has the room ID and the join candidates, it then needs +to obtain enough information about the room to fill in the required fields of +the ``m.room.member`` event. It obtains this by selecting a resident from the +candidate list, and requesting the ``make_join`` endpoint using a ``GET`` +request, specifying the room ID and the user ID of the new member who is +attempting to join. + +The resident server replies to this request with a JSON-encoded object having a +single key called ``event``; within this is an object whose fields contain some +of the information that the joining server will need. Despite its name, this +object is not a full event; notably it does not need to be hashed or signed by +the resident homeserver. The required fields are: + +==================== ======== ============ + Key Type Description +==================== ======== ============ +``type`` String The value ``m.room.member`` +``auth_events`` List An event-reference list containing the + authorization events that would allow this member + to join +``content`` Object The event content +``depth`` Integer (this field must be present but is ignored; it + may be 0) +``event_id`` String A new event ID specified by the resident + homeserver +``origin`` String The name of the resident homeserver +``origin_server_ts`` Integer A timestamp added by the resident homeserver +``prev_events`` List An event-reference list containing the immediate + predecessor events +``room_id`` String The room ID of the room +``sender`` String The user ID of the joining member +``state_key`` String The user ID of the joining member +==================== ======== ============ + +The ``content`` field itself must be an object, containing: + +============== ====== ============ + Key Type Description +============== ====== ============ +``membership`` String The value ``join`` +============== ====== ============ + +The joining server now has sufficient information to construct the real join +event from these protoevent fields. It copies the values of most of them, +adding (or replacing) the following fields: + +==================== ======= ============ + Key Type Description +==================== ======= ============ +``event_id`` String A new event ID specified by the joining homeserver +``origin`` String The name of the joining homeserver +``origin_server_ts`` Integer A timestamp added by the joining homeserver +==================== ======= ============ + +.. TODO-spec + - Why does the protoevent have an event_id, only for the real event to ignore + it and specify a different one? We should definitely pick one or the other. + +This will be a true event, so the joining server should apply the event-signing +algorithm to it, resulting in the addition of the ``hashes`` and ``signatures`` +fields. + +To complete the join handshake, the joining server must now submit this new +event to an resident homeserver, by using the ``send_join`` endpoint. This is +invoked using the room ID and the event ID of the new member event. + +The resident homeserver then accepts this event into the room's event graph, +and responds to the joining server with the full set of state for the newly- +joined room. This is returned as a two-element list, whose first element is the +integer 200, and whose second element is an object which contains the +following keys: + +============== ===== ============ + Key Type Description +============== ===== ============ +``auth_chain`` List A list of events giving the authorization chain for this + join event +``state`` List A complete list of the prevailing state events at the + instant just before accepting the new ``m.room.member`` + event +============== ===== ============ + +.. TODO-spec + - (paul) I don't really understand why the full auth_chain events are given + here. What purpose does it serve expanding them out in full, when surely + they'll appear in the state anyway? + Backfilling ----------- .. NOTE:: @@ -763,6 +924,7 @@ Querying directory information:: servers: list of strings giving the join candidates The list of join candidates is a list of server names that are likely to hold -the given room; these are servers that the requesting server may wish to try -joining with. This list may or may not include the server answering the query. +the given room; these are servers that the requesting server may wish to use as +resident servers as part of the remote join handshake. This list may or may not +include the server answering the query. diff --git a/specification/targets.yaml b/specification/targets.yaml index 8e6a2ce0..cdbfa62e 100644 --- a/specification/targets.yaml +++ b/specification/targets.yaml @@ -1,16 +1,23 @@ targets: - main: # arbitrary name to identify this build target + index: files: # the sort order of files to cat - intro.rst + client_server: + files: - client_server_api.rst - { 1: events.rst } - { 1: event_signing.rst } - modules.rst - { 1: feature_profiles.rst } - { 1: "group:modules" } # reference a group of files + application_service: + files: - application_service_api.rst + server_server: + files: - server_server_api.rst - - identity_servers.rst + appendices: + files: - appendices.rst groups: # reusable blobs of files when prefixed with 'group:' modules: @@ -26,6 +33,8 @@ groups: # reusable blobs of files when prefixed with 'group:' - modules/third_party_invites.rst - modules/search.rst - modules/guest_access.rst + - modules/tags.rst + - modules/account_data.rst title_styles: ["=", "-", "~", "+", "^", "`"] diff --git a/templating/build.py b/templating/build.py index a35d8a08..10fb5aea 100755 --- a/templating/build.py +++ b/templating/build.py @@ -42,6 +42,7 @@ from jinja2 import Environment, FileSystemLoader, StrictUndefined, Template, met from argparse import ArgumentParser, FileType import importlib import json +import logging import os import sys from textwrap import TextWrapper @@ -93,6 +94,27 @@ def main(input_module, file_stream=None, out_dir=None, verbose=False): return '\n\n'.join(output_lines) + def fieldwidths(input, keys, defaults=[], default_width=15): + """ + A template filter to help in the generation of tables. + + Given a list of rows, returns a list giving the maximum length of the + values in each column. + + :param list[dict[str, str]] input: a list of rows. Each row should be a + dict with the keys given in ``keys``. + :param list[str] keys: the keys corresponding to the table columns + :param list[int] defaults: for each column, the default column width. + :param int default_width: if ``defaults`` is shorter than ``keys``, this + will be used as a fallback + """ + def colwidth(key, default): + return reduce(max, (len(row[key]) for row in input), + default if default is not None else default_width) + + results = map(colwidth, keys, defaults) + return results + # make Jinja aware of the templates and filters env = Environment( loader=FileSystemLoader(in_mod.exports["templates"]), @@ -102,6 +124,7 @@ def main(input_module, file_stream=None, out_dir=None, verbose=False): env.filters["indent"] = indent env.filters["indent_block"] = indent_block env.filters["wrap"] = wrap + env.filters["fieldwidths"] = fieldwidths # load up and parse the lowest single units possible: we don't know or care # which spec section will use it, we just need it there in memory for when @@ -188,6 +211,9 @@ if __name__ == '__main__': ) args = parser.parse_args() + if args.verbose: + logging.basicConfig(level=logging.DEBUG) + if not args.input: raise Exception("Missing [i]nput python module.") diff --git a/templating/matrix_templates/sections.py b/templating/matrix_templates/sections.py index e75a75af..78aabca7 100644 --- a/templating/matrix_templates/sections.py +++ b/templating/matrix_templates/sections.py @@ -35,7 +35,7 @@ class MatrixSections(Sections): if not filterFn(event_name): continue sections.append(template.render( - example=examples[event_name], + examples=examples[event_name], event=schemas[event_name], title_kind=subtitle_title_char )) @@ -136,7 +136,7 @@ class MatrixSections(Sections): if not event_name.startswith("m.room.message#m."): continue sections.append(template.render( - example=examples[event_name], + example=examples[event_name][0], event=schemas[event_name], title_kind=subtitle_title_char )) diff --git a/templating/matrix_templates/templates/common-event-fields.tmpl b/templating/matrix_templates/templates/common-event-fields.tmpl index 3f16be3d..8d8c8f0c 100644 --- a/templating/matrix_templates/templates/common-event-fields.tmpl +++ b/templating/matrix_templates/templates/common-event-fields.tmpl @@ -1,17 +1,8 @@ +{% import 'tables.tmpl' as tables -%} + {{common_event.title}} Fields {{(7 + common_event.title | length) * title_kind}} {{common_event.desc | wrap(80)}} -================== ================= =========================================== - Key Type Description -================== ================= =========================================== -{% for row in common_event.rows -%} -{# -#} -{# Row type needs to prepend spaces to line up with the type column (19 ch) -#} -{# Desc needs to prepend the required text (maybe) and prepend spaces too -#} -{# It also needs to then wrap inside the desc col (43 ch width) -#} -{# -#} -{{row.key}}{{row.type|indent(19-row.key|length)}}{{row.desc | indent(18 - (row.type|length)) |wrap(43) |indent_block(37)}} -{% endfor -%} -================== ================= =========================================== +{{ tables.paramtable(common_event.rows, ["Key", "Type", "Description"]) }} diff --git a/templating/matrix_templates/templates/events.tmpl b/templating/matrix_templates/templates/events.tmpl index fb876440..cd165ff0 100644 --- a/templating/matrix_templates/templates/events.tmpl +++ b/templating/matrix_templates/templates/events.tmpl @@ -1,3 +1,5 @@ +{% import 'tables.tmpl' as tables -%} + ``{{event.type}}`` {{(4 + event.type | length) * title_kind}} *{{event.typeof}}* @@ -7,22 +9,13 @@ {% for table in event.content_fields -%} {{"``"+table.title+"``" if table.title else "" }} -======================= ================= =========================================== - {{table.title or "Content"}} Key Type Description -======================= ================= =========================================== -{% for row in table.rows -%} -{# -#} -{# Row type needs to prepend spaces to line up with the type column (19 ch) -#} -{# Desc needs to prepend the required text (maybe) and prepend spaces too -#} -{# It also needs to then wrap inside the desc col (43 ch width) -#} -{# -#} -{{row.key}}{{row.type|indent(24-row.key|length)}}{{row.desc|wrap(43,row.req_str | indent(18 - (row.type|length))) |indent_block(42)}} -{% endfor -%} -======================= ================= =========================================== +{{ tables.paramtable(table.rows, [(table.title or "Content") ~ " Key", "Type", "Description"]) }} {% endfor %} -Example: +Example{% if examples | length > 1 %}s{% endif %}: +{% for example in examples %} .. code:: json {{example | jsonify(4, 4)}} +{% endfor %} diff --git a/templating/matrix_templates/templates/http-api.tmpl b/templating/matrix_templates/templates/http-api.tmpl index 4a9491f3..1253e66e 100644 --- a/templating/matrix_templates/templates/http-api.tmpl +++ b/templating/matrix_templates/templates/http-api.tmpl @@ -1,3 +1,5 @@ +{% import 'tables.tmpl' as tables -%} + ``{{endpoint.method}} {{endpoint.path}}`` {{(5 + (endpoint.path | length) + (endpoint.method | length)) * title_kind}} {% if "alias_for_path" in endpoint -%} @@ -13,17 +15,7 @@ Request format: {% if (endpoint.req_param_by_loc | length) %} -=============================================================== ================= =========================================== - Parameter Value Description -=============================================================== ================= =========================================== -{% for loc in endpoint.req_param_by_loc -%} -*{{loc}} parameters* ------------------------------------------------------------------------------------------------------------------------------ -{% for param in endpoint.req_param_by_loc[loc] -%} -{{param.key}}{{param.type|indent(64-param.key|length)}}{{param.desc|indent(18-param.type|length)|wrap(43)|indent_block(82)}} -{% endfor -%} -{% endfor -%} -=============================================================== ================= =========================================== +{{ tables.split_paramtable(endpoint.req_param_by_loc) }} {% else %} `No parameters` {% endif %} @@ -34,18 +26,7 @@ Response format: {% for table in endpoint.res_tables -%} {{"``"+table.title+"``" if table.title else "" }} -======================= ========================= ========================================== - Param Type Description -======================= ========================= ========================================== -{% for row in table.rows -%} -{# -#} -{# Row type needs to prepend spaces to line up with the type column (20 ch) -#} -{# Desc needs to prepend the required text (maybe) and prepend spaces too -#} -{# It also needs to then wrap inside the desc col (42 ch width) -#} -{# -#} -{{row.key}}{{row.type|indent(24-row.key|length)}}{{row.desc|wrap(42,row.req_str | indent(26 - (row.type|length))) |indent_block(50)}} -{% endfor -%} -======================= ========================= ========================================== +{{ tables.paramtable(table.rows) }} {% endfor %} {% endif -%} diff --git a/templating/matrix_templates/templates/msgtypes.tmpl b/templating/matrix_templates/templates/msgtypes.tmpl index 18d3492b..87cf4a19 100644 --- a/templating/matrix_templates/templates/msgtypes.tmpl +++ b/templating/matrix_templates/templates/msgtypes.tmpl @@ -1,21 +1,12 @@ +{% import 'tables.tmpl' as tables -%} + ``{{event.msgtype}}`` {{(4 + event.msgtype | length) * title_kind}} {{event.desc | wrap(80)}} {% for table in event.content_fields -%} {{"``"+table.title+"``" if table.title else "" }} -================== ================= =========================================== - {{table.title or "Content"}} Key Type Description -================== ================= =========================================== -{% for row in table.rows -%} -{# -#} -{# Row type needs to prepend spaces to line up with the type column (19 ch) -#} -{# Desc needs to prepend the required text (maybe) and prepend spaces too -#} -{# It also needs to then wrap inside the desc col (43 ch width) -#} -{# -#} -{{row.key}}{{row.type|indent(19-row.key|length)}}{{row.desc|wrap(43,row.req_str | indent(18 - (row.type|length))) |indent_block(37)}} -{% endfor -%} -================== ================= =========================================== +{{ tables.paramtable(table.rows, [(table.title or "Content") ~ " Key", "Type", "Description"]) }} {% endfor %} Example: diff --git a/templating/matrix_templates/templates/tables.tmpl b/templating/matrix_templates/templates/tables.tmpl new file mode 100644 index 00000000..aba6c0c4 --- /dev/null +++ b/templating/matrix_templates/templates/tables.tmpl @@ -0,0 +1,104 @@ +{# + # A set of macros for generating RST tables + #} + + +{# + # write a table for a list of parameters. + # + # 'rows' is the list of parameters. Each row should have the keys + # 'key', 'type', and 'desc'. + #} +{% macro paramtable(rows, titles=["Parameter", "Type", "Description"]) -%} +{{ split_paramtable({None: rows}, titles) }} +{% endmacro %} + + +{# + # write a table for the request parameters, split by location. + # 'rows_by_loc' is a map from location to a list of parameters. + # + # As a special case, if a key of 'rows_by_loc' is 'None', no title row is + # written for that location. This is used by the standard 'paramtable' macro. + #} +{% macro split_paramtable(rows_by_loc, + titles=["Parameter", "Type", "Description"]) -%} + +{% set rowkeys = ['key', 'type', 'desc'] %} +{% set titlerow = {'key': titles[0], 'type': titles[1], 'desc': titles[2]} %} + +{# We need the rows flattened into a single list. Abuse the 'sum' filter to + # join arrays instead of add numbers. -#} +{% set flatrows = rows_by_loc.values()|sum(start=[]) -%} + +{# Figure out the widths of the columns. The last column is always 50 characters + # wide; the others default to 10, but stretch if there is wider text in the + # column. -#} +{% set fieldwidths = (([titlerow] + flatrows) | + fieldwidths(rowkeys[0:-1], [10, 10])) + [50] -%} + +{{ tableheader(fieldwidths) }} +{{ tablerow(fieldwidths, titlerow, rowkeys) }} +{{ tableheader(fieldwidths) }} +{% for loc in rows_by_loc -%} + +{% if loc != None -%} +{{ tablespan(fieldwidths, "*" ~ loc ~ " parameters*") }} +{% endif -%} + +{% for row in rows_by_loc[loc] -%} +{{ tablerow(fieldwidths, row, rowkeys) }} +{% endfor -%} +{% endfor -%} + +{{ tableheader(fieldwidths) }} +{% endmacro %} + + + +{# + # Write a table header row, for the given column widths + #} +{% macro tableheader(widths) -%} +{% for arg in widths -%} +{{"="*arg}} {% endfor -%} +{% endmacro %} + + + +{# + # Write a normal table row. Each of 'widths' and 'keys' should be sequences + # of the same length; 'widths' defines the column widths, and 'keys' the + # attributes of 'row' to look up for values to put in the columns. + #} +{% macro tablerow(widths, row, keys) -%} +{% for key in keys -%} +{% set value=row[key] -%} +{% if not loop.last -%} + {# the first few columns need space after them -#} + {{ value }}{{" "*(1+widths[loop.index0]-value|length) -}} +{% else -%} + {# the last column needs wrapping and indenting (by the sum of the widths of + the preceding columns, plus the number of preceding columns (for the + separators)) -#} + {{ value | wrap(widths[loop.index0]) | + indent_block(widths[0:-1]|sum + loop.index0) -}} +{% endif -%} +{% endfor -%} +{% endmacro %} + + + + +{# + # write a tablespan row. This is a single value which spans the entire table. + #} +{% macro tablespan(widths, value) -%} +{{value}} +{# we write a trailing space to stop the separator being misinterpreted + # as a header line. -#} +{{"-"*(widths|sum + widths|length -1)}} {% endmacro %} + + + + diff --git a/templating/matrix_templates/units.py b/templating/matrix_templates/units.py index e5a7c319..06994082 100644 --- a/templating/matrix_templates/units.py +++ b/templating/matrix_templates/units.py @@ -8,6 +8,7 @@ For the actual conversion of data -> RST (including templates), see the sections file instead. """ from batesian.units import Units +import logging import inspect import json import os @@ -16,17 +17,17 @@ import subprocess import urllib import yaml -V1_CLIENT_API = "../api/client-server/v1" -V1_EVENT_EXAMPLES = "../event-schemas/examples/v1" -V1_EVENT_SCHEMA = "../event-schemas/schema/v1" -V2_CLIENT_API = "../api/client-server/v2_alpha" -CORE_EVENT_SCHEMA = "../event-schemas/schema/v1/core-event-schema" +HTTP_APIS = "../api/client-server" +V1_EVENT_EXAMPLES = "../event-schemas/examples" +V1_EVENT_SCHEMA = "../event-schemas/schema" +CORE_EVENT_SCHEMA = "../event-schemas/schema/core-event-schema" CHANGELOG = "../CHANGELOG.rst" TARGETS = "../specification/targets.yaml" ROOM_EVENT = "core-event-schema/room_event.json" STATE_EVENT = "core-event-schema/state_event.json" +logger = logging.getLogger(__name__) def resolve_references(path, schema): if isinstance(schema, dict): @@ -46,6 +47,32 @@ def resolve_references(path, schema): return schema +def inherit_parents(obj): + """ + Recurse through the 'allOf' declarations in the object + """ + logger.debug("inherit_parents %r" % obj) + parents = obj.get("allOf", []) + if not parents: + return obj + + result = {} + + # settings defined in the child take priority over the parents, so we + # iterate through the parents first, and then overwrite with the settings + # from the child. + for p in map(inherit_parents, parents) + [obj]: + for key in ('title', 'type', 'required'): + if p.get(key): + result[key] = p[key] + + for key in ('properties', 'additionalProperties', 'patternProperties'): + if p.get(key): + result.setdefault(key, {}).update(p[key]) + + return result + + def get_json_schema_object_fields(obj, enforce_title=False, include_parents=False): # Algorithm: # f.e. property => add field info (if field is object then recurse) @@ -53,22 +80,44 @@ def get_json_schema_object_fields(obj, enforce_title=False, include_parents=Fals raise Exception( "get_json_schema_object_fields: Object %s isn't an object." % obj ) + + obj = inherit_parents(obj) + + logger.debug("Processing object with title '%s'", obj.get("title")) + if enforce_title and not obj.get("title"): # Force a default titile of "NO_TITLE" to make it obvious in the # specification output which parts of the schema are missing a title obj["title"] = 'NO_TITLE' - required_keys = obj.get("required") - if not required_keys: - required_keys = [] + additionalProps = obj.get("additionalProperties") + if additionalProps: + # not "really" an object, just a KV store + logger.debug("%s is a pseudo-object", obj.get("title")) - fields = { - "title": obj.get("title"), - "rows": [] - } - tables = [fields] + key_type = additionalProps.get("x-pattern", "string") + + value_type = additionalProps["type"] + if value_type == "object": + nested_objects = get_json_schema_object_fields( + additionalProps, + enforce_title=True, + include_parents=include_parents, + ) + value_type = nested_objects[0]["title"] + tables = [x for x in nested_objects if not x.get("no-table")] + else: + key_type = "string" + tables = [] + + tables = [{ + "title": "{%s: %s}" % (key_type, value_type), + "no-table": True + }]+tables + + logger.debug("%s done: returning %s", obj.get("title"), tables) + return tables - parents = obj.get("allOf") props = obj.get("properties") if not props: props = obj.get("patternProperties") @@ -79,83 +128,68 @@ def get_json_schema_object_fields(obj, enforce_title=False, include_parents=Fals if pretty_key: props[pretty_key] = props[key_name] del props[key_name] - if not props and not parents: - # Sometimes you just want to specify that a thing is an object without - # doing all the keys. Allow people to do that if they set a 'title'. - if obj.get("title"): - parents = [{ - "$ref": obj.get("title") - }] - if not props and not parents: - raise Exception( - "Object %s has no properties or parents." % obj - ) - if not props: # parents only - if include_parents: - if obj["title"] == "NO_TITLE" and parents[0].get("title"): - obj["title"] = parents[0].get("title") - props = parents[0].get("properties") - if not props: + # Sometimes you just want to specify that a thing is an object without + # doing all the keys. Allow people to do that if they set a 'title'. + if not props and obj.get("title"): return [{ "title": obj["title"], - "parent": parents[0].get("$ref"), "no-table": True }] + if not props: + raise Exception( + "Object %s has no properties and no title" % obj + ) + + required_keys = set(obj.get("required", [])) + + fields = { + "title": obj.get("title"), + "rows": [] + } + + tables = [fields] + for key_name in sorted(props): + logger.debug("Processing property %s.%s", obj.get('title'), key_name) value_type = None required = key_name in required_keys desc = props[key_name].get("description", "") + prop_type = props[key_name].get('type') - if props[key_name]["type"] == "object": - if props[key_name].get("additionalProperties"): - # not "really" an object, just a KV store - prop_val = props[key_name]["additionalProperties"]["type"] - if prop_val == "object": - nested_object = get_json_schema_object_fields( - props[key_name]["additionalProperties"], - enforce_title=True, - include_parents=include_parents, - ) - key = props[key_name]["additionalProperties"].get( - "x-pattern", "string" - ) - value_type = "{%s: %s}" % (key, nested_object[0]["title"]) - value_id = "%s: %s" % (key, nested_object[0]["title"]) - if not nested_object[0].get("no-table"): - tables += nested_object - else: - value_type = "{string: %s}" % (prop_val,) - value_id = "string: %s" % (prop_val,) - else: - nested_object = get_json_schema_object_fields( - props[key_name], - enforce_title=True, - include_parents=include_parents, - ) - value_type = "{%s}" % nested_object[0]["title"] - value_id = "%s" % (nested_object[0]["title"],) + if prop_type is None: + raise KeyError("Property '%s' of object '%s' missing 'type' field" + % (key_name, obj)) + logger.debug("%s is a %s", key_name, prop_type) - if not nested_object[0].get("no-table"): - tables += nested_object - elif props[key_name]["type"] == "array": + if prop_type == "object": + nested_objects = get_json_schema_object_fields( + props[key_name], + enforce_title=True, + include_parents=include_parents, + ) + value_type = nested_objects[0]["title"] + value_id = value_type + + tables += [x for x in nested_objects if not x.get("no-table")] + elif prop_type == "array": # if the items of the array are objects then recurse if props[key_name]["items"]["type"] == "object": - nested_object = get_json_schema_object_fields( + nested_objects = get_json_schema_object_fields( props[key_name]["items"], enforce_title=True, include_parents=include_parents, ) - value_type = "[%s]" % nested_object[0]["title"] - value_id = "%s" % (nested_object[0]["title"],) - tables += nested_object + value_id = nested_objects[0]["title"] + value_type = "[%s]" % value_id + tables += nested_objects else: value_type = props[key_name]["items"]["type"] if isinstance(value_type, list): value_type = " or ".join(value_type) + value_id = value_type value_type = "[%s]" % value_type - value_id = "%s" % (value_type,) array_enums = props[key_name]["items"].get("enum") if array_enums: if len(array_enums) > 1: @@ -168,8 +202,8 @@ def get_json_schema_object_fields(obj, enforce_title=False, include_parents=Fals " Must be '%s'." % array_enums[0] ) else: - value_type = props[key_name]["type"] - value_id = props[key_name]["type"] + value_type = prop_type + value_id = prop_type if props[key_name].get("enum"): if len(props[key_name].get("enum")) > 1: value_type = "enum" @@ -195,15 +229,32 @@ def get_json_schema_object_fields(obj, enforce_title=False, include_parents=Fals "desc": desc, "req_str": "**Required.** " if required else "" }) + logger.debug("Done property %s" % key_name) + + return tables + + +def get_tables_for_schema(path, schema, include_parents=False): + resolved_schema = resolve_references(path, schema) + tables = get_json_schema_object_fields(resolved_schema, + include_parents=include_parents, + ) + + # the result may contain duplicates, if objects are referred to more than + # once. Filter them out. + # + # Go through the tables backwards so that we end up with a breadth-first + # rather than depth-first ordering. titles = set() filtered = [] - for table in tables: + for table in reversed(tables): if table.get("title") in titles: continue titles.add(table.get("title")) filtered.append(table) + filtered.reverse() return filtered @@ -313,10 +364,8 @@ class MatrixUnits(Units): if is_array_of_objects: req_obj = req_obj["items"] - req_tables = get_json_schema_object_fields( - resolve_references(filepath, req_obj), - include_parents=True, - ) + req_tables = get_tables_for_schema( + filepath, req_obj, include_parents=True) if req_tables > 1: for table in req_tables[1:]: @@ -444,8 +493,7 @@ class MatrixUnits(Units): elif res_type and Units.prop(good_response, "schema/properties"): # response is an object: schema = good_response["schema"] - res_tables = get_json_schema_object_fields( - resolve_references(filepath, schema), + res_tables = get_tables_for_schema(filepath, schema, include_parents=True, ) for table in res_tables: @@ -500,30 +548,21 @@ class MatrixUnits(Units): } def load_swagger_apis(self): - paths = [ - V1_CLIENT_API, V2_CLIENT_API - ] apis = {} - for path in paths: - is_v2 = (path == V2_CLIENT_API) - if not os.path.exists(V2_CLIENT_API): - self.log("Skipping v2 apis: %s does not exist." % V2_CLIENT_API) + path = HTTP_APIS + for filename in os.listdir(path): + if not filename.endswith(".yaml"): continue - for filename in os.listdir(path): - if not filename.endswith(".yaml"): - continue - self.log("Reading swagger API: %s" % filename) - filepath = os.path.join(path, filename) - with open(filepath, "r") as f: - # strip .yaml - group_name = filename[:-5].replace("-", "_") - if is_v2: - group_name = "v2_" + group_name - api = yaml.load(f.read()) - api["__meta"] = self._load_swagger_meta( - filepath, api, group_name - ) - apis[group_name] = api + self.log("Reading swagger API: %s" % filename) + filepath = os.path.join(path, filename) + with open(filepath, "r") as f: + # strip .yaml + group_name = filename[:-5].replace("-", "_") + api = yaml.load(f.read()) + api["__meta"] = self._load_swagger_meta( + filepath, api, group_name + ) + apis[group_name] = api return apis def load_common_event_fields(self): @@ -572,9 +611,14 @@ class MatrixUnits(Units): if not filename.startswith("m."): continue with open(os.path.join(path, filename), "r") as f: - examples[filename] = json.loads(f.read()) - if filename == "m.room.message#m.text": - examples["m.room.message"] = examples[filename] + event_name = filename.split("#")[0] + example = json.loads(f.read()) + + examples[filename] = examples.get(filename, []) + examples[filename].append(example) + if filename != event_name: + examples[event_name] = examples.get(event_name, []) + examples[event_name].append(example) return examples def load_event_schemas(self): @@ -584,8 +628,9 @@ class MatrixUnits(Units): for filename in os.listdir(path): if not filename.startswith("m."): continue - self.log("Reading %s" % os.path.join(path, filename)) - with open(os.path.join(path, filename), "r") as f: + filepath = os.path.join(path, filename) + self.log("Reading %s" % filepath) + with open(filepath, "r") as f: json_schema = json.loads(f.read()) schema = { "typeof": None, @@ -627,15 +672,15 @@ class MatrixUnits(Units): schema["desc"] = json_schema.get("description", "") # walk the object for field info - schema["content_fields"] = get_json_schema_object_fields( + schema["content_fields"] = get_tables_for_schema(filepath, Units.prop(json_schema, "properties/content") ) # This is horrible because we're special casing a key on m.room.member. # We need to do this because we want to document a non-content object. if schema["type"] == "m.room.member": - invite_room_state = get_json_schema_object_fields( - json_schema["properties"]["invite_room_state"]["items"] + invite_room_state = get_tables_for_schema(filepath, + json_schema["properties"]["invite_room_state"]["items"], ) schema["content_fields"].extend(invite_room_state)