Merge branch 'master' of github.com:matrix-org/matrix-doc into erikj/disable_federation
This commit is contained in:
commit
3b4c3522e6
37 changed files with 1138 additions and 423 deletions
|
@ -6,6 +6,34 @@
|
||||||
.. in Jenkins. Comments like this are ignored by both RST and the templating
|
.. in Jenkins. Comments like this are ignored by both RST and the templating
|
||||||
.. system. Add the newest release notes beneath this comment.
|
.. system. Add the newest release notes beneath this comment.
|
||||||
|
|
||||||
|
Specification changes in v0.2.0 (2015-10-02)
|
||||||
|
============================================
|
||||||
|
|
||||||
|
This update fundamentally restructures the specification. The specification has
|
||||||
|
been split into more digestible "modules" which each describe a particular
|
||||||
|
function (e.g. typing). This was done in order make the specification easier to
|
||||||
|
maintain and help define which modules are mandatory for certain types
|
||||||
|
of clients. Types of clients along with the mandatory modules can be found in a
|
||||||
|
new "Feature Profiles" section. This update also begins to aggressively
|
||||||
|
standardise on using Swagger and JSON Schema to document HTTP endpoints and
|
||||||
|
Events respectively. It also introduces a number of new concepts to Matrix.
|
||||||
|
|
||||||
|
Additions:
|
||||||
|
- New section: Feature Profiles.
|
||||||
|
- New section: Receipts.
|
||||||
|
- New section: Room history visibility.
|
||||||
|
- New event: ``m.receipt``.
|
||||||
|
- New event: ``m.room.canonical_alias``
|
||||||
|
- New event: ``m.room.history_visibility``
|
||||||
|
- New keys: ``/createRoom`` - allows room "presets" using ``preset`` and
|
||||||
|
``initial_state`` keys.
|
||||||
|
- New endpoint: ``/tokenrefresh`` - Related to refreshing access tokens.
|
||||||
|
|
||||||
|
Modifications:
|
||||||
|
- Convert most of the older HTTP APIs to Swagger documentation.
|
||||||
|
- Convert most of the older event formats to JSON Schema.
|
||||||
|
- Move selected client-server sections to be "Modules".
|
||||||
|
|
||||||
Specification changes in v0.1.0 (2015-06-01)
|
Specification changes in v0.1.0 (2015-06-01)
|
||||||
============================================
|
============================================
|
||||||
- First numbered release.
|
- First numbered release.
|
||||||
|
|
|
@ -34,7 +34,7 @@ def check_parameter(filepath, request, parameter):
|
||||||
example = None
|
example = None
|
||||||
try:
|
try:
|
||||||
example_json = schema.get('example')
|
example_json = schema.get('example')
|
||||||
if example_json:
|
if example_json and not schema.get("format") == "byte":
|
||||||
example = json.loads(example_json)
|
example = json.loads(example_json)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise ValueError("Error parsing JSON example request for %r" % (
|
raise ValueError("Error parsing JSON example request for %r" % (
|
||||||
|
|
|
@ -15,16 +15,22 @@ paths:
|
||||||
summary: Upload some content to the content repository.
|
summary: Upload some content to the content repository.
|
||||||
produces: ["application/json"]
|
produces: ["application/json"]
|
||||||
parameters:
|
parameters:
|
||||||
|
- in: header
|
||||||
|
name: Content-Type
|
||||||
|
type: string
|
||||||
|
description: The content type of the file being uploaded
|
||||||
|
x-example: "Content-Type: audio/mpeg"
|
||||||
- in: body
|
- in: body
|
||||||
name: content
|
name: "<content>"
|
||||||
description: The content to be uploaded.
|
description: The content to be uploaded.
|
||||||
required: true
|
required: true
|
||||||
schema:
|
schema:
|
||||||
type: string
|
type: string
|
||||||
|
example: "<bytes>"
|
||||||
format: byte
|
format: byte
|
||||||
responses:
|
responses:
|
||||||
200:
|
200:
|
||||||
description: Information about the uploaded content.
|
description: The MXC URI for the uploaded content.
|
||||||
schema:
|
schema:
|
||||||
type: object
|
type: object
|
||||||
required: ["content_uri"]
|
required: ["content_uri"]
|
||||||
|
@ -32,6 +38,11 @@ paths:
|
||||||
content_uri:
|
content_uri:
|
||||||
type: string
|
type: string
|
||||||
description: "The MXC URI to the uploaded content."
|
description: "The MXC URI to the uploaded content."
|
||||||
|
examples:
|
||||||
|
"application/json": |-
|
||||||
|
{
|
||||||
|
"content_uri": "mxc://example.com/AQwafuaFswefuhsfAFAgsw"
|
||||||
|
}
|
||||||
"/download/{serverName}/{mediaId}":
|
"/download/{serverName}/{mediaId}":
|
||||||
get:
|
get:
|
||||||
summary: "Download content from the content repository."
|
summary: "Download content from the content repository."
|
||||||
|
@ -40,18 +51,27 @@ paths:
|
||||||
- in: path
|
- in: path
|
||||||
type: string
|
type: string
|
||||||
name: serverName
|
name: serverName
|
||||||
|
x-example: matrix.org
|
||||||
required: true
|
required: true
|
||||||
description: |
|
description: |
|
||||||
The server name from the ``mxc://`` URI (the authoritory component)
|
The server name from the ``mxc://`` URI (the authoritory component)
|
||||||
- in: path
|
- in: path
|
||||||
type: string
|
type: string
|
||||||
name: mediaId
|
name: mediaId
|
||||||
|
x-example: ascERGshawAWawugaAcauga
|
||||||
required: true
|
required: true
|
||||||
description: |
|
description: |
|
||||||
The media ID from the ``mxc://`` URI (the path component)
|
The media ID from the ``mxc://`` URI (the path component)
|
||||||
responses:
|
responses:
|
||||||
200:
|
200:
|
||||||
description: "The content downloaded."
|
description: "The content that was previously uploaded."
|
||||||
|
headers:
|
||||||
|
Content-Type:
|
||||||
|
description: "The content type of the file that was previously uploaded."
|
||||||
|
type: "string"
|
||||||
|
Content-Disposition:
|
||||||
|
description: "The name of the file that was previously uploaded, if set."
|
||||||
|
type: "string"
|
||||||
schema:
|
schema:
|
||||||
type: file
|
type: file
|
||||||
"/thumbnail/{serverName}/{mediaId}":
|
"/thumbnail/{serverName}/{mediaId}":
|
||||||
|
@ -63,30 +83,44 @@ paths:
|
||||||
type: string
|
type: string
|
||||||
name: serverName
|
name: serverName
|
||||||
required: true
|
required: true
|
||||||
|
x-example: matrix.org
|
||||||
description: |
|
description: |
|
||||||
The server name from the ``mxc://`` URI (the authoritory component)
|
The server name from the ``mxc://`` URI (the authoritory component)
|
||||||
- in: path
|
- in: path
|
||||||
type: string
|
type: string
|
||||||
name: mediaId
|
name: mediaId
|
||||||
|
x-example: ascERGshawAWawugaAcauga
|
||||||
required: true
|
required: true
|
||||||
description: |
|
description: |
|
||||||
The media ID from the ``mxc://`` URI (the path component)
|
The media ID from the ``mxc://`` URI (the path component)
|
||||||
- in: query
|
- in: query
|
||||||
type: integer
|
type: integer
|
||||||
|
x-example: 64
|
||||||
name: width
|
name: width
|
||||||
description: The desired width of the thumbnail.
|
description: |-
|
||||||
|
The *desired* width of the thumbnail. The actual thumbnail may not
|
||||||
|
match the size specified.
|
||||||
- in: query
|
- in: query
|
||||||
type: integer
|
type: integer
|
||||||
|
x-example: 64
|
||||||
name: height
|
name: height
|
||||||
description: The desired height of the thumbnail.
|
description: |-
|
||||||
|
The *desired* height of the thumbnail. The actual thumbnail may not
|
||||||
|
match the size specified.
|
||||||
- in: query
|
- in: query
|
||||||
type: string
|
type: string
|
||||||
enum: ["crop", "scale"]
|
enum: ["crop", "scale"]
|
||||||
name: method
|
name: method
|
||||||
|
x-example: "scale"
|
||||||
description: The desired resizing method.
|
description: The desired resizing method.
|
||||||
responses:
|
responses:
|
||||||
200:
|
200:
|
||||||
description: "A thumbnail of the requested content."
|
description: "A thumbnail of the requested content."
|
||||||
|
headers:
|
||||||
|
Content-Type:
|
||||||
|
description: "The content type of the thumbnail."
|
||||||
|
type: "string"
|
||||||
|
enum: ["image/jpeg", "image/png"]
|
||||||
schema:
|
schema:
|
||||||
type: file
|
type: file
|
||||||
|
|
||||||
|
|
|
@ -267,6 +267,12 @@ paths:
|
||||||
type: string
|
type: string
|
||||||
description: "The user's membership state in this room."
|
description: "The user's membership state in this room."
|
||||||
enum: ["invite", "join", "leave", "ban"]
|
enum: ["invite", "join", "leave", "ban"]
|
||||||
|
invite:
|
||||||
|
type: object
|
||||||
|
title: "InviteEvent"
|
||||||
|
description: "The invite event if ``membership`` is ``invite``"
|
||||||
|
allOf:
|
||||||
|
- "$ref": "v1-event-schema/m.room.member"
|
||||||
messages:
|
messages:
|
||||||
type: object
|
type: object
|
||||||
title: PaginationChunk
|
title: PaginationChunk
|
||||||
|
|
77
api/client-server/v1/typing.yaml
Normal file
77
api/client-server/v1/typing.yaml
Normal file
|
@ -0,0 +1,77 @@
|
||||||
|
swagger: '2.0'
|
||||||
|
info:
|
||||||
|
title: "Matrix Client-Server v1 Typing 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}/typing/{userId}":
|
||||||
|
put:
|
||||||
|
summary: Informs the server that the user has started or stopped typing.
|
||||||
|
description: |-
|
||||||
|
This tells the server that the user is typing for the next N
|
||||||
|
milliseconds where N is the value specified in the ``timeout`` key.
|
||||||
|
Alternatively, if ``typing`` is ``false``, it tells the server that the
|
||||||
|
user has stopped typing.
|
||||||
|
security:
|
||||||
|
- accessToken: []
|
||||||
|
parameters:
|
||||||
|
- in: path
|
||||||
|
type: string
|
||||||
|
name: userId
|
||||||
|
description: The user who has started to type.
|
||||||
|
required: true
|
||||||
|
x-example: "@alice:example.com"
|
||||||
|
- in: path
|
||||||
|
type: string
|
||||||
|
name: roomId
|
||||||
|
description: The room in which the user is typing.
|
||||||
|
required: true
|
||||||
|
x-example: "!wefh3sfukhs:example.com"
|
||||||
|
- in: body
|
||||||
|
name: typingState
|
||||||
|
description: The current typing state.
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
example: |-
|
||||||
|
{
|
||||||
|
"typing": true,
|
||||||
|
"timeout": 30000
|
||||||
|
}
|
||||||
|
properties:
|
||||||
|
typing:
|
||||||
|
type: boolean
|
||||||
|
description: |-
|
||||||
|
Whether the user is typing or not. If ``false``, the ``timeout``
|
||||||
|
key can be omitted.
|
||||||
|
timeout:
|
||||||
|
type: integer
|
||||||
|
description: The length of time in milliseconds to mark this user as typing.
|
||||||
|
required: ["typing"]
|
||||||
|
responses:
|
||||||
|
200:
|
||||||
|
description: The new typing state was set.
|
||||||
|
examples:
|
||||||
|
application/json: |-
|
||||||
|
{}
|
||||||
|
schema:
|
||||||
|
type: object # empty json object
|
||||||
|
429:
|
||||||
|
description: This request was rate-limited.
|
||||||
|
schema:
|
||||||
|
"$ref": "definitions/error.yaml"
|
||||||
|
|
68
api/client-server/v1/voip.yaml
Normal file
68
api/client-server/v1/voip.yaml
Normal file
|
@ -0,0 +1,68 @@
|
||||||
|
swagger: '2.0'
|
||||||
|
info:
|
||||||
|
title: "Matrix Client-Server v1 Voice over IP 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:
|
||||||
|
"/turnServer":
|
||||||
|
get:
|
||||||
|
summary: Obtain TURN server credentials.
|
||||||
|
description: |-
|
||||||
|
This API provides credentials for the client to use when initiating
|
||||||
|
calls.
|
||||||
|
security:
|
||||||
|
- accessToken: []
|
||||||
|
responses:
|
||||||
|
200:
|
||||||
|
description: The TURN server credentials.
|
||||||
|
examples:
|
||||||
|
application/json: |-
|
||||||
|
{
|
||||||
|
"username":"1443779631:@user:example.com",
|
||||||
|
"password":"JlKfBy1QwLrO20385QyAtEyIv0=",
|
||||||
|
"uris":[
|
||||||
|
"turn:turn.example.com:3478?transport=udp",
|
||||||
|
"turn:10.20.30.40:3478?transport=tcp",
|
||||||
|
"turns:10.20.30.40:443?transport=tcp"
|
||||||
|
],
|
||||||
|
"ttl":86400
|
||||||
|
}
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
username:
|
||||||
|
type: string
|
||||||
|
description: |-
|
||||||
|
The username to use.
|
||||||
|
password:
|
||||||
|
type: string
|
||||||
|
description: |-
|
||||||
|
The password to use.
|
||||||
|
uris:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
description: A list of TURN URIs
|
||||||
|
ttl:
|
||||||
|
type: integer
|
||||||
|
description: The time-to-live in seconds
|
||||||
|
required: ["username", "password", "uris", "ttl"]
|
||||||
|
429:
|
||||||
|
description: This request was rate-limited.
|
||||||
|
schema:
|
||||||
|
"$ref": "definitions/error.yaml"
|
||||||
|
|
68
api/client-server/v2_alpha/receipts.yaml
Normal file
68
api/client-server/v2_alpha/receipts.yaml
Normal file
|
@ -0,0 +1,68 @@
|
||||||
|
swagger: '2.0'
|
||||||
|
info:
|
||||||
|
title: "Matrix Client-Server v2 Receipts 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:
|
||||||
|
"/rooms/{roomId}/receipt/{receiptType}/{eventId}":
|
||||||
|
post:
|
||||||
|
summary: Send a receipt for the given event ID.
|
||||||
|
description: |-
|
||||||
|
This API updates the marker for the given receipt type to the event ID
|
||||||
|
specified.
|
||||||
|
security:
|
||||||
|
- accessToken: []
|
||||||
|
parameters:
|
||||||
|
- in: path
|
||||||
|
type: string
|
||||||
|
name: roomId
|
||||||
|
description: The room in which to send the event.
|
||||||
|
required: true
|
||||||
|
x-example: "!wefuh21ffskfuh345:example.com"
|
||||||
|
- in: path
|
||||||
|
type: string
|
||||||
|
name: receiptType
|
||||||
|
description: The type of receipt to send.
|
||||||
|
required: true
|
||||||
|
x-example: "m.read"
|
||||||
|
enum: ["m.read"]
|
||||||
|
- in: path
|
||||||
|
type: string
|
||||||
|
name: eventId
|
||||||
|
description: The event ID to acknowledge up to.
|
||||||
|
required: true
|
||||||
|
x-example: "$1924376522eioj:example.com"
|
||||||
|
- in: body
|
||||||
|
description: |-
|
||||||
|
Extra receipt information to attach to ``content`` if any. The
|
||||||
|
server will automatically set the ``ts`` field.
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
example: |-
|
||||||
|
{}
|
||||||
|
responses:
|
||||||
|
200:
|
||||||
|
description: The receipt was sent.
|
||||||
|
examples:
|
||||||
|
application/json: |-
|
||||||
|
{}
|
||||||
|
schema:
|
||||||
|
type: object # empty json object
|
||||||
|
429:
|
||||||
|
description: This request was rate-limited.
|
||||||
|
schema:
|
||||||
|
"$ref": "definitions/error.yaml"
|
|
@ -10,6 +10,6 @@
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"nopt": "^3.0.2",
|
"nopt": "^3.0.2",
|
||||||
"swagger-parser": "^2.4.1"
|
"swagger-parser": "^3.2.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,11 +26,10 @@ if (!opts.schema) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
var errFn = function(err, api, metadata) {
|
var errFn = function(err, api) {
|
||||||
if (!err) {
|
if (!err) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
console.log(metadata);
|
|
||||||
console.error(err);
|
console.error(err);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
};
|
};
|
||||||
|
@ -46,11 +45,12 @@ if (isDir) {
|
||||||
files.forEach(function(f) {
|
files.forEach(function(f) {
|
||||||
var suffix = ".yaml";
|
var suffix = ".yaml";
|
||||||
if (f.indexOf(suffix, f.length - suffix.length) > 0) {
|
if (f.indexOf(suffix, f.length - suffix.length) > 0) {
|
||||||
parser.parse(path.join(opts.schema, f), function(err, api, metadata) {
|
parser.validate(path.join(opts.schema, f), function(err, api, metadata) {
|
||||||
if (!err) {
|
if (!err) {
|
||||||
console.log("%s is valid.", f);
|
console.log("%s is valid.", f);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
|
console.error("%s is not valid.", f);
|
||||||
errFn(err, api, metadata);
|
errFn(err, api, metadata);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -59,12 +59,12 @@ if (isDir) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
else{
|
else{
|
||||||
parser.parse(opts.schema, function(err, api, metadata) {
|
parser.validate(opts.schema, function(err, api) {
|
||||||
if (!err) {
|
if (!err) {
|
||||||
console.log("%s is valid", opts.schema);
|
console.log("%s is valid", opts.schema);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
errFn(err, api, metadata);
|
errFn(err, api);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
"room_id": "!KpjVgQyZpzBwvMBsnT:matrix.org",
|
"room_id": "!KpjVgQyZpzBwvMBsnT:matrix.org",
|
||||||
"content": {
|
"content": {
|
||||||
"$1435641916114394fHBLK:matrix.org": {
|
"$1435641916114394fHBLK:matrix.org": {
|
||||||
"read": {
|
"m.read": {
|
||||||
"@rikj:jki.re": {
|
"@rikj:jki.re": {
|
||||||
"ts": 1436451550453
|
"ts": 1436451550453
|
||||||
}
|
}
|
||||||
|
|
7
event-schemas/examples/v1/m.typing
Normal file
7
event-schemas/examples/v1/m.typing
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
{
|
||||||
|
"type": "m.typing",
|
||||||
|
"room_id": "!z0mnsuiwhifuhwwfw:matrix.org",
|
||||||
|
"content": {
|
||||||
|
"user_ids": ["@alice:matrix.org", "@bob:example.com"]
|
||||||
|
}
|
||||||
|
}
|
|
@ -5,26 +5,32 @@
|
||||||
"properties": {
|
"properties": {
|
||||||
"content": {
|
"content": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"description": "The event ids which the receipts relate to.",
|
|
||||||
"patternProperties": {
|
"patternProperties": {
|
||||||
"^\\$": {
|
"^\\$": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"description": "The types of the receipts.",
|
"x-pattern": "$EVENT_ID",
|
||||||
"additionalProperties": {
|
"title": "Receipts",
|
||||||
"type": "object",
|
"description": "The mapping of event ID to a collection of receipts for this event ID. The event ID is the ID of the event being acknowledged and *not* an ID for the receipt itself.",
|
||||||
"description": "User ids of the receipts",
|
"properties": {
|
||||||
"patternProperties": {
|
"m.read": {
|
||||||
"^@": {
|
"type": "object",
|
||||||
"type": "object",
|
"title": "Users",
|
||||||
"properties": {
|
"description": "A collection of users who have sent ``m.read`` receipts for this event.",
|
||||||
"ts": {
|
"patternProperties": {
|
||||||
"type": "number",
|
"^@": {
|
||||||
"description": "The timestamp the receipt was sent at"
|
"type": "object",
|
||||||
|
"title": "Receipt",
|
||||||
|
"description": "The mapping of user ID to receipt. The user ID is the entity who sent this receipt.",
|
||||||
|
"x-pattern": "$USER_ID",
|
||||||
|
"properties": {
|
||||||
|
"ts": {
|
||||||
|
"type": "number",
|
||||||
|
"description": "The timestamp the receipt was sent at."
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
"additionalProperties": false
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
@ -32,6 +32,26 @@
|
||||||
"type": {
|
"type": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"enum": ["m.room.member"]
|
"enum": ["m.room.member"]
|
||||||
|
},
|
||||||
|
"invite_room_state": {
|
||||||
|
"type": "array",
|
||||||
|
"description": "A subset of the state of the room at the time of the invite, if ``membership`` is ``invite``",
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"title": "StateEvent",
|
||||||
|
"description": "A stripped down state event, with only the ``type``, ``state_key`` and ``content`` keys.",
|
||||||
|
"properties": {
|
||||||
|
"type": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"state_key": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"content": {
|
||||||
|
"type": "object"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
28
event-schemas/schema/v1/m.typing
Normal file
28
event-schemas/schema/v1/m.typing
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"title": "Typing Event",
|
||||||
|
"description": "Informs the client of the list of users currently typing.",
|
||||||
|
"properties": {
|
||||||
|
"content": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"user_ids": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"description": "The list of user IDs typing in this room, if any."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["user_ids"]
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["m.typing"]
|
||||||
|
},
|
||||||
|
"room_id": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["type", "room_id", "content"]
|
||||||
|
}
|
|
@ -17,6 +17,7 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
|
"time"
|
||||||
|
|
||||||
fsnotify "gopkg.in/fsnotify.v1"
|
fsnotify "gopkg.in/fsnotify.v1"
|
||||||
)
|
)
|
||||||
|
@ -67,7 +68,6 @@ func watchFS(ch chan struct{}, w *fsnotify.Watcher) {
|
||||||
select {
|
select {
|
||||||
case e := <-w.Events:
|
case e := <-w.Events:
|
||||||
if filter(e) {
|
if filter(e) {
|
||||||
wg.Add(1)
|
|
||||||
fmt.Printf("Noticed change to %s, re-generating spec\n", e.Name)
|
fmt.Printf("Noticed change to %s, re-generating spec\n", e.Name)
|
||||||
ch <- struct{}{}
|
ch <- struct{}{}
|
||||||
}
|
}
|
||||||
|
@ -98,6 +98,11 @@ func filter(e fsnotify.Event) bool {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Ignore the .git directory - It's very noisy
|
||||||
|
if strings.Contains(e.Name, "/.git/") {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
// Avoid infinite cycles being caused by writing actual output
|
// Avoid infinite cycles being caused by writing actual output
|
||||||
if strings.Contains(e.Name, "/tmp/") || strings.Contains(e.Name, "/gen/") {
|
if strings.Contains(e.Name, "/tmp/") || strings.Contains(e.Name, "/gen/") {
|
||||||
return false
|
return false
|
||||||
|
@ -133,8 +138,20 @@ func populateOnce(dir string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func doPopulate(ch chan struct{}, dir string) {
|
func doPopulate(ch chan struct{}, dir string) {
|
||||||
for _ = range ch {
|
var pending int
|
||||||
populateOnce(dir)
|
for {
|
||||||
|
select {
|
||||||
|
case <-ch:
|
||||||
|
if pending == 0 {
|
||||||
|
wg.Add(1)
|
||||||
|
}
|
||||||
|
pending++
|
||||||
|
case <-time.After(10 * time.Millisecond):
|
||||||
|
if pending > 0 {
|
||||||
|
pending = 0
|
||||||
|
populateOnce(dir)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -244,10 +244,6 @@ div.viewcode-block:target {
|
||||||
border-bottom: 1px solid #ac9;
|
border-bottom: 1px solid #ac9;
|
||||||
}
|
}
|
||||||
|
|
||||||
p {
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
ul li dd {
|
ul li dd {
|
||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
}
|
}
|
||||||
|
@ -282,3 +278,9 @@ td[colspan]:not([colspan="1"]) {
|
||||||
thead {
|
thead {
|
||||||
background: #eeeeee;
|
background: #eeeeee;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
div.admonition-rationale {
|
||||||
|
background-color: #efe;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
|
@ -24,6 +24,7 @@ import (
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"syscall"
|
"syscall"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
type PullRequest struct {
|
type PullRequest struct {
|
||||||
|
@ -58,24 +59,42 @@ func (u *User) IsTrusted() bool {
|
||||||
return allowedMembers[u.Login]
|
return allowedMembers[u.Login]
|
||||||
}
|
}
|
||||||
|
|
||||||
const pullsPrefix = "https://api.github.com/repos/matrix-org/matrix-doc/pulls"
|
const (
|
||||||
|
pullsPrefix = "https://api.github.com/repos/matrix-org/matrix-doc/pulls"
|
||||||
|
matrixDocCloneURL = "https://github.com/matrix-org/matrix-doc.git"
|
||||||
|
)
|
||||||
|
|
||||||
|
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")
|
||||||
|
}
|
||||||
|
|
||||||
func gitClone(url string) (string, error) {
|
|
||||||
dst := path.Join("/tmp/matrix-doc", strconv.FormatInt(rand.Int63(), 10))
|
|
||||||
cmd := exec.Command("git", "clone", url, dst)
|
|
||||||
err := cmd.Run()
|
err := cmd.Run()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", fmt.Errorf("error cloning repo: %v", err)
|
return "", fmt.Errorf("error cloning repo: %v", err)
|
||||||
}
|
}
|
||||||
return dst, nil
|
return directory, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func gitCheckout(path, sha string) error {
|
func gitCheckout(path, sha string) error {
|
||||||
cmd := exec.Command("git", "checkout", sha)
|
return runGitCommand(path, []string{"checkout", sha})
|
||||||
|
}
|
||||||
|
|
||||||
|
func gitFetchAndMerge(path string) error {
|
||||||
|
if err := runGitCommand(path, []string{"fetch"}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return runGitCommand(path, []string{"merge"})
|
||||||
|
}
|
||||||
|
|
||||||
|
func runGitCommand(path string, args []string) error {
|
||||||
|
cmd := exec.Command("git", args...)
|
||||||
cmd.Dir = path
|
cmd.Dir = path
|
||||||
err := cmd.Run()
|
err := cmd.Run()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("error checking out repo: %v", err)
|
return fmt.Errorf("error running %q: %v", strings.Join(cmd.Args, " "), err)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -119,10 +138,18 @@ func writeError(w http.ResponseWriter, code int, err error) {
|
||||||
io.WriteString(w, fmt.Sprintf("%v\n", err))
|
io.WriteString(w, fmt.Sprintf("%v\n", err))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type server struct {
|
||||||
|
matrixDocCloneURL string
|
||||||
|
}
|
||||||
|
|
||||||
// generateAt generates spec from repo at sha.
|
// generateAt generates spec from repo at sha.
|
||||||
// Returns the path where the generation was done.
|
// Returns the path where the generation was done.
|
||||||
func generateAt(repo, sha string) (dst string, err error) {
|
func (s *server) generateAt(sha string) (dst string, err error) {
|
||||||
dst, err = gitClone(repo)
|
err = gitFetchAndMerge(s.matrixDocCloneURL)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
dst, err = gitClone(s.matrixDocCloneURL, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -135,21 +162,28 @@ func generateAt(repo, sha string) (dst string, err error) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func serveSpec(w http.ResponseWriter, req *http.Request) {
|
func (s *server) serveSpec(w http.ResponseWriter, req *http.Request) {
|
||||||
pr, err := lookupPullRequest(*req.URL, "/spec")
|
var sha string
|
||||||
if err != nil {
|
|
||||||
writeError(w, 400, err)
|
if strings.ToLower(req.URL.Path) == "/spec/head" {
|
||||||
return
|
sha = "HEAD"
|
||||||
|
} else {
|
||||||
|
pr, err := lookupPullRequest(*req.URL, "/spec")
|
||||||
|
if err != nil {
|
||||||
|
writeError(w, 400, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// We're going to run whatever Python is specified in the pull request, which
|
||||||
|
// may do bad things, so only trust people we trust.
|
||||||
|
if err := checkAuth(pr); err != nil {
|
||||||
|
writeError(w, 403, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
sha = pr.Head.SHA
|
||||||
}
|
}
|
||||||
|
|
||||||
// We're going to run whatever Python is specified in the pull request, which
|
dst, err := s.generateAt(sha)
|
||||||
// may do bad things, so only trust people we trust.
|
|
||||||
if err := checkAuth(pr); err != nil {
|
|
||||||
writeError(w, 403, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
dst, err := generateAt(pr.Head.Repo.CloneURL, pr.Head.SHA)
|
|
||||||
defer os.RemoveAll(dst)
|
defer os.RemoveAll(dst)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeError(w, 500, err)
|
writeError(w, 500, err)
|
||||||
|
@ -171,7 +205,7 @@ func checkAuth(pr *PullRequest) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func serveRSTDiff(w http.ResponseWriter, req *http.Request) {
|
func (s *server) serveRSTDiff(w http.ResponseWriter, req *http.Request) {
|
||||||
pr, err := lookupPullRequest(*req.URL, "/diff/rst")
|
pr, err := lookupPullRequest(*req.URL, "/diff/rst")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeError(w, 400, err)
|
writeError(w, 400, err)
|
||||||
|
@ -185,14 +219,14 @@ func serveRSTDiff(w http.ResponseWriter, req *http.Request) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
base, err := generateAt(pr.Base.Repo.CloneURL, pr.Base.SHA)
|
base, err := s.generateAt(pr.Base.SHA)
|
||||||
defer os.RemoveAll(base)
|
defer os.RemoveAll(base)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeError(w, 500, err)
|
writeError(w, 500, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
head, err := generateAt(pr.Head.Repo.CloneURL, pr.Head.SHA)
|
head, err := s.generateAt(pr.Head.SHA)
|
||||||
defer os.RemoveAll(head)
|
defer os.RemoveAll(head)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeError(w, 500, err)
|
writeError(w, 500, err)
|
||||||
|
@ -209,7 +243,7 @@ func serveRSTDiff(w http.ResponseWriter, req *http.Request) {
|
||||||
w.Write(diff.Bytes())
|
w.Write(diff.Bytes())
|
||||||
}
|
}
|
||||||
|
|
||||||
func serveHTMLDiff(w http.ResponseWriter, req *http.Request) {
|
func (s *server) serveHTMLDiff(w http.ResponseWriter, req *http.Request) {
|
||||||
pr, err := lookupPullRequest(*req.URL, "/diff/html")
|
pr, err := lookupPullRequest(*req.URL, "/diff/html")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeError(w, 400, err)
|
writeError(w, 400, err)
|
||||||
|
@ -223,14 +257,14 @@ func serveHTMLDiff(w http.ResponseWriter, req *http.Request) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
base, err := generateAt(pr.Base.Repo.CloneURL, pr.Base.SHA)
|
base, err := s.generateAt(pr.Base.SHA)
|
||||||
defer os.RemoveAll(base)
|
defer os.RemoveAll(base)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeError(w, 500, err)
|
writeError(w, 500, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
head, err := generateAt(pr.Head.Repo.CloneURL, pr.Head.SHA)
|
head, err := s.generateAt(pr.Head.SHA)
|
||||||
defer os.RemoveAll(head)
|
defer os.RemoveAll(head)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeError(w, 500, err)
|
writeError(w, 500, err)
|
||||||
|
@ -287,7 +321,7 @@ func listPulls(w http.ResponseWriter, req *http.Request) {
|
||||||
s += fmt.Sprintf(`<li>%d: <a href="%s">%s</a>: <a href="%s">%s</a>: <a href="spec/%d">spec</a> <a href="diff/html/%d">spec diff</a> <a href="diff/rst/%d">rst diff</a></li>`,
|
s += fmt.Sprintf(`<li>%d: <a href="%s">%s</a>: <a href="%s">%s</a>: <a href="spec/%d">spec</a> <a href="diff/html/%d">spec diff</a> <a href="diff/rst/%d">rst diff</a></li>`,
|
||||||
pull.Number, pull.User.HTMLURL, pull.User.Login, pull.HTMLURL, pull.Title, pull.Number, pull.Number, pull.Number)
|
pull.Number, pull.User.HTMLURL, pull.User.Login, pull.HTMLURL, pull.Title, pull.Number, pull.Number, pull.Number)
|
||||||
}
|
}
|
||||||
s += "</ul></body>"
|
s += `</ul><div><a href="spec/head">View the spec at head</a></div></body>`
|
||||||
io.WriteString(w, s)
|
io.WriteString(w, s)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -317,11 +351,19 @@ func main() {
|
||||||
"Kegsay": true,
|
"Kegsay": true,
|
||||||
"NegativeMjark": true,
|
"NegativeMjark": true,
|
||||||
}
|
}
|
||||||
http.HandleFunc("/spec/", serveSpec)
|
rand.Seed(time.Now().Unix())
|
||||||
http.HandleFunc("/diff/rst/", serveRSTDiff)
|
masterCloneDir, err := gitClone(matrixDocCloneURL, false)
|
||||||
http.HandleFunc("/diff/html/", serveHTMLDiff)
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
s := server{masterCloneDir}
|
||||||
|
http.HandleFunc("/spec/", s.serveSpec)
|
||||||
|
http.HandleFunc("/diff/rst/", s.serveRSTDiff)
|
||||||
|
http.HandleFunc("/diff/html/", s.serveHTMLDiff)
|
||||||
http.HandleFunc("/healthz", serveText("ok"))
|
http.HandleFunc("/healthz", serveText("ok"))
|
||||||
http.HandleFunc("/", listPulls)
|
http.HandleFunc("/", listPulls)
|
||||||
|
|
||||||
|
fmt.Printf("Listening on port %d\n", *port)
|
||||||
log.Fatal(http.ListenAndServe(fmt.Sprintf(":%d", *port), nil))
|
log.Fatal(http.ListenAndServe(fmt.Sprintf(":%d", *port), nil))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,93 @@
|
||||||
Feature Profiles
|
Feature Profiles
|
||||||
================
|
================
|
||||||
|
|
||||||
|
.. sect:feature-profiles:
|
||||||
|
|
||||||
|
Matrix supports many different kinds of clients: from embedded IoT devices to
|
||||||
|
desktop clients. Not all clients can provide the same feature sets as other
|
||||||
|
clients e.g. due to lack of physical hardware such as not having a screen.
|
||||||
|
Clients can fall into one of several profiles and each profile contains a set
|
||||||
|
of features that the client MUST support. This section details a set of
|
||||||
|
"feature profiles". Clients are expected to implement a profile in its entirety
|
||||||
|
in order for it to be classified as that profile.
|
||||||
|
|
||||||
|
Summary
|
||||||
|
-------
|
||||||
|
|
||||||
|
===================================== ========== ========== ========== ========== ==========
|
||||||
|
Module / Profile Web Mobile Desktop CLI Embedded
|
||||||
|
===================================== ========== ========== ========== ========== ==========
|
||||||
|
`Instant Messaging`_ Required Required Required Required Optional
|
||||||
|
`Presence`_ Required Required Required Required Optional
|
||||||
|
`Push Notifications`_ Optional Required Optional Optional Optional
|
||||||
|
`Receipts`_ Required Required Required Required Optional
|
||||||
|
`Typing Notifications`_ Required Required Required Required Optional
|
||||||
|
`VoIP`_ Required Required Required Optional Optional
|
||||||
|
`Content Repository`_ Required Required Required Optional Optional
|
||||||
|
`Managing History Visibility`_ Required Required Required Required Optional
|
||||||
|
`End-to-End Encryption`_ Optional Optional Optional Optional Optional
|
||||||
|
===================================== ========== ========== ========== ========== ==========
|
||||||
|
|
||||||
|
*Please see each module for more details on what clients need to implement.*
|
||||||
|
|
||||||
|
.. _End-to-End Encryption: `module:e2e`_
|
||||||
|
.. _Instant Messaging: `module:im`_
|
||||||
|
.. _Presence: `module:presence`_
|
||||||
|
.. _Push Notifications: `module:push`_
|
||||||
|
.. _Receipts: `module:receipts`_
|
||||||
|
.. _Typing Notifications: `module:typing`_
|
||||||
|
.. _VoIP: `module:voip`_
|
||||||
|
.. _Content Repository: `module:content`_
|
||||||
|
.. _Managing History Visibility: `module:history-visibility`_
|
||||||
|
|
||||||
|
Clients
|
||||||
|
-------
|
||||||
|
|
||||||
|
Stand-alone web (``Web``)
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
This is a web page which heavily uses Matrix for communication. Single-page web
|
||||||
|
apps would be classified as a stand-alone web client, as would multi-page web
|
||||||
|
apps which use Matrix on nearly every page.
|
||||||
|
|
||||||
|
Mobile (``Mobile``)
|
||||||
|
~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
This is a Matrix client specifically designed for consumption on mobile devices.
|
||||||
|
This is typically a mobile app but need not be so provided the feature set can
|
||||||
|
be reached (e.g. if a mobile site could display push notifications it could be
|
||||||
|
classified as a mobile client).
|
||||||
|
|
||||||
|
Desktop (``Desktop``)
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
This is a native GUI application which can run in its own environment outside a
|
||||||
|
browser.
|
||||||
|
|
||||||
|
Command Line Interface (``CLI``)
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
This is a client which is used via a text-based terminal.
|
||||||
|
|
||||||
|
Embedded (``Embedded``)
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
This is a client which is embedded into another application or an embedded
|
||||||
|
device.
|
||||||
|
|
||||||
|
Application
|
||||||
|
+++++++++++
|
||||||
|
|
||||||
|
This is a Matrix client which is embedded in another website, e.g. using
|
||||||
|
iframes. These embedded clients are typically for a single purpose
|
||||||
|
related to the website in question, and are not intended to be fully-fledged
|
||||||
|
communication apps.
|
||||||
|
|
||||||
|
Device
|
||||||
|
++++++
|
||||||
|
|
||||||
|
This is a client which is typically running on an embedded device such as a
|
||||||
|
kettle, fridge or car. These clients tend to perform a few operations and run
|
||||||
|
in a resource constrained environment. Like embedded applications, they are
|
||||||
|
not intended to be fully-fledged communication systems.
|
||||||
|
|
||||||
|
|
|
@ -24,7 +24,6 @@ Introduction
|
||||||
The Matrix specification is still evolving: the APIs are not yet frozen
|
The Matrix specification is still evolving: the APIs are not yet frozen
|
||||||
and this document is in places a work in progress or stale. We have made every
|
and this document is in places a work in progress or stale. We have made every
|
||||||
effort to clearly flag areas which are still being finalised.
|
effort to clearly flag areas which are still being finalised.
|
||||||
|
|
||||||
We're publishing it at this point because it's complete enough to be more than
|
We're publishing it at this point because it's complete enough to be more than
|
||||||
useful and provide a canonical reference to how Matrix is evolving. Our end
|
useful and provide a canonical reference to how Matrix is evolving. Our end
|
||||||
goal is to mirror WHATWG's `Living Standard
|
goal is to mirror WHATWG's `Living Standard
|
||||||
|
@ -34,10 +33,9 @@ Matrix is a set of open APIs for open-federated Instant Messaging (IM), Voice
|
||||||
over IP (VoIP) and Internet of Things (IoT) communication, designed to create
|
over IP (VoIP) and Internet of Things (IoT) communication, designed to create
|
||||||
and support a new global real-time communication ecosystem. The intention is to
|
and support a new global real-time communication ecosystem. The intention is to
|
||||||
provide an open decentralised pubsub layer for the internet for securely
|
provide an open decentralised pubsub layer for the internet for securely
|
||||||
persisting and publishing/subscribing JSON objects.
|
persisting and publishing/subscribing JSON objects. This specification is the
|
||||||
|
ongoing result of standardising the APIs used by the various components of the
|
||||||
This specification is the ongoing result of standardising the APIs used by the
|
Matrix ecosystem to communicate with one another.
|
||||||
various components of the Matrix ecosystem to communicate with one another.
|
|
||||||
|
|
||||||
The principles that Matrix attempts to follow are:
|
The principles that Matrix attempts to follow are:
|
||||||
|
|
||||||
|
@ -182,6 +180,8 @@ of a "Room".
|
||||||
Event Graphs
|
Event Graphs
|
||||||
~~~~~~~~~~~~
|
~~~~~~~~~~~~
|
||||||
|
|
||||||
|
.. _sect:event-graph:
|
||||||
|
|
||||||
Events exchanged in the context of a room are stored in a directed acyclic graph
|
Events exchanged in the context of a room are stored in a directed acyclic graph
|
||||||
(DAG) called an ``event graph``. The partial ordering of this graph gives the
|
(DAG) called an ``event graph``. The partial ordering of this graph gives the
|
||||||
chronological ordering of events within the room. Each event in the graph has a
|
chronological ordering of events within the room. Each event in the graph has a
|
||||||
|
@ -214,10 +214,8 @@ which have the form::
|
||||||
There is exactly one room ID for each room. Whilst the room ID does contain a
|
There is exactly one room ID for each room. Whilst the room ID does contain a
|
||||||
domain, it is simply for globally namespacing room IDs. The room does NOT
|
domain, it is simply for globally namespacing room IDs. The room does NOT
|
||||||
reside on the domain specified. Room IDs are not meant to be human readable.
|
reside on the domain specified. Room IDs are not meant to be human readable.
|
||||||
They are case-sensitive.
|
They are case-sensitive. The following conceptual diagram shows an
|
||||||
|
``m.room.message`` event being sent to the room ``!qporfwt:matrix.org``::
|
||||||
The following conceptual diagram shows an ``m.room.message`` event being sent to
|
|
||||||
the room ``!qporfwt:matrix.org``::
|
|
||||||
|
|
||||||
{ @alice:matrix.org } { @bob:domain.com }
|
{ @alice:matrix.org } { @bob:domain.com }
|
||||||
| ^
|
| ^
|
||||||
|
@ -258,28 +256,28 @@ the room ``!qporfwt:matrix.org``::
|
||||||
Federation maintains *shared data structures* per-room between multiple home
|
Federation maintains *shared data structures* per-room between multiple home
|
||||||
servers. The data is split into ``message events`` and ``state events``.
|
servers. The data is split into ``message events`` and ``state events``.
|
||||||
|
|
||||||
``Message events`` describe transient 'once-off' activity in a room such as an
|
Message events:
|
||||||
instant messages, VoIP call setups, file transfers, etc. They generally describe
|
These describe transient 'once-off' activity in a room such as an
|
||||||
communication activity.
|
instant messages, VoIP call setups, file transfers, etc. They generally
|
||||||
|
describe communication activity.
|
||||||
|
|
||||||
``State events`` describe updates to a given piece of persistent information
|
State events:
|
||||||
('state') related to a room, such as the room's name, topic, membership,
|
These describe updates to a given piece of persistent information
|
||||||
participating servers, etc. State is modelled as a lookup table of key/value
|
('state') related to a room, such as the room's name, topic, membership,
|
||||||
pairs per room, with each key being a tuple of ``state_key`` and ``event type``.
|
participating servers, etc. State is modelled as a lookup table of key/value
|
||||||
Each state event updates the value of a given key.
|
pairs per room, with each key being a tuple of ``state_key`` and ``event type``.
|
||||||
|
Each state event updates the value of a given key.
|
||||||
|
|
||||||
The state of the room at a given point is calculated by considering all events
|
The state of the room at a given point is calculated by considering all events
|
||||||
preceding and including a given event in the graph. Where events describe the
|
preceding and including a given event in the graph. Where events describe the
|
||||||
same state, a merge conflict algorithm is applied. The state resolution
|
same state, a merge conflict algorithm is applied. The state resolution
|
||||||
algorithm is transitive and does not depend on server state, as it must
|
algorithm is transitive and does not depend on server state, as it must
|
||||||
consistently select the same event irrespective of the server or the order the
|
consistently select the same event irrespective of the server or the order the
|
||||||
events were received in.
|
events were received in. Events are signed by the originating server (the
|
||||||
|
signature includes the parent relations, type, depth and payload hash) and are
|
||||||
Events are signed by the originating server (the signature includes the parent
|
pushed over federation to the participating servers in a room, currently using
|
||||||
relations, type, depth and payload hash) and are pushed over federation to the
|
full mesh topology. Servers may also request backfill of events over federation
|
||||||
participating servers in a room, currently using full mesh topology. Servers may
|
from the other servers participating in a room.
|
||||||
also request backfill of events over federation from the other servers
|
|
||||||
participating in a room.
|
|
||||||
|
|
||||||
|
|
||||||
Room Aliases
|
Room Aliases
|
||||||
|
@ -324,12 +322,10 @@ Users in Matrix are identified via their matrix user ID (MXID). However,
|
||||||
existing 3rd party ID namespaces can also be used in order to identify Matrix
|
existing 3rd party ID namespaces can also be used in order to identify Matrix
|
||||||
users. A Matrix "Identity" describes both the user ID and any other existing IDs
|
users. A Matrix "Identity" describes both the user ID and any other existing IDs
|
||||||
from third party namespaces *linked* to their account.
|
from third party namespaces *linked* to their account.
|
||||||
|
|
||||||
Matrix users can *link* third-party IDs (3PIDs) such as email addresses, social
|
Matrix users can *link* third-party IDs (3PIDs) such as email addresses, social
|
||||||
network accounts and phone numbers to their user ID. Linking 3PIDs creates a
|
network accounts and phone numbers to their user ID. Linking 3PIDs creates a
|
||||||
mapping from a 3PID to a user ID. This mapping can then be used by Matrix
|
mapping from a 3PID to a user ID. This mapping can then be used by Matrix
|
||||||
users in order to discover the MXIDs of their contacts.
|
users in order to discover the MXIDs of their contacts.
|
||||||
|
|
||||||
In order to ensure that the mapping from 3PID to user ID is genuine, a globally
|
In order to ensure that the mapping from 3PID to user ID is genuine, a globally
|
||||||
federated cluster of trusted "Identity Servers" (IS) are used to verify the 3PID
|
federated cluster of trusted "Identity Servers" (IS) are used to verify the 3PID
|
||||||
and persist and replicate the mappings.
|
and persist and replicate the mappings.
|
||||||
|
@ -338,49 +334,6 @@ Usage of an IS is not required in order for a client application to be part of
|
||||||
the Matrix ecosystem. However, without one clients will not be able to look up
|
the Matrix ecosystem. However, without one clients will not be able to look up
|
||||||
user IDs using 3PIDs.
|
user IDs using 3PIDs.
|
||||||
|
|
||||||
Presence
|
|
||||||
~~~~~~~~
|
|
||||||
|
|
||||||
Each user has the concept of presence information. This encodes:
|
|
||||||
|
|
||||||
* Whether the user is currently online
|
|
||||||
* How recently the user was last active (as seen by the server)
|
|
||||||
* Whether a given client considers the user to be currently idle
|
|
||||||
* Arbitrary information about the user's current status (e.g. "in a meeting").
|
|
||||||
|
|
||||||
This information is collated from both per-device (online; idle; last_active) and
|
|
||||||
per-user (status) data, aggregated by the user's homeserver and transmitted as
|
|
||||||
an ``m.presence`` event. This is one of the few events which are sent *outside
|
|
||||||
the context of a room*. Presence events are sent to all users who subscribe to
|
|
||||||
this user's presence through a presence list or by sharing membership of a room.
|
|
||||||
|
|
||||||
.. TODO
|
|
||||||
How do we let users hide their presence information?
|
|
||||||
|
|
||||||
.. TODO
|
|
||||||
The last_active specifics should be moved to the detailed presence event section
|
|
||||||
|
|
||||||
Last activity is tracked by the server maintaining a timestamp of the last time
|
|
||||||
it saw a pro-active event from the user. Any event which could be triggered by a
|
|
||||||
human using the application is considered pro-active (e.g. sending an event to a
|
|
||||||
room). An example of a non-proactive client activity would be a client setting
|
|
||||||
'idle' presence status, or polling for events. This timestamp is presented via a
|
|
||||||
key called ``last_active_ago``, which gives the relative number of milliseconds
|
|
||||||
since the message is generated/emitted that the user was last seen active.
|
|
||||||
|
|
||||||
N.B. in v1 API, status/online/idle state are muxed into a single 'presence'
|
|
||||||
field on the ``m.presence`` event.
|
|
||||||
|
|
||||||
Presence Lists
|
|
||||||
~~~~~~~~~~~~~~
|
|
||||||
|
|
||||||
Each user's home server stores a "presence list". This stores a list of user IDs
|
|
||||||
whose presence the user wants to follow.
|
|
||||||
|
|
||||||
To be added to this list, the user being added must be invited by the list owner
|
|
||||||
and accept the invitation. Once accepted, both user's HSes track the
|
|
||||||
subscription.
|
|
||||||
|
|
||||||
|
|
||||||
Profiles
|
Profiles
|
||||||
~~~~~~~~
|
~~~~~~~~
|
||||||
|
@ -410,6 +363,10 @@ dedicated API. The API is symmetrical to managing Profile data.
|
||||||
API Standards
|
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
|
The mandatory baseline for communication in Matrix is exchanging JSON objects
|
||||||
over HTTP APIs. HTTPS is mandated as the baseline for server-server
|
over HTTP APIs. HTTPS is mandated as the baseline for server-server
|
||||||
(federation) communication. HTTPS is recommended for client-server
|
(federation) communication. HTTPS is recommended for client-server
|
||||||
|
@ -417,20 +374,11 @@ communication, although HTTP may be supported as a fallback to support basic
|
||||||
HTTP clients. More efficient optional transports for client-server
|
HTTP clients. More efficient optional transports for client-server
|
||||||
communication will in future be supported as optional extensions - e.g. a
|
communication will in future be supported as optional extensions - e.g. a
|
||||||
packed binary encoding over stream-cipher encrypted TCP socket for
|
packed binary encoding over stream-cipher encrypted TCP socket for
|
||||||
low-bandwidth/low-roundtrip mobile usage.
|
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
|
||||||
.. TODO
|
MUST be encoded as UTF-8. Clients are authenticated using opaque
|
||||||
We need to specify capability negotiation for extensible transports
|
``access_token`` strings (see `Client Authentication`_ for details), passed as a
|
||||||
|
query string parameter on all requests.
|
||||||
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.
|
|
||||||
|
|
||||||
.. TODO
|
|
||||||
Need to specify any HMAC or access_token lifetime/ratcheting tricks
|
|
||||||
|
|
||||||
Any errors which occur at the Matrix API level MUST return a "standard error
|
Any errors which occur at the Matrix API level MUST return a "standard error
|
||||||
response". This is a JSON object which looks like::
|
response". This is a JSON object which looks like::
|
||||||
|
|
|
@ -197,6 +197,7 @@ This specification defines the following login types:
|
||||||
- ``m.login.recaptcha``
|
- ``m.login.recaptcha``
|
||||||
- ``m.login.oauth2``
|
- ``m.login.oauth2``
|
||||||
- ``m.login.email.identity``
|
- ``m.login.email.identity``
|
||||||
|
- ``m.login.token``
|
||||||
- ``m.login.dummy``
|
- ``m.login.dummy``
|
||||||
|
|
||||||
Password-based
|
Password-based
|
||||||
|
@ -228,6 +229,37 @@ To respond to this type, reply with an auth dict as follows::
|
||||||
"response": "<captcha response>"
|
"response": "<captcha response>"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Token-based
|
||||||
|
~~~~~~~~~~~
|
||||||
|
:Type:
|
||||||
|
``m.login.token``
|
||||||
|
:Description:
|
||||||
|
The client submits a username and token.
|
||||||
|
|
||||||
|
To respond to this type, reply with an auth dict as follows::
|
||||||
|
|
||||||
|
{
|
||||||
|
"type": "m.login.token",
|
||||||
|
"user": "<user_id or user localpart>",
|
||||||
|
"token": "<token>",
|
||||||
|
"txn_id": "<client generated nonce>"
|
||||||
|
}
|
||||||
|
|
||||||
|
The ``nonce`` should be a random string generated by the client for the
|
||||||
|
request. The same ``nonce`` should be used if retrying the request.
|
||||||
|
|
||||||
|
There are many ways a client may receive a ``token``, including via an email or
|
||||||
|
from an existing logged in device.
|
||||||
|
|
||||||
|
The ``txn_id`` may be used by the server to disallow other devices from using
|
||||||
|
the token, thus providing "single use" tokens while still allowing the device
|
||||||
|
to retry the request. This would be done by tying the token to the ``txn_id``
|
||||||
|
server side, as well as potentially invalidating the token completely once the
|
||||||
|
device has successfully logged in (e.g. when we receive a request from the
|
||||||
|
newly provisioned access_token).
|
||||||
|
|
||||||
|
The ``token`` must be a macaroon.
|
||||||
|
|
||||||
OAuth2-based
|
OAuth2-based
|
||||||
~~~~~~~~~~~~
|
~~~~~~~~~~~~
|
||||||
:Type:
|
:Type:
|
||||||
|
@ -395,6 +427,8 @@ the complete dataset is provided in "chunk".
|
||||||
Events
|
Events
|
||||||
------
|
------
|
||||||
|
|
||||||
|
.. _sect:events:
|
||||||
|
|
||||||
Overview
|
Overview
|
||||||
~~~~~~~~
|
~~~~~~~~
|
||||||
|
|
||||||
|
@ -640,13 +674,10 @@ to add keys that are, for example offensive or illegal. Since some events
|
||||||
cannot be simply deleted, e.g. membership events, we instead 'redact' events.
|
cannot be simply deleted, e.g. membership events, we instead 'redact' events.
|
||||||
This involves removing all keys from an event that are not required by the
|
This involves removing all keys from an event that are not required by the
|
||||||
protocol. This stripped down event is thereafter returned anytime a client or
|
protocol. This stripped down event is thereafter returned anytime a client or
|
||||||
remote server requests it.
|
remote server requests it. Redacting an event cannot be undone, allowing server
|
||||||
|
owners to delete the offending content from the databases. Events that have been
|
||||||
Events that have been redacted include a ``redacted_because`` key whose value
|
redacted include a ``redacted_because`` key whose value is the event that caused
|
||||||
is the event that caused it to be redacted, which may include a reason.
|
it to be redacted, which may include a reason.
|
||||||
|
|
||||||
Redacting an event cannot be undone, allowing server owners to delete the
|
|
||||||
offending content from the databases.
|
|
||||||
|
|
||||||
.. TODO
|
.. TODO
|
||||||
Currently, only room admins can redact events by sending a ``m.room.redaction``
|
Currently, only room admins can redact events by sending a ``m.room.redaction``
|
||||||
|
@ -677,12 +708,10 @@ one of the following event types:
|
||||||
.. TODO
|
.. TODO
|
||||||
Need to update m.room.power_levels to reflect new power levels formatting
|
Need to update m.room.power_levels to reflect new power levels formatting
|
||||||
|
|
||||||
The redaction event should be added under the key ``redacted_because``.
|
The redaction event should be added under the key ``redacted_because``. When a
|
||||||
|
client receives a redaction event it should change the redacted event
|
||||||
When a client receives a redaction event it should change the redacted event
|
|
||||||
in the same way a server does.
|
in the same way a server does.
|
||||||
|
|
||||||
|
|
||||||
Rooms
|
Rooms
|
||||||
-----
|
-----
|
||||||
|
|
||||||
|
@ -762,10 +791,47 @@ options which can be set when creating a room:
|
||||||
Description:
|
Description:
|
||||||
Allows clients to add keys to the content of ``m.room.create``.
|
Allows clients to add keys to the content of ``m.room.create``.
|
||||||
|
|
||||||
|
``preset``
|
||||||
|
Type:
|
||||||
|
String
|
||||||
|
Optional:
|
||||||
|
Yes
|
||||||
|
Value:
|
||||||
|
``private_chat``, ``trusted_private_chat`` or ``public_chat``
|
||||||
|
Description:
|
||||||
|
Convenience parameter for setting various default state events based on a
|
||||||
|
preset.
|
||||||
|
|
||||||
|
Three presets are defined:
|
||||||
|
|
||||||
|
- ``private_chat``: Sets the ``join_rules`` to ``invite`` and
|
||||||
|
``history_visibility`` to ``shared``
|
||||||
|
- ``trusted_private_chat``: Set the ``join_rules`` to ``invite``,
|
||||||
|
``history_visibility`` to ``shared`` and gives all invitees the same
|
||||||
|
power level as the creator.
|
||||||
|
- ``public_chat``: Sets the ``join_rules`` to ``public`` and
|
||||||
|
``history_visibility`` to ``shared``
|
||||||
|
|
||||||
|
``initial_state``
|
||||||
|
Type:
|
||||||
|
List
|
||||||
|
Optional:
|
||||||
|
Yes
|
||||||
|
Value:
|
||||||
|
A list of state events to set in the new room.
|
||||||
|
Description:
|
||||||
|
Allows the user to override the default state events set in the new room.
|
||||||
|
|
||||||
|
The expected format of the state events are an object with ``type``,
|
||||||
|
``state_key`` and ``content`` keys set.
|
||||||
|
|
||||||
|
Takes precedence over events set by ``presets``, but gets overriden by
|
||||||
|
``name`` and ``topic`` keys.
|
||||||
|
|
||||||
Example::
|
Example::
|
||||||
|
|
||||||
{
|
{
|
||||||
"visibility": "public",
|
"preset": "public_chat",
|
||||||
"room_alias_name": "thepub",
|
"room_alias_name": "thepub",
|
||||||
"name": "The Grand Duke Pub",
|
"name": "The Grand Duke Pub",
|
||||||
"topic": "All about happy hour",
|
"topic": "All about happy hour",
|
||||||
|
@ -836,18 +902,14 @@ Permissions
|
||||||
|
|
||||||
Permissions for rooms are done via the concept of power levels - to do any
|
Permissions for rooms are done via the concept of power levels - to do any
|
||||||
action in a room a user must have a suitable power level. Power levels are
|
action in a room a user must have a suitable power level. Power levels are
|
||||||
stored as state events in a given room.
|
stored as state events in a given room. The power levels required for operations
|
||||||
|
and the power levels for users are defined in ``m.room.power_levels``, where
|
||||||
The power levels required for operations and the power levels for users are
|
both a default and specific users' power levels can be set.
|
||||||
defined in ``m.room.power_levels``, where both a default and specific users'
|
|
||||||
power levels can be set.
|
|
||||||
|
|
||||||
By default all users have a power level of 0, other than the room creator whose
|
By default all users have a power level of 0, other than the room creator whose
|
||||||
power level defaults to 100. Users can grant other users increased power levels
|
power level defaults to 100. Users can grant other users increased power levels
|
||||||
up to their own power level. For example, user A with a power level of 50 could
|
up to their own power level. For example, user A with a power level of 50 could
|
||||||
increase the power level of user B to a maximum of level 50. Power levels for
|
increase the power level of user B to a maximum of level 50. Power levels for
|
||||||
users are tracked per-room even if the user is not present in the room.
|
users are tracked per-room even if the user is not present in the room.
|
||||||
|
|
||||||
The keys contained in ``m.room.power_levels`` determine the levels required for
|
The keys contained in ``m.room.power_levels`` determine the levels required for
|
||||||
certain operations such as kicking, banning and sending state events. See
|
certain operations such as kicking, banning and sending state events. See
|
||||||
`m.room.power_levels`_ for more information.
|
`m.room.power_levels`_ for more information.
|
||||||
|
@ -864,10 +926,9 @@ room. There are several states in which a user may be, in relation to a room:
|
||||||
- Banned (the user is not allowed to join the room)
|
- Banned (the user is not allowed to join the room)
|
||||||
|
|
||||||
Some rooms require that users be invited to it before they can join; others
|
Some rooms require that users be invited to it before they can join; others
|
||||||
allow anyone to join.
|
allow anyone to join. Whether a given room is an "invite-only" room is
|
||||||
|
determined by the room config key ``m.room.join_rules``. It can have one of the
|
||||||
Whether a given room is an "invite-only" room is determined by the room config
|
following values:
|
||||||
key ``m.room.join_rules``. It can have one of the following values:
|
|
||||||
|
|
||||||
``public``
|
``public``
|
||||||
This room is free for anyone to join without an invite.
|
This room is free for anyone to join without an invite.
|
||||||
|
@ -1075,6 +1136,27 @@ Profiles
|
||||||
|
|
||||||
{{profile_http_api}}
|
{{profile_http_api}}
|
||||||
|
|
||||||
|
Events on Change of Profile Information
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
Because the profile display name and avatar information are likely to be used in
|
||||||
|
many places of a client's display, changes to these fields cause an automatic
|
||||||
|
propagation event to occur, informing likely-interested parties of the new
|
||||||
|
values. This change is conveyed using two separate mechanisms:
|
||||||
|
|
||||||
|
- a ``m.room.member`` event is sent to every room the user is a member of,
|
||||||
|
to update the ``displayname`` and ``avatar_url``.
|
||||||
|
- a ``m.presence`` presence status update is sent, again containing the new
|
||||||
|
values of the ``displayname`` and ``avatar_url`` keys, in addition to the
|
||||||
|
required ``presence`` key containing the current presence state of the user.
|
||||||
|
|
||||||
|
Both of these should be done automatically by the home server when a user
|
||||||
|
successfully changes their display name or avatar URL fields.
|
||||||
|
|
||||||
|
Additionally, when home servers emit room membership events for their own
|
||||||
|
users, they should include the display name and avatar URL fields in these
|
||||||
|
events so that clients already have these details to hand, and do not have to
|
||||||
|
perform extra round trips to query it.
|
||||||
|
|
||||||
Security
|
Security
|
||||||
--------
|
--------
|
||||||
|
|
||||||
|
|
|
@ -4,11 +4,9 @@ Application Service API
|
||||||
The Matrix client-server API and server-server APIs provide the means to
|
The Matrix client-server API and server-server APIs provide the means to
|
||||||
implement a consistent self-contained federated messaging fabric. However, they
|
implement a consistent self-contained federated messaging fabric. However, they
|
||||||
provide limited means of implementing custom server-side behaviour in Matrix
|
provide limited means of implementing custom server-side behaviour in Matrix
|
||||||
(e.g. gateways, filters, extensible hooks etc).
|
(e.g. gateways, filters, extensible hooks etc). The Application Service API
|
||||||
|
defines a standard API to allow such extensible functionality to be implemented
|
||||||
The Application Service API defines a standard API to allow such extensible
|
irrespective of the underlying homeserver implementation.
|
||||||
functionality to be implemented irrespective of the underlying homeserver
|
|
||||||
implementation.
|
|
||||||
|
|
||||||
.. TODO-spec
|
.. TODO-spec
|
||||||
Add in Client-Server services? Overview of bots? Seems weird to be in the spec
|
Add in Client-Server services? Overview of bots? Seems weird to be in the spec
|
||||||
|
@ -18,12 +16,10 @@ Passive Application Services
|
||||||
----------------------------
|
----------------------------
|
||||||
"Passive" application services can only observe events from a given home server.
|
"Passive" application services can only observe events from a given home server.
|
||||||
They cannot prevent events from being sent, nor can they modify the content of
|
They cannot prevent events from being sent, nor can they modify the content of
|
||||||
the event being sent.
|
the event being sent. In order to observe events from a homeserver, the
|
||||||
|
homeserver needs to be configured to pass certain types of traffic to the
|
||||||
In order to observe events from a homeserver, the homeserver needs to be
|
application service. This is achieved by manually configuring the homeserver
|
||||||
configured to pass certain types of traffic to the application service. This
|
with information about the AS.
|
||||||
is achieved by manually configuring the homeserver with information about the
|
|
||||||
AS..
|
|
||||||
|
|
||||||
.. NOTE::
|
.. NOTE::
|
||||||
Previously, application services could register with a homeserver via HTTP
|
Previously, application services could register with a homeserver via HTTP
|
||||||
|
|
|
@ -59,13 +59,11 @@ and an optional TLS port.
|
||||||
.. **
|
.. **
|
||||||
|
|
||||||
If the port is present then the server is discovered by looking up an AAAA or
|
If the port is present then the server is discovered by looking up an AAAA or
|
||||||
A record for the DNS name and connecting to the specified TLS port.
|
A record for the DNS name and connecting to the specified TLS port. If the port
|
||||||
|
is absent then the server is discovered by looking up a ``_matrix._tcp`` SRV
|
||||||
If the port is absent then the server is discovered by looking up a
|
record for the DNS name. If this record does not exist then the server is
|
||||||
``_matrix._tcp`` SRV record for the DNS name. If this record does not exist
|
discovered by looking up an AAAA or A record on the DNS name and taking the
|
||||||
then the server is discovered by looking up an AAAA or A record on the DNS
|
default fallback port number of 8448.
|
||||||
name and taking the default fallback port number of 8448.
|
|
||||||
|
|
||||||
Home servers may use SRV records to load balance requests between multiple TLS
|
Home servers may use SRV records to load balance requests between multiple TLS
|
||||||
endpoints or to failover to another endpoint if an endpoint fails.
|
endpoints or to failover to another endpoint if an endpoint fails.
|
||||||
|
|
||||||
|
|
56
specification/modules/_template.rst
Normal file
56
specification/modules/_template.rst
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
Module Heading
|
||||||
|
==============
|
||||||
|
|
||||||
|
.. _module:short-name:
|
||||||
|
|
||||||
|
A short summary of the module. What features does this module provide? An anchor
|
||||||
|
should be specified at the top of the module using the format ``module:name``.
|
||||||
|
|
||||||
|
Complicated modules may wish to have architecture diagrams or event flows
|
||||||
|
(e.g. VoIP call flows) here. Custom subsections can be included but they should
|
||||||
|
be used *sparingly* to reduce the risk of putting client or server behaviour
|
||||||
|
information in these custom sections.
|
||||||
|
|
||||||
|
Events
|
||||||
|
------
|
||||||
|
List the new event types introduced by this module, if any. If there are no
|
||||||
|
new events, this section can be omitted. Event types should be done as
|
||||||
|
subsections. This section is intended to document the "common shared event
|
||||||
|
structure" between client and server. Deviations from this shared structure
|
||||||
|
should be documented in the relevant behaviour section.
|
||||||
|
|
||||||
|
``m.example.event.type``
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
There should be JSON Schema docs for this event. Once there is JSON schema,
|
||||||
|
there will be a template variable with dots in the event type replaced with
|
||||||
|
underscores and the suffix ``_event``. You can insert a template like so:
|
||||||
|
|
||||||
|
{{m_example_event_type_event}}
|
||||||
|
|
||||||
|
Client behaviour
|
||||||
|
----------------
|
||||||
|
List any new HTTP endpoints. These endpoints should be documented using Swagger.
|
||||||
|
Once there is Swagger, there will be a template variable based on the name of
|
||||||
|
the YAML file with the suffix ``_http_api``. You can insert a template for
|
||||||
|
swagger docs like so:
|
||||||
|
|
||||||
|
{{name-of-yaml-file-without-file-ext_http_api}}
|
||||||
|
|
||||||
|
List the steps the client needs to take to
|
||||||
|
correctly process this module. List what data structures the client should be
|
||||||
|
storing in order to aid implementation.
|
||||||
|
|
||||||
|
Server behaviour
|
||||||
|
----------------
|
||||||
|
Does the server need to handle any of the new events in a special way (e.g.
|
||||||
|
typing timeouts, presence). Advice on how to persist events and/or requests are
|
||||||
|
recommended to aid implementation. Federation-specific logic should be included
|
||||||
|
here.
|
||||||
|
|
||||||
|
Security considerations
|
||||||
|
-----------------------
|
||||||
|
This includes privacy leaks: for example leaking presence info. How do
|
||||||
|
misbehaving clients or servers impact this module? This section should always be
|
||||||
|
included, if only to say "we've thought about it but there isn't anything to do
|
||||||
|
here".
|
||||||
|
|
|
@ -1,44 +1,33 @@
|
||||||
Content repository
|
Content repository
|
||||||
==================
|
==================
|
||||||
|
|
||||||
HTTP API
|
.. _module:content:
|
||||||
--------
|
|
||||||
|
|
||||||
Uploads are POSTed to a resource which returns a token which is used to GET
|
This module allows users to upload content to their homeserver which is
|
||||||
the download. Uploads are POSTed to the sender's local homeserver, but are
|
retrievable from other homeservers. Its' purpose is to allow users to share
|
||||||
downloaded from the recipient's local homeserver, which must thus first transfer
|
attachments in a room. Content locations are represented as Matrix Content (MXC)
|
||||||
the content from the origin homeserver using the same API (unless the origin
|
URIs. They look like::
|
||||||
and destination homeservers are the same). The upload/download API is::
|
|
||||||
|
|
||||||
=> POST /_matrix/media/v1/upload HTTP/1.1
|
mxc://<server-name>/<media-id>
|
||||||
Content-Type: <media-type>
|
|
||||||
|
|
||||||
<media>
|
<server-name> : The name of the homeserver where this content originated, e.g. matrix.org
|
||||||
|
<media-id> : An opaque ID which identifies the content.
|
||||||
|
|
||||||
<= HTTP/1.1 200 OK
|
Uploads are POSTed to a resource on the user's local homeserver which returns a
|
||||||
Content-Type: application/json
|
token which is used to GET the download. Content is downloaded from the
|
||||||
|
recipient's local homeserver, which must first transfer the content from the
|
||||||
|
origin homeserver using the same API (unless the origin and destination
|
||||||
|
homeservers are the same).
|
||||||
|
|
||||||
{ "content-uri": "mxc://<server-name>/<media-id>" }
|
Client behaviour
|
||||||
|
----------------
|
||||||
|
|
||||||
=> GET /_matrix/media/v1/download/<server-name>/<media-id> HTTP/1.1
|
Clients can upload and download content using the following HTTP APIs.
|
||||||
|
|
||||||
<= HTTP/1.1 200 OK
|
{{content_repo_http_api}}
|
||||||
Content-Type: <media-type>
|
|
||||||
Content-Disposition: attachment;filename=<upload-filename>
|
|
||||||
|
|
||||||
<media>
|
|
||||||
|
|
||||||
Clients can get thumbnails by supplying a desired width and height and
|
|
||||||
thumbnailing method::
|
|
||||||
|
|
||||||
=> GET /_matrix/media/v1/thumbnail/<server_name>
|
|
||||||
/<media-id>?width=<w>&height=<h>&method=<m> HTTP/1.1
|
|
||||||
|
|
||||||
<= HTTP/1.1 200 OK
|
|
||||||
Content-Type: image/jpeg or image/png
|
|
||||||
|
|
||||||
<thumbnail>
|
|
||||||
|
|
||||||
|
Thumbnails
|
||||||
|
~~~~~~~~~~
|
||||||
The thumbnail methods are "crop" and "scale". "scale" tries to return an
|
The thumbnail methods are "crop" and "scale". "scale" tries to return an
|
||||||
image where either the width or the height is smaller than the requested
|
image where either the width or the height is smaller than the requested
|
||||||
size. The client should then scale and letterbox the image if it needs to
|
size. The client should then scale and letterbox the image if it needs to
|
||||||
|
@ -47,6 +36,9 @@ width and height are close to the requested size and the aspect matches
|
||||||
the requested size. The client should scale the image if it needs to fit
|
the requested size. The client should scale the image if it needs to fit
|
||||||
within a given rectangle.
|
within a given rectangle.
|
||||||
|
|
||||||
|
Server behaviour
|
||||||
|
----------------
|
||||||
|
|
||||||
Homeservers may generate thumbnails for content uploaded to remote
|
Homeservers may generate thumbnails for content uploaded to remote
|
||||||
homeservers themselves or may rely on the remote homeserver to thumbnail
|
homeservers themselves or may rely on the remote homeserver to thumbnail
|
||||||
the content. Homeservers may return thumbnails of a different size to that
|
the content. Homeservers may return thumbnails of a different size to that
|
||||||
|
@ -56,21 +48,27 @@ Homeservers must never upscale images.
|
||||||
Security considerations
|
Security considerations
|
||||||
-----------------------
|
-----------------------
|
||||||
|
|
||||||
Clients may try to upload very large files. Homeservers should not store files
|
The HTTP GET endpoint does not require any authentication. Knowing the URL of
|
||||||
that are too large and should not serve them to clients.
|
the content is sufficient to retrieve the content, even if the entity isn't in
|
||||||
|
the room.
|
||||||
|
|
||||||
Clients may try to upload very large images. Homeservers should not attempt to
|
Homeservers have additional concerns:
|
||||||
generate thumbnails for images that are too large.
|
|
||||||
|
|
||||||
Remote homeservers may host very large files or images. Homeserver should not
|
- Clients may try to upload very large files. Homeservers should not store files
|
||||||
proxy or thumbnail large files or images from remote homeservers.
|
that are too large and should not serve them to clients.
|
||||||
|
|
||||||
Clients may try to upload a large number of files. Homeservers should limit the
|
- Clients may try to upload very large images. Homeservers should not attempt to
|
||||||
number and total size of media that can be uploaded by clients.
|
generate thumbnails for images that are too large.
|
||||||
|
|
||||||
Clients may try to access a large number of remote files through a homeserver.
|
- Remote homeservers may host very large files or images. Homeservers should not
|
||||||
Homeservers should restrict the number and size of remote files that it caches.
|
proxy or thumbnail large files or images from remote homeservers.
|
||||||
|
|
||||||
Clients or remote homeservers may try to upload malicious files targeting
|
- Clients may try to upload a large number of files. Homeservers should limit the
|
||||||
vulnerabilities in either the homeserver thumbnailing or the client decoders.
|
number and total size of media that can be uploaded by clients.
|
||||||
|
|
||||||
|
- Clients may try to access a large number of remote files through a homeserver.
|
||||||
|
Homeservers should restrict the number and size of remote files that it caches.
|
||||||
|
|
||||||
|
- Clients or remote homeservers may try to upload malicious files targeting
|
||||||
|
vulnerabilities in either the homeserver thumbnailing or the client decoders.
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
End-to-End Encryption
|
End-to-End Encryption
|
||||||
=====================
|
=====================
|
||||||
|
|
||||||
|
.. _module:e2e:
|
||||||
|
|
||||||
.. TODO-doc
|
.. TODO-doc
|
||||||
- Why is this needed.
|
- Why is this needed.
|
||||||
- Overview of process
|
- Overview of process
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
Room History Visibility
|
Room History Visibility
|
||||||
-----------------------
|
-----------------------
|
||||||
|
|
||||||
|
.. _module:history-visibility:
|
||||||
|
|
||||||
Whether a member of a room can see the events that happened in a room from
|
Whether a member of a room can see the events that happened in a room from
|
||||||
before they joined the room is controlled by the ``history_visibility`` key
|
before they joined the room is controlled by the ``history_visibility`` key
|
||||||
of the ``m.room.history_visibility`` state event. The valid values for
|
of the ``m.room.history_visibility`` state event. The valid values for
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
Instant Messaging
|
Instant Messaging
|
||||||
=================
|
=================
|
||||||
|
|
||||||
|
.. _module:im:
|
||||||
|
|
||||||
Events
|
Events
|
||||||
------
|
------
|
||||||
|
|
||||||
|
|
|
@ -1,63 +1,112 @@
|
||||||
Presence
|
Presence
|
||||||
========
|
========
|
||||||
|
|
||||||
|
.. _module:presence:
|
||||||
|
|
||||||
|
Each user has the concept of presence information. This encodes:
|
||||||
|
|
||||||
|
* Whether the user is currently online
|
||||||
|
* How recently the user was last active (as seen by the server)
|
||||||
|
* Whether a given client considers the user to be currently idle
|
||||||
|
* Arbitrary information about the user's current status (e.g. "in a meeting").
|
||||||
|
|
||||||
|
This information is collated from both per-device (``online``, ``idle``,
|
||||||
|
``last_active``) and per-user (status) data, aggregated by the user's homeserver
|
||||||
|
and transmitted as an ``m.presence`` event. This is one of the few events which
|
||||||
|
are sent *outside the context of a room*. Presence events are sent to all users
|
||||||
|
who subscribe to this user's presence through a presence list or by sharing
|
||||||
|
membership of a room.
|
||||||
|
|
||||||
|
A presence list is a list of user IDs whose presence the user wants to follow.
|
||||||
|
To be added to this list, the user being added must be invited by the list owner
|
||||||
|
who must accept the invitation.
|
||||||
|
|
||||||
Each user has the concept of presence information. This encodes the
|
User's presence state is represented by the ``presence`` key, which is an enum
|
||||||
"availability" of that user, suitable for display on other user's clients.
|
of one of the following:
|
||||||
This is transmitted as an ``m.presence`` event and is one of the few events
|
|
||||||
which are sent *outside the context of a room*. The basic piece of presence
|
|
||||||
information is represented by the ``presence`` key, which is an enum of one
|
|
||||||
of the following:
|
|
||||||
|
|
||||||
- ``online`` : The default state when the user is connected to an event
|
- ``online`` : The default state when the user is connected to an event
|
||||||
stream.
|
stream.
|
||||||
- ``unavailable`` : The user is not reachable at this time.
|
- ``unavailable`` : The user is not reachable at this time e.g. they are
|
||||||
- ``offline`` : The user is not connected to an event stream.
|
idle.
|
||||||
|
- ``offline`` : The user is not connected to an event stream or is
|
||||||
|
explicitly suppressing their profile information from being sent.
|
||||||
- ``free_for_chat`` : The user is generally willing to receive messages
|
- ``free_for_chat`` : The user is generally willing to receive messages
|
||||||
moreso than default.
|
moreso than default.
|
||||||
- ``hidden`` : Behaves as offline, but allows the user to see the client
|
|
||||||
state anyway and generally interact with client features. (Not yet
|
|
||||||
implemented in synapse).
|
|
||||||
|
|
||||||
In addition, the server maintains a timestamp of the last time it saw a
|
|
||||||
pro-active event from the user; either sending a message to a room, or
|
|
||||||
changing presence state from a lower to a higher level of availability
|
|
||||||
(thus: changing state from ``unavailable`` to ``online`` counts as a
|
|
||||||
proactive event, whereas in the other direction it will not). This timestamp
|
|
||||||
is presented via a key called ``last_active_ago``, which gives the relative
|
|
||||||
number of milliseconds since the message is generated/emitted that the user
|
|
||||||
was last seen active.
|
|
||||||
|
|
||||||
Events
|
Events
|
||||||
------
|
------
|
||||||
|
|
||||||
{{presence_events}}
|
{{presence_events}}
|
||||||
|
|
||||||
Presence HTTP API
|
Client behaviour
|
||||||
-----------------
|
----------------
|
||||||
.. TODO-spec
|
|
||||||
- Define how users receive presence invites, and how they accept/decline them
|
Clients can manually set/get their presence/presence list using the HTTP APIs
|
||||||
|
listed below.
|
||||||
|
|
||||||
{{presence_http_api}}
|
{{presence_http_api}}
|
||||||
|
|
||||||
|
Idle timeout
|
||||||
|
~~~~~~~~~~~~
|
||||||
|
|
||||||
|
Clients SHOULD implement an "idle timeout". This is a timer which fires after
|
||||||
|
a period of inactivity on the client. The definition of inactivity varies
|
||||||
|
depending on the client. For example, web implementations may determine
|
||||||
|
inactivity to be not moving the mouse for a certain period of time. When this
|
||||||
|
timer fires it should set the presence state to ``unavailable``. When the user
|
||||||
|
becomes active again (e.g. by moving the mouse) the client should set the
|
||||||
|
presence state to ``online``. A timeout value between 1 and 5 minutes is
|
||||||
|
recommended.
|
||||||
|
|
||||||
|
Server behaviour
|
||||||
|
----------------
|
||||||
|
|
||||||
|
Each user's home server stores a "presence list" per user. Once a user accepts
|
||||||
|
a presence list, both user's HSes must track the subscription.
|
||||||
|
|
||||||
|
Propagating profile information
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
Because the profile display name and avatar information are likely to be used in
|
||||||
|
many places of a client's display, changes to these fields SHOULD cause an
|
||||||
|
automatic propagation event to occur, informing likely-interested parties of the
|
||||||
|
new values. One of these change mechanisms SHOULD be via ``m.presence`` events.
|
||||||
|
These events should set ``displayname`` and ``avatar_url`` to the new values
|
||||||
|
along with the presence-specific keys. This SHOULD be done automatically by the
|
||||||
|
home server when a user successfully changes their display name or avatar URL.
|
||||||
|
|
||||||
|
.. admonition:: Rationale
|
||||||
|
|
||||||
|
The intention for sending this information in ``m.presence`` is so that any
|
||||||
|
"user list" can display the *current* name/presence for a user ID outside the
|
||||||
|
scope of a room e.g. for a user page. This is bundled into a single event for
|
||||||
|
several reasons. The user's display name can change per room. This
|
||||||
|
event provides the "canonical" name for the user. In addition, the name is
|
||||||
|
bundled into a single event for the ease of client implementations. If this
|
||||||
|
was not done, the client would need to search all rooms for their own
|
||||||
|
membership event to pull out the display name.
|
||||||
|
|
||||||
|
|
||||||
|
Last active ago
|
||||||
|
~~~~~~~~~~~~~~~
|
||||||
|
The server maintains a timestamp of the last time it saw a
|
||||||
|
pro-active event from the user. A pro-active event may be sending a message to a
|
||||||
|
room or changing presence state to a higher level of availability. Levels of
|
||||||
|
availability are defined from low to high as follows:
|
||||||
|
|
||||||
|
- ``offline``
|
||||||
|
- ``unavailable``
|
||||||
|
- ``online``
|
||||||
|
- ``free_for_chat``
|
||||||
|
|
||||||
|
Based on this list, changing state from ``unavailable`` to ``online`` counts as
|
||||||
|
a pro-active event, whereas ``online`` to ``unavailable`` does not. This
|
||||||
|
timestamp is presented via a key called ``last_active_ago`` which gives the
|
||||||
|
relative number of milliseconds since the pro-active event.
|
||||||
|
|
||||||
|
Security considerations
|
||||||
|
-----------------------
|
||||||
|
|
||||||
|
Presence information is shared with all users who share a room with the target
|
||||||
Events on Change of Profile Information
|
user. In large public rooms this could be undesirable.
|
||||||
---------------------------------------
|
|
||||||
Because the profile displayname and avatar information are likely to be used in
|
|
||||||
many places of a client's display, changes to these fields cause an automatic
|
|
||||||
propagation event to occur, informing likely-interested parties of the new
|
|
||||||
values. This change is conveyed using two separate mechanisms:
|
|
||||||
|
|
||||||
- a ``m.room.member`` event is sent to every room the user is a member of,
|
|
||||||
to update the ``displayname`` and ``avatar_url``.
|
|
||||||
- a ``m.presence`` presence status update is sent, again containing the new values of the
|
|
||||||
``displayname`` and ``avatar_url`` keys, in addition to the required
|
|
||||||
``presence`` key containing the current presence state of the user.
|
|
||||||
|
|
||||||
Both of these should be done automatically by the home server when a user
|
|
||||||
successfully changes their displayname or avatar URL fields.
|
|
||||||
|
|
||||||
Additionally, when home servers emit room membership events for their own
|
|
||||||
users, they should include the displayname and avatar URL fields in these
|
|
||||||
events so that clients already have these details to hand, and do not have to
|
|
||||||
perform extra roundtrips to query it.
|
|
||||||
|
|
||||||
|
|
|
@ -99,16 +99,13 @@ be redundant. Actions for the highest priority rule and only that rule apply
|
||||||
(for example, a set_tweak action in a lower priority rule will not apply if a
|
(for example, a set_tweak action in a lower priority rule will not apply if a
|
||||||
higher priority rule matches, even if that rule does not specify any tweaks).
|
higher priority rule matches, even if that rule does not specify any tweaks).
|
||||||
|
|
||||||
Rules also have an identifier, rule_id, which is a string. The rule_id is
|
Rules also have an identifier, ``rule_id``, which is a string. The ``rule_id``
|
||||||
unique within the kind of rule and scope: rule_ids need not be unique between
|
is unique within the kind of rule and scope: ``rule_ids`` need not be unique
|
||||||
rules of the same kind on different devices.
|
between rules of the same kind on different devices. A home server may also have
|
||||||
|
server default rules of each kind and in each scope. Server default rules are
|
||||||
A home server may also have server default rules of each kind and in each scope.
|
lower priority than user-defined rules in each scope. Server default rules (and
|
||||||
Server default rules are lower priority than user-defined rules in each scope.
|
only server default rules) begin with a dot ('.') character. In addition, all
|
||||||
Server default rules (and only server default rules) begin with a dot ('.')
|
rules may be enabled or disabled. Disabled rules never match.
|
||||||
character.
|
|
||||||
|
|
||||||
In addition, all rules may be enabled or disabled. Disabled rules never match.
|
|
||||||
|
|
||||||
If no rules match an event, the Home Server should not notify for the message
|
If no rules match an event, the Home Server should not notify for the message
|
||||||
(that is to say, the default action is "dont-notify"). Events that the user sent
|
(that is to say, the default action is "dont-notify"). Events that the user sent
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
Push Notifications
|
Push Notifications
|
||||||
==================
|
==================
|
||||||
|
|
||||||
|
.. _module:push:
|
||||||
|
|
||||||
Overview
|
Overview
|
||||||
--------
|
--------
|
||||||
|
|
||||||
|
|
|
@ -1,64 +1,64 @@
|
||||||
Receipts
|
Receipts
|
||||||
--------
|
========
|
||||||
|
|
||||||
Receipts are used to publish which events in a room the user or their devices
|
.. _module:receipts:
|
||||||
have interacted with. For example, which events the user has read. For
|
|
||||||
efficiency this is done as "up to" markers, i.e. marking a particular event
|
|
||||||
as, say, ``read`` indicates the user has read all events *up to* that event.
|
|
||||||
|
|
||||||
Client-Server API
|
This module adds in support for receipts. These receipts are a form of
|
||||||
~~~~~~~~~~~~~~~~~
|
acknowledgement of an event. This module defines a single acknowledgement:
|
||||||
|
``m.read`` which indicates that the user has read up to a given event.
|
||||||
|
|
||||||
Clients will receive receipts in the following format::
|
Sending a receipt for each event can result in sending large amounts of traffic
|
||||||
|
to a homeserver. To prevent this from becoming a problem, receipts are implemented
|
||||||
|
using "up to" markers. This marker indicates that the acknowledgement applies
|
||||||
|
to all events "up to and including" the event specified. For example, marking
|
||||||
|
an event as "read" would indicate that the user had read all events *up to* the
|
||||||
|
referenced event.
|
||||||
|
|
||||||
{
|
Events
|
||||||
"type": "m.receipt",
|
------
|
||||||
"room_id": <room_id>,
|
Each ``user_id``, ``receipt_type`` pair must be associated with only a
|
||||||
"content": {
|
single ``event_id``.
|
||||||
<event_id>: {
|
|
||||||
<receipt_type>: {
|
|
||||||
<user_id>: { "ts": <ts>, ... },
|
|
||||||
...
|
|
||||||
}
|
|
||||||
},
|
|
||||||
...
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
For example::
|
{{m_receipt_event}}
|
||||||
|
|
||||||
{
|
Client behaviour
|
||||||
"type": "m.receipt",
|
----------------
|
||||||
"room_id": "!KpjVgQyZpzBwvMBsnT:matrix.org",
|
|
||||||
"content": {
|
|
||||||
"$1435641916114394fHBLK:matrix.org": {
|
|
||||||
"read": {
|
|
||||||
"@erikj:jki.re": { "ts": 1436451550453 },
|
|
||||||
...
|
|
||||||
}
|
|
||||||
},
|
|
||||||
...
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
For efficiency, receipts are batched into one event per room. In the initialSync
|
In v1 ``/initialSync``, receipts are listed in a separate top level ``receipts``
|
||||||
and v2 sync APIs the receipts are listed in a separate top level ``receipts``
|
key. In v2 ``/sync``, receipts are contained in the ``ephemeral`` block for a
|
||||||
key. Each ``user_id``, ``receipt_type`` pair must be associated with only a
|
room. New receipts that come down the event streams are deltas which update
|
||||||
single ``event_id``. New receipts that come down the event streams are deltas.
|
existing mappings. Clients should replace older receipt acknowledgements based
|
||||||
Deltas update existing mappings, clobbering based on ``user_id``,
|
on ``user_id`` and ``receipt_type`` pairs. For example::
|
||||||
``receipt_type`` pairs.
|
|
||||||
|
|
||||||
|
Client receives m.receipt:
|
||||||
|
user = @alice:example.com
|
||||||
|
receipt_type = m.read
|
||||||
|
event_id = $aaa:example.com
|
||||||
|
|
||||||
A client can update the markers for its user by issuing a request::
|
Client receives another m.receipt:
|
||||||
|
user = @alice:example.com
|
||||||
|
receipt_type = m.read
|
||||||
|
event_id = $bbb:example.com
|
||||||
|
|
||||||
POST /_matrix/client/v2_alpha/rooms/<room_id>/receipt/read/<event_id>
|
The client should replace the older acknowledgement for $aaa:example.com with
|
||||||
|
this one for $bbb:example.com
|
||||||
|
|
||||||
Where the contents of the ``POST`` will be included in the content sent to
|
Clients should send read receipts when there is some certainty that the event in
|
||||||
other users. The server will automatically set the ``ts`` field.
|
question has been **displayed** to the user. Simply receiving an event does not
|
||||||
|
provide enough certainty that the user has seen the event. The user SHOULD need
|
||||||
|
to *take some action* such as viewing the room that the event was sent to or
|
||||||
|
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.
|
||||||
|
|
||||||
Server-Server API
|
{{v2_receipts_http_api}}
|
||||||
~~~~~~~~~~~~~~~~~
|
|
||||||
|
Server behaviour
|
||||||
|
----------------
|
||||||
|
|
||||||
|
For efficiency, receipts SHOULD be batched into one event per room before
|
||||||
|
delivering them to clients.
|
||||||
|
|
||||||
Receipts are sent across federation as EDUs with type ``m.receipt``. The
|
Receipts are sent across federation as EDUs with type ``m.receipt``. The
|
||||||
format of the EDUs are::
|
format of the EDUs are::
|
||||||
|
@ -73,5 +73,12 @@ format of the EDUs are::
|
||||||
...
|
...
|
||||||
}
|
}
|
||||||
|
|
||||||
These are always sent as deltas to previously sent receipts.
|
These are always sent as deltas to previously sent receipts. Currently only a
|
||||||
|
single ``<receipt_type>`` should be used: ``m.read``.
|
||||||
|
|
||||||
|
Security considerations
|
||||||
|
-----------------------
|
||||||
|
|
||||||
|
As receipts are sent outside the context of the event graph, there are no
|
||||||
|
integrity checks performed on the contents of ``m.receipt`` events.
|
||||||
|
|
||||||
|
|
|
@ -1,47 +1,46 @@
|
||||||
Typing Notifications
|
Typing Notifications
|
||||||
--------------------
|
====================
|
||||||
|
|
||||||
Client APIs
|
.. _module:typing:
|
||||||
~~~~~~~~~~~
|
|
||||||
|
|
||||||
To set "I am typing for the next N msec"::
|
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.
|
||||||
|
|
||||||
PUT .../rooms/<room_id>/typing/<user_id>
|
.. _Event Graph: `sect:event-graph`_
|
||||||
Content: { "typing": true, "timeout": N }
|
|
||||||
# timeout is in milliseconds; suggested no more than 20 or 30 seconds
|
|
||||||
|
|
||||||
This should be re-sent by the client to continue informing the server the user
|
Events
|
||||||
is still typing; a safety margin of 5 seconds before the expected
|
------
|
||||||
timeout runs out is recommended. Just keep declaring a new timeout, it will
|
|
||||||
replace the old one.
|
|
||||||
|
|
||||||
To set "I am no longer typing"::
|
{{m_typing_event}}
|
||||||
|
|
||||||
PUT ../rooms/<room_id>/typing/<user_id>
|
Client behaviour
|
||||||
Content: { "typing": false }
|
----------------
|
||||||
|
|
||||||
Client Events
|
When a client receives an ``m.typing`` event, it MUST use the user ID list to
|
||||||
~~~~~~~~~~~~~
|
**REPLACE** its knowledge of every user who is currently typing. The reason for
|
||||||
|
this is that the server *does not remember* users who are not currently typing
|
||||||
|
as that list gets big quickly. The client should mark as not typing any user ID
|
||||||
|
who is not in that list.
|
||||||
|
|
||||||
All room members will receive an event on the event stream::
|
It is recommended that clients store a ``boolean`` indicating whether the user
|
||||||
|
is typing or not. Whilst this value is ``true`` a timer should fire periodically
|
||||||
|
every N seconds to send a typing HTTP request. The value of N is recommended to
|
||||||
|
be no more than 20-30 seconds. This request should be re-sent by the client to
|
||||||
|
continue informing the server the user is still typing. As subsequent
|
||||||
|
requests will replace older requests, a safety margin of 5 seconds before the
|
||||||
|
expected timeout runs out is recommended. When the user stops typing, the
|
||||||
|
state change of the ``boolean`` to ``false`` should trigger another HTTP request
|
||||||
|
to inform the server that the user has stopped typing.
|
||||||
|
|
||||||
{
|
{{typing_http_api}}
|
||||||
"type": "m.typing",
|
|
||||||
"room_id": "!room-id-here:matrix.org",
|
|
||||||
"content": {
|
|
||||||
"user_ids": ["list of", "every user", "who is", "currently typing"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
The client must use this list to *REPLACE* its knowledge of every user who is
|
Server behaviour
|
||||||
currently typing. The reason for this is that the server DOES NOT remember
|
----------------
|
||||||
users who are not currently typing, as that list gets big quickly. The client
|
|
||||||
should mark as not typing, any user ID who is not in that list.
|
|
||||||
|
|
||||||
Server APIs
|
Servers MUST emit typing EDUs in a different form to ``m.typing`` events which
|
||||||
~~~~~~~~~~~
|
are shown to clients. This form looks like::
|
||||||
|
|
||||||
Servers will emit EDUs in the following form::
|
|
||||||
|
|
||||||
{
|
{
|
||||||
"type": "m.typing",
|
"type": "m.typing",
|
||||||
|
@ -52,10 +51,16 @@ Servers will emit EDUs in the following form::
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Server EDUs don't (currently) contain timing information; it is up to
|
This does not contain timing information so it is up to originating homeservers
|
||||||
originating HSes to ensure they eventually send "stop" notifications.
|
to ensure they eventually send "stop" notifications.
|
||||||
|
|
||||||
.. TODO
|
.. TODO
|
||||||
((This will eventually need addressing, as part of the wider typing/presence
|
((This will eventually need addressing, as part of the wider typing/presence
|
||||||
timer addition work))
|
timer addition work))
|
||||||
|
|
||||||
|
Security considerations
|
||||||
|
-----------------------
|
||||||
|
|
||||||
|
Clients may not wish to inform everyone in a room that they are typing and
|
||||||
|
instead only specific users in the room.
|
||||||
|
|
||||||
|
|
|
@ -1,17 +1,26 @@
|
||||||
Voice over IP
|
Voice over IP
|
||||||
-------------
|
=============
|
||||||
Matrix can also be used to set up VoIP calls. This is part of the core
|
|
||||||
specification, although is at a relatively early stage. Voice (and video) over
|
.. _module:voip:
|
||||||
Matrix is built on the WebRTC 1.0 standard. Call events are sent to a room, like
|
|
||||||
any other event. This means that clients must only send call events to rooms
|
This module outlines how two users in a room can set up a Voice over IP (VoIP)
|
||||||
with exactly two participants as currently the WebRTC standard is based around
|
call to each other. Voice and video calls are built upon the WebRTC 1.0 standard.
|
||||||
two-party communication.
|
Call signalling is achieved by sending `message events`_ to the room. As a result,
|
||||||
|
this means that clients MUST only send call events to rooms with exactly two
|
||||||
|
participants as currently the WebRTC standard is based around two-party
|
||||||
|
communication.
|
||||||
|
|
||||||
|
.. _message events: `sect:events`_
|
||||||
|
|
||||||
|
Events
|
||||||
|
------
|
||||||
|
|
||||||
{{voip_events}}
|
{{voip_events}}
|
||||||
|
|
||||||
Message Exchange
|
Client behaviour
|
||||||
~~~~~~~~~~~~~~~~
|
----------------
|
||||||
A call is set up with messages exchanged as follows:
|
|
||||||
|
A call is set up with message events exchanged as follows:
|
||||||
|
|
||||||
::
|
::
|
||||||
|
|
||||||
|
@ -38,29 +47,55 @@ Or a rejected call:
|
||||||
|
|
||||||
Calls are negotiated according to the WebRTC specification.
|
Calls are negotiated according to the WebRTC specification.
|
||||||
|
|
||||||
|
|
||||||
Glare
|
Glare
|
||||||
~~~~~
|
~~~~~
|
||||||
This specification aims to address the problem of two users calling each other
|
|
||||||
at roughly the same time and their invites crossing on the wire. It is a far
|
|
||||||
better experience for the users if their calls are connected if it is clear
|
|
||||||
that their intention is to set up a call with one another.
|
|
||||||
|
|
||||||
In Matrix, calls are to rooms rather than users (even if those rooms may only
|
"Glare" is a problem which occurs when two users call each other at roughly the
|
||||||
contain one other user) so we consider calls which are to the same room. The
|
same time. This results in the call failing to set up as there already is an
|
||||||
rules for dealing with such a situation are as follows:
|
incoming/outgoing call. A glare resolution algorithm can be used to determine
|
||||||
|
which call to hangup and which call to answer. If both clients implement the
|
||||||
|
same algorithm then they will both select the same call and the call will be
|
||||||
|
successfully connected.
|
||||||
|
|
||||||
|
|
||||||
|
As calls are "placed" to rooms rather than users, the glare resolution algorithm
|
||||||
|
outlined below is only considered for calls which are to the same room. The
|
||||||
|
algorithm is as follows:
|
||||||
|
|
||||||
|
- If an ``m.call.invite`` to a room is received whilst the client is
|
||||||
|
**preparing to send** an ``m.call.invite`` to the same room:
|
||||||
|
|
||||||
|
* the client should cancel its outgoing call and instead
|
||||||
|
automatically accept the incoming call on behalf of the user.
|
||||||
|
|
||||||
|
- If an ``m.call.invite`` to a room is received **after the client has sent**
|
||||||
|
an ``m.call.invite`` to the same room and is waiting for a response:
|
||||||
|
|
||||||
|
* the client should perform a lexicographical comparison of the call IDs of
|
||||||
|
the two calls and use the *lesser* of the two calls, aborting the
|
||||||
|
greater. If the incoming call is the lesser, the client should accept
|
||||||
|
this call on behalf of the user.
|
||||||
|
|
||||||
- If an invite to a room is received whilst the client is preparing to send an
|
|
||||||
invite to the same room, the client should cancel its outgoing call and
|
|
||||||
instead automatically accept the incoming call on behalf of the user.
|
|
||||||
- If an invite to a room is received after the client has sent an invite to
|
|
||||||
the same room and is waiting for a response, the client should perform a
|
|
||||||
lexicographical comparison of the call IDs of the two calls and use the
|
|
||||||
lesser of the two calls, aborting the greater. If the incoming call is the
|
|
||||||
lesser, the client should accept this call on behalf of the user.
|
|
||||||
|
|
||||||
The call setup should appear seamless to the user as if they had simply placed
|
The call setup should appear seamless to the user as if they had simply placed
|
||||||
a call and the other party had accepted. Thusly, any media stream that had been
|
a call and the other party had accepted. This means any media stream that had been
|
||||||
setup for use on a call should be transferred and used for the call that
|
setup for use on a call should be transferred and used for the call that
|
||||||
replaces it.
|
replaces it.
|
||||||
|
|
||||||
|
Server behaviour
|
||||||
|
----------------
|
||||||
|
|
||||||
|
The homeserver MAY provide a TURN server which clients can use to contact the
|
||||||
|
remote party. The following HTTP API endpoints will be used by clients in order
|
||||||
|
to get information about the TURN server.
|
||||||
|
|
||||||
|
{{voip_http_api}}
|
||||||
|
|
||||||
|
|
||||||
|
Security considerations
|
||||||
|
-----------------------
|
||||||
|
|
||||||
|
Calls should only be placed to rooms with one other user in them. If they are
|
||||||
|
placed to group chat rooms it is possible that another user will intercept and
|
||||||
|
answer the call.
|
||||||
|
|
||||||
|
|
|
@ -2,11 +2,11 @@ targets:
|
||||||
main: # arbitrary name to identify this build target
|
main: # arbitrary name to identify this build target
|
||||||
files: # the sort order of files to cat
|
files: # the sort order of files to cat
|
||||||
- 0-intro.rst
|
- 0-intro.rst
|
||||||
- { 1: 0-feature_profiles.rst }
|
|
||||||
- 1-client_server_api.rst
|
- 1-client_server_api.rst
|
||||||
- { 1: 0-events.rst }
|
- { 1: 0-events.rst }
|
||||||
- { 1: 0-event_signing.rst }
|
- { 1: 0-event_signing.rst }
|
||||||
- 2-modules.rst
|
- 2-modules.rst
|
||||||
|
- { 1: 0-feature_profiles.rst }
|
||||||
- { 1: "group:modules" } # reference a group of files
|
- { 1: "group:modules" } # reference a group of files
|
||||||
- 3-application_service_api.rst
|
- 3-application_service_api.rst
|
||||||
- 4-server_server_api.rst
|
- 4-server_server_api.rst
|
||||||
|
|
|
@ -122,7 +122,7 @@ def main(input_module, file_stream=None, out_dir=None, verbose=False):
|
||||||
|
|
||||||
# check the input files and substitute in sections where required
|
# check the input files and substitute in sections where required
|
||||||
log("Parsing input template: %s" % file_stream.name)
|
log("Parsing input template: %s" % file_stream.name)
|
||||||
temp_str = file_stream.read()
|
temp_str = file_stream.read().decode("utf-8")
|
||||||
# do sanity checking on the template to make sure they aren't reffing things
|
# do sanity checking on the template to make sure they aren't reffing things
|
||||||
# which will never be replaced with a section.
|
# which will never be replaced with a section.
|
||||||
ast = env.parse(temp_str)
|
ast = env.parse(temp_str)
|
||||||
|
@ -140,7 +140,7 @@ def main(input_module, file_stream=None, out_dir=None, verbose=False):
|
||||||
with open(
|
with open(
|
||||||
os.path.join(out_dir, os.path.basename(file_stream.name)), "w"
|
os.path.join(out_dir, os.path.basename(file_stream.name)), "w"
|
||||||
) as f:
|
) as f:
|
||||||
f.write(output)
|
f.write(output.encode("utf-8"))
|
||||||
log("Output file for: %s" % file_stream.name)
|
log("Output file for: %s" % file_stream.name)
|
||||||
check_unaccessed("units", units)
|
check_unaccessed("units", units)
|
||||||
|
|
||||||
|
|
|
@ -31,18 +31,18 @@ Response format:
|
||||||
{% for table in endpoint.res_tables -%}
|
{% for table in endpoint.res_tables -%}
|
||||||
{{"``"+table.title+"``" if table.title else "" }}
|
{{"``"+table.title+"``" if table.title else "" }}
|
||||||
|
|
||||||
================== ================= ===========================================
|
=================== ================= ==========================================
|
||||||
Param Type Description
|
Param Type Description
|
||||||
================== ================= ===========================================
|
=================== ================= ==========================================
|
||||||
{% for row in table.rows -%}
|
{% for row in table.rows -%}
|
||||||
{# -#}
|
{# -#}
|
||||||
{# Row type needs to prepend spaces to line up with the type column (19 ch) -#}
|
{# 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 -#}
|
{# 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) -#}
|
{# It also needs to then wrap inside the desc col (42 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)}}
|
{{row.key}}{{row.type|indent(20-row.key|length)}}{{row.desc|wrap(42,row.req_str | indent(18 - (row.type|length))) |indent_block(38)}}
|
||||||
{% endfor -%}
|
{% endfor -%}
|
||||||
================== ================= ===========================================
|
=================== ================= ==========================================
|
||||||
|
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% endif -%}
|
{% endif -%}
|
||||||
|
|
|
@ -19,6 +19,7 @@ import yaml
|
||||||
V1_CLIENT_API = "../api/client-server/v1"
|
V1_CLIENT_API = "../api/client-server/v1"
|
||||||
V1_EVENT_EXAMPLES = "../event-schemas/examples/v1"
|
V1_EVENT_EXAMPLES = "../event-schemas/examples/v1"
|
||||||
V1_EVENT_SCHEMA = "../event-schemas/schema/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"
|
CORE_EVENT_SCHEMA = "../event-schemas/schema/v1/core-event-schema"
|
||||||
CHANGELOG = "../CHANGELOG.rst"
|
CHANGELOG = "../CHANGELOG.rst"
|
||||||
TARGETS = "../specification/targets.yaml"
|
TARGETS = "../specification/targets.yaml"
|
||||||
|
@ -49,8 +50,17 @@ def get_json_schema_object_fields(obj, enforce_title=False):
|
||||||
}
|
}
|
||||||
tables = [fields]
|
tables = [fields]
|
||||||
|
|
||||||
props = obj.get("properties", obj.get("patternProperties"))
|
|
||||||
parents = obj.get("allOf")
|
parents = obj.get("allOf")
|
||||||
|
props = obj.get("properties")
|
||||||
|
if not props:
|
||||||
|
props = obj.get("patternProperties")
|
||||||
|
if props:
|
||||||
|
# try to replace horrible regex key names with pretty x-pattern ones
|
||||||
|
for key_name in props.keys():
|
||||||
|
pretty_key = props[key_name].get("x-pattern")
|
||||||
|
if pretty_key:
|
||||||
|
props[pretty_key] = props[key_name]
|
||||||
|
del props[key_name]
|
||||||
if not props and not parents:
|
if not props and not parents:
|
||||||
raise Exception(
|
raise Exception(
|
||||||
"Object %s has no properties or parents." % obj
|
"Object %s has no properties or parents." % obj
|
||||||
|
@ -70,10 +80,17 @@ def get_json_schema_object_fields(obj, enforce_title=False):
|
||||||
if props[key_name]["type"] == "object":
|
if props[key_name]["type"] == "object":
|
||||||
if props[key_name].get("additionalProperties"):
|
if props[key_name].get("additionalProperties"):
|
||||||
# not "really" an object, just a KV store
|
# not "really" an object, just a KV store
|
||||||
value_type = (
|
prop_val = props[key_name]["additionalProperties"]["type"]
|
||||||
"{string: %s}" %
|
if prop_val == "object":
|
||||||
props[key_name]["additionalProperties"]["type"]
|
nested_object = get_json_schema_object_fields(
|
||||||
)
|
props[key_name]["additionalProperties"],
|
||||||
|
enforce_title=True
|
||||||
|
)
|
||||||
|
value_type = "{string: %s}" % nested_object[0]["title"]
|
||||||
|
if not nested_object[0].get("no-table"):
|
||||||
|
tables += nested_object
|
||||||
|
else:
|
||||||
|
value_type = "{string: %s}" % prop_val
|
||||||
else:
|
else:
|
||||||
nested_object = get_json_schema_object_fields(
|
nested_object = get_json_schema_object_fields(
|
||||||
props[key_name],
|
props[key_name],
|
||||||
|
@ -151,6 +168,13 @@ class MatrixUnits(Units):
|
||||||
|
|
||||||
# assign value expected for this param
|
# assign value expected for this param
|
||||||
val_type = param.get("type") # integer/string
|
val_type = param.get("type") # integer/string
|
||||||
|
|
||||||
|
if param.get("enum"):
|
||||||
|
val_type = "enum"
|
||||||
|
desc += (
|
||||||
|
" One of: %s" % json.dumps(param.get("enum"))
|
||||||
|
)
|
||||||
|
|
||||||
refType = Units.prop(param, "schema/$ref/") # Error,Event
|
refType = Units.prop(param, "schema/$ref/") # Error,Event
|
||||||
schemaFmt = Units.prop(param, "schema/format") # bytes e.g. uploads
|
schemaFmt = Units.prop(param, "schema/format") # bytes e.g. uploads
|
||||||
if not val_type and refType:
|
if not val_type and refType:
|
||||||
|
@ -253,17 +277,27 @@ class MatrixUnits(Units):
|
||||||
if good_response:
|
if good_response:
|
||||||
self.log("Found a 200 response for this API")
|
self.log("Found a 200 response for this API")
|
||||||
res_type = Units.prop(good_response, "schema/type")
|
res_type = Units.prop(good_response, "schema/type")
|
||||||
|
res_name = Units.prop(good_response, "schema/name")
|
||||||
if res_type and res_type not in ["object", "array"]:
|
if res_type and res_type not in ["object", "array"]:
|
||||||
# response is a raw string or something like that
|
# response is a raw string or something like that
|
||||||
endpoint["res_tables"].append({
|
good_table = {
|
||||||
"title": None,
|
"title": None,
|
||||||
"rows": [{
|
"rows": [{
|
||||||
"key": good_response["schema"].get("name", ""),
|
"key": "<" + res_type + ">" if not res_name else res_name,
|
||||||
"type": res_type,
|
"type": res_type,
|
||||||
"desc": res.get("description", ""),
|
"desc": res.get("description", ""),
|
||||||
"req_str": ""
|
"req_str": ""
|
||||||
}]
|
}]
|
||||||
})
|
}
|
||||||
|
if good_response.get("headers"):
|
||||||
|
for (header_name, header) in good_response.get("headers").iteritems():
|
||||||
|
good_table["rows"].append({
|
||||||
|
"key": header_name,
|
||||||
|
"type": "Header<" + header["type"] + ">",
|
||||||
|
"desc": header["description"],
|
||||||
|
"req_str": ""
|
||||||
|
})
|
||||||
|
endpoint["res_tables"].append(good_table)
|
||||||
elif res_type and Units.prop(good_response, "schema/properties"):
|
elif res_type and Units.prop(good_response, "schema/properties"):
|
||||||
# response is an object:
|
# response is an object:
|
||||||
schema = good_response["schema"]
|
schema = good_response["schema"]
|
||||||
|
@ -320,18 +354,27 @@ class MatrixUnits(Units):
|
||||||
}
|
}
|
||||||
|
|
||||||
def load_swagger_apis(self):
|
def load_swagger_apis(self):
|
||||||
path = V1_CLIENT_API
|
paths = [
|
||||||
|
V1_CLIENT_API, V2_CLIENT_API
|
||||||
|
]
|
||||||
apis = {}
|
apis = {}
|
||||||
for filename in os.listdir(path):
|
for path in paths:
|
||||||
if not filename.endswith(".yaml"):
|
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)
|
||||||
continue
|
continue
|
||||||
self.log("Reading swagger API: %s" % filename)
|
for filename in os.listdir(path):
|
||||||
with open(os.path.join(path, filename), "r") as f:
|
if not filename.endswith(".yaml"):
|
||||||
# strip .yaml
|
continue
|
||||||
group_name = filename[:-5]
|
self.log("Reading swagger API: %s" % filename)
|
||||||
api = yaml.load(f.read())
|
with open(os.path.join(path, filename), "r") as f:
|
||||||
api["__meta"] = self._load_swagger_meta(api, group_name)
|
# strip .yaml
|
||||||
apis[group_name] = api
|
group_name = filename[:-5].replace("-", "_")
|
||||||
|
if is_v2:
|
||||||
|
group_name = "v2_" + group_name
|
||||||
|
api = yaml.load(f.read())
|
||||||
|
api["__meta"] = self._load_swagger_meta(api, group_name)
|
||||||
|
apis[group_name] = api
|
||||||
return apis
|
return apis
|
||||||
|
|
||||||
def load_common_event_fields(self):
|
def load_common_event_fields(self):
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue