Merge branch 'master' into paul/small-fixes

pull/977/head
Paul "LeoNerd" Evans 9 years ago
commit 9f286841da

5
.gitignore vendored

@ -1,3 +1,8 @@
scripts/gen
scripts/continuserv/continuserv
scripts/speculator/speculator
templating/out
*.pyc
supporting-docs/_site
supporting-docs/.sass-cache
api/node_modules

@ -6,6 +6,34 @@
.. in Jenkins. Comments like this are ignored by both RST and the templating
.. 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)
============================================
- First numbered release.

@ -0,0 +1,118 @@
#! /usr/bin/env python
import sys
import json
import os
def import_error(module, package, debian, error):
sys.stderr.write((
"Error importing %(module)s: %(error)r\n"
"To install %(module)s run:\n"
" pip install %(package)s\n"
"or on Debian run:\n"
" sudo apt-get install python-%(debian)s\n"
) % locals())
if __name__ == '__main__':
sys.exit(1)
try:
import jsonschema
except ImportError as e:
import_error("jsonschema", "jsonschema", "jsonschema", e)
raise
try:
import yaml
except ImportError as e:
import_error("yaml", "PyYAML", "yaml", e)
raise
def check_parameter(filepath, request, parameter):
schema = parameter.get("schema")
example = None
try:
example_json = schema.get('example')
if example_json and not schema.get("format") == "byte":
example = json.loads(example_json)
except Exception as e:
raise ValueError("Error parsing JSON example request for %r" % (
request
), e)
fileurl = "file://" + os.path.abspath(filepath)
if example and schema:
try:
print ("Checking request schema for: %r %r" % (
filepath, request
))
# Setting the 'id' tells jsonschema where the file is so that it
# can correctly resolve relative $ref references in the schema
schema['id'] = fileurl
jsonschema.validate(example, schema)
except Exception as e:
raise ValueError("Error validating JSON schema for %r" % (
request
), e)
def check_response(filepath, request, code, response):
example = None
try:
example_json = response.get('examples', {}).get('application/json')
if example_json:
example = json.loads(example_json)
except Exception as e:
raise ValueError("Error parsing JSON example response for %r %r" % (
request, code
), e)
schema = response.get('schema')
fileurl = "file://" + os.path.abspath(filepath)
if example and schema:
try:
print ("Checking response schema for: %r %r %r" % (
filepath, request, code
))
# Setting the 'id' tells jsonschema where the file is so that it
# can correctly resolve relative $ref references in the schema
schema['id'] = fileurl
jsonschema.validate(example, schema)
except Exception as e:
raise ValueError("Error validating JSON schema for %r %r" % (
request, code
), e)
def check_swagger_file(filepath):
with open(filepath) as f:
swagger = yaml.load(f)
for path, path_api in swagger.get('paths', {}).items():
for method, request_api in path_api.items():
request = "%s %s" % (method.upper(), path)
for parameter in request_api.get('parameters', ()):
if parameter['in'] == 'body':
check_parameter(filepath, request, parameter)
try:
responses = request_api['responses']
except KeyError:
raise ValueError("No responses for %r" % (request,))
for code, response in responses.items():
check_response(filepath, request, code, response)
if __name__ == '__main__':
paths = sys.argv[1:]
if not paths:
paths = []
for (root, dirs, files) in os.walk(os.curdir):
for filename in files:
if filename.endswith(".yaml"):
paths.append(os.path.join(root, filename))
for path in paths:
try:
check_swagger_file(path)
except Exception as e:
raise ValueError("Error checking file %r" % (path,), e)

@ -0,0 +1,201 @@
swagger: '2.0'
info:
title: "Matrix Application Service API"
version: "1.0.0"
host: localhost:8008
schemes:
- https
- http
basePath: "/"
consumes:
- application/json
produces:
- application/json
paths:
"/transactions/{txnId}":
put:
summary: Send some events to the application service.
description: |-
This API is called by the HS when the HS wants to push an event (or
batch of events) to the AS.
parameters:
- in: path
name: txnId
type: string
description: |-
The transaction ID for this set of events. Homeservers generate
these IDs and they are used to ensure idempotency of requests.
required: true
x-example: "35"
- in: body
name: body
description: A list of events
schema:
type: object
example: |-
{
"events": [
{
"age": 32,
"content": {
"body": "incoming message",
"msgtype": "m.text"
},
"event_id": "$14328055551tzaee:localhost",
"origin_server_ts": 1432804485886,
"room_id": "!TmaZBKYIFrIPVGoUYp:localhost",
"type": "m.room.message",
"user_id": "@bob:localhost"
},
{
"age": 1984,
"content": {
"body": "another incoming message",
"msgtype": "m.text"
},
"event_id": "$1228055551ffsef:localhost",
"origin_server_ts": 1432804485886,
"room_id": "!TmaZBKYIFrIPVGoUYp:localhost",
"type": "m.room.message",
"user_id": "@bob:localhost"
}
]
}
description: "Transaction informations"
properties:
events:
type: array
description: A list of events
items:
type: object
title: Event
required: ["events"]
responses:
200:
description: The transaction was processed successfully.
examples:
application/json: |-
{}
schema:
type: object
"/rooms/{roomAlias}":
get:
summary: Query if a room alias should exist on the application service.
description: |-
This endpoint is invoked by the homeserver on an application service to query
the existence of a given room alias. The homeserver will only query room
aliases inside the application service's ``aliases`` namespace. The
homeserver will send this request when it receives a request to join a
room alias within the application service's namespace.
parameters:
- in: path
name: roomAlias
type: string
description: The room alias being queried.
required: true
x-example: "#magicforest:example.com"
responses:
200:
description: |-
The application service indicates that this room alias exists. The
application service MUST have created a room and associated it with
the queried room alias using the client-server API. Additional
information about the room such as its name and topic can be set
before responding.
examples:
application/json: |-
{}
schema:
type: object
401:
description: |-
The homeserver has not supplied credentials to the application service.
Optional error information can be included in the body of this response.
examples:
application/json: |-
{
"errcode": "COM.EXAMPLE.MYAPPSERVICE_UNAUTHORIZED"
}
schema:
type: object
403:
description: |-
The credentials supplied by the homeserver were rejected.
examples:
application/json: |-
{
"errcode": "M_FORBIDDEN"
}
schema:
type: object
404:
description: |-
The application service indicates that this room alias does not exist.
Optional error information can be included in the body of this response.
examples:
application/json: |-
{
"errcode": "COM.EXAMPLE.MYAPPSERVICE_NOT_FOUND"
}
schema:
type: object
"/users/{userId}":
get:
summary: Query if a user should exist on the application service.
description: |-
This endpoint is invoked by the homeserver on an application service to query
the existence of a given user ID. The homeserver will only query user IDs
inside the application service's ``users`` namespace. The homeserver will
send this request when it receives an event for an unknown user ID in
the application service's namespace.
parameters:
- in: path
name: userId
type: string
description: The user ID being queried.
required: true
x-example: "@alice:example.com"
responses:
200:
description: |-
The application service indicates that this user exists. The application
service MUST create the user using the client-server API.
examples:
application/json: |-
{}
schema:
type: object
401:
description: |-
The homeserver has not supplied credentials to the application service.
Optional error information can be included in the body of this response.
examples:
application/json: |-
{
"errcode": "COM.EXAMPLE.MYAPPSERVICE_UNAUTHORIZED"
}
schema:
type: object
403:
description: |-
The credentials supplied by the homeserver were rejected.
examples:
application/json: |-
{
"errcode": "M_FORBIDDEN"
}
schema:
type: object
404:
description: |-
The application service indicates that this user does not exist.
Optional error information can be included in the body of this response.
examples:
application/json: |-
{
"errcode": "COM.EXAMPLE.MYAPPSERVICE_NOT_FOUND"
}
schema:
type: object

@ -15,16 +15,22 @@ paths:
summary: Upload some content to the content repository.
produces: ["application/json"]
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
name: content
name: "<content>"
description: The content to be uploaded.
required: true
schema:
type: string
example: "<bytes>"
format: byte
responses:
200:
description: Information about the uploaded content.
description: The MXC URI for the uploaded content.
schema:
type: object
required: ["content_uri"]
@ -32,6 +38,11 @@ paths:
content_uri:
type: string
description: "The MXC URI to the uploaded content."
examples:
"application/json": |-
{
"content_uri": "mxc://example.com/AQwafuaFswefuhsfAFAgsw"
}
"/download/{serverName}/{mediaId}":
get:
summary: "Download content from the content repository."
@ -40,18 +51,27 @@ paths:
- in: path
type: string
name: serverName
x-example: matrix.org
required: true
description: |
The server name from the ``mxc://`` URI (the authoritory component)
- in: path
type: string
name: mediaId
x-example: ascERGshawAWawugaAcauga
required: true
description: |
The media ID from the ``mxc://`` URI (the path component)
responses:
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:
type: file
"/thumbnail/{serverName}/{mediaId}":
@ -63,30 +83,44 @@ paths:
type: string
name: serverName
required: true
x-example: matrix.org
description: |
The server name from the ``mxc://`` URI (the authoritory component)
- in: path
type: string
name: mediaId
x-example: ascERGshawAWawugaAcauga
required: true
description: |
The media ID from the ``mxc://`` URI (the path component)
- in: query
type: integer
x-example: 64
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
type: integer
x-example: 64
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
type: string
enum: ["crop", "scale"]
name: method
x-example: "scale"
description: The desired resizing method.
responses:
200:
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:
type: file

@ -0,0 +1 @@
v1-event-schema/core-event-schema

@ -0,0 +1,148 @@
swagger: '2.0'
info:
title: "Matrix Client-Server v1 Room Creation 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:
"/createRoom":
post:
summary: Create a new room
description: |-
Create a new room with various configuration options.
security:
- accessToken: []
parameters:
- in: body
name: body
description: The desired room configuration.
schema:
type: object
example: |-
{
"preset": "public_chat",
"room_alias_name": "thepub",
"name": "The Grand Duke Pub",
"topic": "All about happy hour",
"creation_content": {
"m.federate": false
}
}
properties:
visibility:
type: string
enum: ["public", "private"]
description: |-
A ``public`` visibility indicates that the room will be shown
in the published room list. A ``private`` visibility will hide
the room from the published room list. Rooms default to
``private`` visibility if this key is not included. NB: This
should not be confused with ``join_rules`` which also uses the
word ``public``.
room_alias_name:
type: string
description: |-
The desired room alias **local part**. If this is included, a
room alias will be created and mapped to the newly created
room. The alias will belong on the *same* home server which
created the room. For example, if this was set to "foo" and
sent to the homeserver "example.com" the complete room alias
would be ``#foo:example.com``.
name:
type: string
description: |-
If this is included, an ``m.room.name`` event will be sent
into the room to indicate the name of the room. See Room
Events for more information on ``m.room.name``.
topic:
type: string
description: |-
If this is included, an ``m.room.topic`` event will be sent
into the room to indicate the topic for the room. See Room
Events for more information on ``m.room.topic``.
invite:
type: array
description: |-
A list of user IDs to invite to the room. This will tell the
server to invite everyone in the list to the newly created room.
items:
type: string
creation_content:
title: CreationContent
type: object
description: |-
Extra keys to be added to the content of the ``m.room.create``.
The server will clober the following keys: ``creator``. Future
versions of the specification may allow the server to clobber
other keys.
initial_state:
type: array
description: |-
A list of state events to set in the new room. This 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.
items:
type: object
title: StateEvent
properties:
type:
type: string
state_key:
type: string
content:
type: string
preset:
type: string
enum: ["private_chat", "public_chat", "trusted_private_chat"]
description: |-
Convenience parameter for setting various default state events
based on a preset. Must be either:
``private_chat`` =>
``join_rules`` is set to ``invite``.
``history_visibility`` is set to ``shared``.
``trusted_private_chat`` =>
``join_rules`` is set to ``invite``.
``history_visibility`` is set to ``shared``.
All invitees are given the same power level as the room creator.
``public_chat``: =>
``join_rules`` is set to ``public``.
``history_visibility`` is set to ``shared``.
responses:
200:
description: Information about the newly created room.
schema:
type: object
description: Information about the newly created room.
properties:
room_id:
type: string
description: |-
The created room's ID.
examples:
application/json: |-
{
"room_id": "!sefiuhWgwghwWgh:example.com"
}
400:
description: >
The request body is malformed or the room alias specified is already taken.

@ -1,7 +0,0 @@
type: object
description: A Matrix Event
properties:
event_id:
type: string
description: An event ID.
required: ["event_id"]

@ -0,0 +1,9 @@
{
"type": "object",
"properties": {
"kind": {
"type": "string",
"enum": ["event_match", "profile_tag", "contains_display_name", "room_member_count"]
}
}
}

@ -0,0 +1,20 @@
{
"type": "object",
"properties": {
"default": {
"type": "boolean"
},
"enabled": {
"type": "boolean"
},
"rule_id": {
"type": "string"
},
"actions": {
"items": {
"type": ["object", "string"]
},
"type": "array"
}
}
}

@ -0,0 +1,60 @@
{
"type": "object",
"properties": {
"content": {
"items": {
"type": "object",
"allOf": [
{
"$ref": "push_rule.json"
}
]
},
"type": "array"
},
"override": {
"items": {
"type": "object",
"allOf": [
{
"$ref": "push_rule.json"
}
]
},
"type": "array"
},
"sender": {
"items": {
"type": "object",
"allOf": [
{
"$ref": "push_rule.json"
}
]
},
"type": "array"
},
"underride": {
"items": {
"type": "object",
"allOf": [
{
"$ref": "push_rule.json"
}
]
},
"type": "array"
},
"room": {
"items": {
"type": "object",
"allOf": [
{
"$ref": "push_rule.json"
}
]
},
"type": "array"
}
}
}

@ -1,9 +0,0 @@
type: object
description: A Matrix Room Event
properties:
event_id:
type: string
description: An event ID.
room_id:
type: string
required: ["event_id", "room_id"]

@ -1,11 +0,0 @@
type: object
description: A Matrix State Event
properties:
event_id:
type: string
description: An event ID.
room_id:
type: string
state_key:
type: string
required: ["event_id", "room_id", "state_key"]

@ -0,0 +1,160 @@
swagger: '2.0'
info:
title: "Matrix Client-Server v1 Registration and Login 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:
"/login":
post:
summary: Authenticates the user.
description: |-
Authenticates the user by password, and issues an access token they can
use to authorize themself in subsequent requests.
security:
- accessToken: []
parameters:
- in: body
name: body
schema:
type: object
example: |-
{
"type": "m.login.pasword",
"user": "cheeky_monkey",
"password": "ilovebananas"
}
properties:
type:
type: string
description: The login type being used. Currently only "m.login.password" is supported.
user:
type: string
description: The fully qualified user ID or just local part of the user ID, to log in.
password:
type: string
description: The user's password.
required: ["type", "user", "password"]
responses:
200:
description: The user has been authenticated.
examples:
application/json: |-
{
"user_id": "@cheeky_monkey:matrix.org",
"access_token": "abc123",
"home_server": "matrix.org"
}
schema:
type: object
properties:
user_id:
type: string
description: The fully-qualified Matrix ID that has been registered.
access_token:
type: string
description: |-
An access token for the account.
This access token can then be used to authorize other requests.
The access token may expire at some point, and if so, it SHOULD come with a ``refresh_token``.
There is no specific error message to indicate that a request has failed because
an access token has expired; instead, if a client has reason to believe its
access token is valid, and it receives an auth error, they should attempt to
refresh for a new token on failure, and retry the request with the new token.
refresh_token:
type: string
# TODO: Work out how to linkify /tokenrefresh
description: |-
(optional) A ``refresh_token`` may be exchanged for a new ``access_token`` using the /tokenrefresh API endpoint.
home_server:
type: string
description: The hostname of the Home Server on which the account has been registered.
400:
description: |-
Part of the request was invalid. For example, the login type may not be recognised.
examples:
application/json: |-
{
"errcode": "M_UNKNOWN",
"error": "Bad login type."
}
403:
description: |-
The login attempt failed. For example, the password may have been incorrect.
examples:
application/json: |-
{"errcode": "M_FORBIDDEN"}
429:
description: This request was rate-limited.
schema:
"$ref": "definitions/error.yaml"
"/tokenrefresh":
post:
summary: Exchanges a refresh token for an access token.
description: |-
Exchanges a refresh token for a new access token.
This is intended to be used if the access token has expired.
security:
- accessToken: []
parameters:
- in: body
name: body
schema:
type: object
example: |-
{
"refresh_token": "a1b2c3"
}
properties:
refresh_token:
type: string
description: The refresh token which was issued by the server.
required: ["refresh_token"]
responses:
200:
description: |-
The refresh token was accepted, and a new access token has been issued.
The passed refresh token is no longer valid and cannot be used.
A new refresh token will have been returned unless some policy does
not allow the user to continue to renew their session.
examples:
application/json: |-
{
"access_token": "bearwithme123",
"refresh_token": "exchangewithme987"
}
schema:
type: object
properties:
access_token:
type: string
description: |-
An access token for the account.
This access token can then be used to authorize other requests.
The access token may expire at some point, and if so, it SHOULD come with a ``refresh_token``.
refresh_token:
type: string
description: (optional) A ``refresh_token`` may be exchanged for a new ``access_token`` using the TODO Linkify /tokenrefresh API endpoint.
403:
description: |-
The exchange attempt failed. For example, the refresh token may have already been used.
examples:
application/json: |-
{"errcode": "M_FORBIDDEN"}
429:
description: This request was rate-limited.
schema:
"$ref": "definitions/error.yaml"

@ -0,0 +1,237 @@
swagger: '2.0'
info:
title: "Matrix Client-Server v1 Room Membership API"
version: "1.0.0"
host: localhost:8008
schemes:
- https
- http
basePath: /_matrix/client/api/v1
consumes:
- application/json
produces:
- application/json
securityDefinitions:
accessToken:
type: apiKey
description: The user_id or application service access_token
name: access_token
in: query
paths:
"/rooms/{roomId}/join":
post:
summary: Start the requesting user participating in a particular room.
description: |-
This API starts a user participating in a particular room, if that user
is allowed to participate in that room. After this call, the client is
allowed to see all current state events in the room, and all subsequent
events associated with the room until the user leaves the room.
After a user has joined a room, the room will appear as an entry in the
response of the |initialSync| API.
security:
- accessToken: []
parameters:
- in: path
type: string
name: roomId
description: The room identifier or room alias to join.
required: true
x-example: "#monkeys:matrix.org"
responses:
200:
description: |-
The room has been joined.
The joined room ID must be returned in the ``room_id`` field.
examples:
application/json: |-
{"room_id": "!d41d8cd:matrix.org"}
schema:
type: object
403:
description: |-
You do not have permission to join the room. A meaningful ``errcode`` and description error text will be returned. Example reasons for rejection are:
- The room is invite-only and the user was not invited.
- The user has been banned from the room.
examples:
application/json: |-
{"errcode": "M_FORBIDDEN", "error": "You are not invited to this room."}
429:
description: This request was rate-limited.
schema:
"$ref": "definitions/error.yaml"
x-alias:
canonical-link: "post-matrix-client-api-v1-rooms-roomid-join"
aliases:
- /join/{roomId}
# With an extra " " to disambiguate from the 3pid invite endpoint
# The extra space makes it sort first for what I'm sure is a good reason.
"/rooms/{roomId}/invite ":
post:
summary: Invite a user to participate in a particular room.
description: |-
*Note that there are two forms of this API, which are documented separately.
This version of the API requires that the inviter knows the Matrix
identifier of the invitee.*
This API invites a user to participate in a particular room.
They do not start participating in the room until they actually join the
room.
Only users currently in a particular room can invite other users to
join that room.
If the user was invited to the room, the home server will append a
``m.room.member`` event to the room.
security:
- accessToken: []
parameters:
- in: path
type: string
name: roomId
description: The room identifier (not alias) to which to invite the user.
required: true
x-example: "!d41d8cd:matrix.org"
- in: body
name: body
required: true
schema:
type: object
example: |-
{
"user_id": "@cheeky_monkey:matrix.org"
}
properties:
user_id:
type: string
description: The fully qualified user ID of the invitee.
required: ["user_id"]
responses:
200:
description: The user has been invited to join the room.
examples:
application/json: |-
{}
schema:
type: object
403:
description: |-
You do not have permission to invite the user to the room. A meaningful ``errcode`` and description error text will be returned. Example reasons for rejections are:
- The invitee has been banned from the room.
- The invitee is already a member of the room.
- The inviter is not currently in the room.
- The inviter's power level is insufficient to invite users to the room.
examples:
application/json: |-
{"errcode": "M_FORBIDDEN", "error": "@cheeky_monkey:matrix.org is banned from the room"}
429:
description: This request was rate-limited.
schema:
"$ref": "definitions/error.yaml"
"/rooms/{roomId}/invite":
post:
summary: Invite a user to participate in a particular room.
description: |-
*Note that there are two forms of this API, which are documented separately.
This version of the API does not require that the inviter know the Matrix
identifier of the invitee, and instead relies on third party identifiers.
The homeserver uses an identity server to perform the mapping from
third party identifier to a Matrix identifier.*
This API invites a user to participate in a particular room.
They do not start participating in the room until they actually join the
room.
Only users currently in a particular room can invite other users to
join that room.
If the identity server did know the Matrix user identifier for the
third party identifier, the home server will append a ``m.room.member``
event to the room.
If the identity server does not know a Matrix user identifier for the
passed third party identifier, the homeserver will issue an invitation
which can be accepted upon providing proof of ownership of the third
party identifier. This is achieved by the identity server generating a
token, which it gives to the inviting homeserver. The homeserver will
add an ``m.room.third_party_invite`` event into the graph for the room,
containing that token.
When the invitee binds the invited third party identifier to a Matrix
user ID, the identity server will give the user a list of pending
invitations, each containing:
- The room ID to which they were invited
- The token given to the homeserver
- A signature of the token, signed with the identity server's private key
- The matrix user ID who invited them to the room
If a token is requested from the identity server, the home server will
append a ``m.room.third_party_invite`` event to the room.
security:
- accessToken: []
parameters:
- in: path
type: string
name: roomId
description: The room identifier (not alias) to which to invite the user.
required: true
x-example: "!d41d8cd:matrix.org"
- in: body
name: body
required: true
schema:
type: object
example: |-
{
"id_server": "matrix.org",
"medium": "email",
"address": "cheeky@monkey.com",
"display_name": "A very cheeky monkey"
}
properties:
id_server:
type: string
description: The hostname+port of the identity server which should be used for third party identifier lookups.
medium:
type: string
# TODO: Link to identity service spec when it eixsts
description: The kind of address being passed in the address field, for example ``email``.
address:
type: string
description: The invitee's third party identifier.
display_name:
type: string
description: A user-friendly string describing who has been invited. It should not contain the address of the invitee, to avoid leaking mappings between third party identities and matrix user IDs.
required: ["id_server", "medium", "address", "display_name"]
responses:
200:
description: The user has been invited to join the room.
examples:
application/json: |-
{}
schema:
type: object
403:
description: |-
You do not have permission to invite the user to the room. A meaningful ``errcode`` and description error text will be returned. Example reasons for rejections are:
- The invitee has been banned from the room.
- The invitee is already a member of the room.
- The inviter is not currently in the room.
- The inviter's power level is insufficient to invite users to the room.
examples:
application/json: |-
{"errcode": "M_FORBIDDEN", "error": "@cheeky_monkey:matrix.org is banned from the room"}
429:
description: This request was rate-limited.
schema:
"$ref": "definitions/error.yaml"

@ -0,0 +1,131 @@
swagger: '2.0'
info:
title: "Matrix Client-Server v1 Rooms 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}/messages":
get:
summary: Get a list of events for this room
description: |-
This API returns a list of message and state events for a room. It uses
pagination query parameters to paginate history in the room.
security:
- accessToken: []
parameters:
- in: path
type: string
name: roomId
description: The room to get events from.
required: true
x-example: "!636q39766251:example.com"
- in: query
type: string
name: from
description: |-
The token to start returning events from. This token can be obtained
from the initial sync API.
required: true
x-example: "s345_678_333"
- in: query
type: string
enum: ["b", "f"]
name: dir
description: |-
The direction to return events from.
required: true
x-example: "b"
- in: query
type: integer
name: limit
description: |-
The maximum number of events to return. Default: 10.
x-example: "3"
responses:
200:
description: A list of messages with a new token to request more.
schema:
type: object
description: A list of messages with a new token to request more.
properties:
start:
type: string
description: |-
The token to start paginating from. If ``dir=b`` this will be
the token supplied in ``from``.
end:
type: string
description: |-
The token the pagination ends at. If ``dir=b`` this token should
be used again to request even earlier events.
chunk:
type: array
description: |-
A list of room events.
items:
type: object
title: RoomEvent
examples:
application/json: |-
{
"start": "t47429-4392820_219380_26003_2265",
"end": "t47409-4357353_219380_26003_2265",
"chunk": [
{
"origin_server_ts": 1444812213737,
"user_id": "@alice:example.com",
"event_id": "$1444812213350496Caaaa:example.com",
"content": {
"body": "hello world",
"msgtype":"m.text"
},
"room_id":"!Xq3620DUiqCaoxq:example.com",
"type":"m.room.message",
"age": 1042
},
{
"origin_server_ts": 1444812194656 ,
"user_id": "@bob:example.com",
"event_id": "$1444812213350496Cbbbb:example.com",
"content": {
"body": "the world is big",
"msgtype":"m.text"
},
"room_id":"!Xq3620DUiqCaoxq:example.com",
"type":"m.room.message",
"age": 20123
},
{
"origin_server_ts": 1444812163990,
"user_id": "@bob:example.com",
"event_id": "$1444812213350496Ccccc:example.com",
"content": {
"name": "New room name"
},
"prev_content": {
"name": "Old room name"
},
"state_key": "",
"room_id":"!Xq3620DUiqCaoxq:example.com",
"type":"m.room.name",
"age": 50789
}
]
}
403:
description: >
You aren't a member of the room.

@ -101,7 +101,7 @@ paths:
The length of time in milliseconds since an action was performed
by this user.
status_msg:
type: string
type: [string, "null"]
description: The state message for this user if one was set.
404:
description: |-
@ -185,7 +185,7 @@ paths:
"last_active_ago": 395,
"presence": "offline",
"user_id": "@alice:matrix.org"
}
},
"type": "m.presence"
},
{
@ -195,7 +195,7 @@ paths:
"last_active_ago": 16874,
"presence": "online",
"user_id": "@marisa:matrix.org"
}
},
"type": "m.presence"
}
]
@ -205,5 +205,4 @@ paths:
type: object
title: PresenceEvent
allOf:
- "$ref": "definitions/event.yaml"
- "$ref": "core-event-schema/event.json"

@ -0,0 +1,192 @@
swagger: '2.0'
info:
title: "Matrix Push Notification API"
version: "1.0.0"
host: localhost:8008
schemes:
- https
- http
basePath: /_matrix/push/v1
consumes:
- application/json
produces:
- application/json
paths:
"/notify":
post:
summary: Notify a push gateway about an event.
description: |-
This endpoint is invoked by HTTP pushers to notify a push gateway about
an event.
*NB: Notifications are sent to the URL configured when the pusher is
created. This means that the HTTP path may be different depending on the
push gateway.*
parameters:
- in: body
name: notification
description: Information about the push notification.
required: true
schema:
type: object
example: |-
{
"notification": {
"id": "$3957tyerfgewrf384",
"room_id": "!slw48wfj34rtnrf:example.com",
"type": "m.room.message",
"sender": "@exampleuser:matrix.org",
"sender_display_name": "Major Tom",
"room_name": "Mission Control",
"room_alias": "#exampleroom:matrix.org",
"prio": "high",
"content": {
"msgtype": "m.text",
"body": "I'm floating in a most peculiar way."
}
},
"counts": {
"unread" : 2,
"missed_calls": 1
},
"devices": [
{
"app_id": "org.matrix.matrixConsole.ios",
"pushkey": "V2h5IG9uIGVhcnRoIGRpZCB5b3UgZGVjb2RlIHRoaXM/",
"pushkey_ts": 12345678,
"data" : {},
"tweaks": {
"sound": "bing"
}
}
]
}
required: ["notification", "counts", "devices"]
properties:
notification:
type: object
description: Information about the push notification
required: ["id", "room_id", "type", "sender"]
properties:
id:
type: string
description: |-
An identifier for this notification that may be used to
detect duplicate notification requests. This is not
necessarily the ID of the event that triggered the
notification.
room_id:
type: string
description: The ID of the room in which this event occurred.
type:
type: string
description: The type of the event as in the event's ``type`` field.
sender:
type: string
description: The sender of the event as in the corresponding event field.
sender_display_name:
type: string
description: |-
The current display name of the sender in the room in which
the event occurred.
room_name:
type: string
description: The name of the room in which the event occurred.
room_alias:
type: string
description: An alias to display for the room in which the event occurred.
user_is_target:
type: boolean
description: |-
This is true if the user receiving the notification is the
subject of a member event (i.e. the ``state_key`` of the
member event is equal to the user's Matrix ID).
prio:
type: string
enum: ["high", "low"]
description: |-
The priority of the notification. If omitted, ``high`` is
assumed. This may be used by push gateways to deliver less
time-sensitive notifications in a way that will preserve
battery power on mobile devices.
content:
type: object
title: EventContent
description: |-
The ``content`` field from the event, if present. If the
event had no content field, this field is omitted.
counts:
type: object
description: |-
This is a dictionary of the current number of unacknowledged
communications for the recipient user. Counts whose value is
zero are omitted.
properties:
unread:
type: integer
description: |-
The number of unread messages a user has across all of the
rooms they are a member of.
missed_calls:
type: integer
description: |-
The number of unacknowledged missed calls a user has
across all rooms of which they are a member.
devices:
type: array
title: Devices
description: |-
This is an array of devices that the notification should be sent to.
items:
type: object
properties:
app_id:
type: string
description: |-
The app_id given when the pusher was created.
pushkey:
type: string
description: The pushkey given when the pusher was created.
pushkey_ts:
type: integer
description: |-
The unix timestamp (in seconds) when the
pushkey was last updated.
data:
type: object
title: PusherData
description: |-
A dictionary of additional pusher-specific data. For
'http' pushers, this is the data dictionary passed in at
pusher creation minus the ``url`` key.
tweaks:
type: object
title: Tweaks
description: |-
A dictionary of customisations made to the way this
notification is to be presented. These are added by push rules.
responses:
200:
description: A list of rejected push keys.
examples:
application/json: |-
{
"rejected": [ "V2h5IG9uIGVhcnRoIGRpZCB5b3UgZGVjb2RlIHRoaXM/" ]
}
schema:
type: object # empty json object
properties:
rejected:
type: array
description: |-
A list of all pushkeys given in the notification request that
are not valid. These could have been rejected by an upstream
gateway because they have expired or have never been valid.
Homeservers must cease sending notification requests for these
pushkeys and remove the associated pushers. It may not
necessarily be the notification in the request that failed:
it could be that a previous notification to the same pushkey
failed.
items:
type: string
description: A pushkey

@ -0,0 +1,144 @@
swagger: '2.0'
info:
title: "Matrix Client-Server v1 Push 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:
"/pushers/set":
post:
summary: Modify a pusher for this user on the homeserver.
description: |-
This endpoint allows the creation, modification and deletion of `pushers`_
for this user ID. The behaviour of this endpoint varies depending on the
values in the JSON body.
security:
- accessToken: []
parameters:
- in: body
name: pusher
description: The pusher information
required: true
schema:
type: object
example: |-
{
"lang": "en",
"kind": "http",
"app_display_name": "Mat Rix",
"device_display_name": "iPhone 9",
"app_id": "com.example.app.ios",
"profile_tag": "4bea66906d0111e59d70feff819cdc9f",
"pushkey": "APA91bHPRgkF3JUikC4ENAHEeMrd41Zxv3hVZjC9KtT8OvPVGJ-hQMRKRrZuJAEcl7B338qju59zJMjw2DELjzEvxwYv7hH5Ynpc1ODQ0aT4U4OFEeco8ohsN5PjL1iC2dNtk2BAokeMCg2ZXKqpc8FXKmhX94kIxQ",
"data": {
"url": "https://push-gateway.location.here"
},
"append": false
}
properties:
pushkey:
type: string
description: |-
This is a unique identifier for this pusher. The value you
should use for this is the routing or destination address
information for the notification, for example, the APNS token
for APNS or the Registration ID for GCM. If your notification
client has no such concept, use any unique identifier.
Max length, 512 bytes.
kind:
type: string
enum: ["http", null]
description: |-
The kind of pusher to configure. ``"http"`` makes a pusher that
sends HTTP pokes. ``null`` deletes the pusher.
profile_tag:
type: string
description: |-
This is a string that determines what set of device rules will
be matched when evaluating push rules for this pusher. It is
an arbitrary string. Multiple devices may use the same
``profile_tag``. It is advised that when an app's data is
copied or restored to a different device, this value remain
the same. Client apps should offer ways to change the
``profile_tag``, optionally copying rules from the old
profile tag. Max length, 32 bytes.
app_id:
type: string
description: |-
This is a reverse-DNS style identifier for the application.
It is recommended that this end with the platform, such that
different platform versions get different app identifiers.
Max length, 64 chars.
app_display_name:
type: string
description: |-
A string that will allow the user to identify what application
owns this pusher.
device_display_name:
type: string
description: |-
A string that will allow the user to identify what device owns
this pusher.
lang:
type: string
description: |-
The preferred language for receiving notifications (e.g. 'en'
or 'en-US')
data:
type: object
description: |-
A dictionary of information for the pusher implementation
itself. If ``kind`` is ``http``, this should contain ``url``
which is the URL to use to send notifications to.
properties:
url:
type: string
description: |-
Required if ``kind`` is ``http``. The URL to use to send
notifications to.
append:
type: boolean
description: |-
If true, the homeserver should add another pusher with the
given pushkey and App ID in addition to any others with
different user IDs. Otherwise, the Home Server must remove any
other pushers with the same App ID and pushkey for different
users. The default is ``false``.
required: ['profile_tag', 'kind', 'app_id', 'app_display_name',
'device_display_name', 'pushkey', 'lang', 'data']
responses:
200:
description: The pusher was set.
examples:
application/json: |-
{}
schema:
type: object # empty json object
400:
description: One or more of the pusher values were invalid.
examples:
application/json: |-
{
"error": "Missing parameters: lang, data",
"errcode": "M_MISSING_PARAM"
}
schema:
type: object
429:
description: This request was rate-limited.
schema:
"$ref": "definitions/error.yaml"

@ -0,0 +1,488 @@
swagger: '2.0'
info:
title: "Matrix Client-Server v1 Push Rules 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:
"/pushrules/":
get:
summary: Retrieve all push rulesets.
description: |-
Retrieve all push rulesets for this user. Clients can "drill-down" on
the rulesets by suffixing a ``scope`` to this path e.g.
``/pushrules/global/``. This will return a subset of this data under the
specified key e.g. the ``global`` key.
security:
- accessToken: []
responses:
200:
description: All the push rulesets for this user.
schema:
type: object
required: ["device", "global"]
properties:
device:
type: object
title: Devices
description: A dictionary of profile tags to rulesets.
additionalProperties:
x-pattern: "$PROFILE_TAG"
type: object
description: The ruleset for this profile tag.
title: Ruleset
allOf: [
"$ref": "definitions/push_ruleset.json"
]
global:
type: object
description: The global ruleset.
title: Ruleset
allOf: [
"$ref": "definitions/push_ruleset.json"
]
examples:
application/json: |-
{
"device": {},
"global": {
"content": [
{
"actions": [
"notify",
{
"set_tweak": "sound",
"value": "default"
},
{
"set_tweak": "highlight"
}
],
"default": true,
"enabled": true,
"pattern": "alice",
"rule_id": ".m.rule.contains_user_name"
}
],
"override": [
{
"actions": [
"dont_notify"
],
"conditions": [],
"default": true,
"enabled": false,
"rule_id": ".m.rule.master"
},
{
"actions": [
"dont_notify"
],
"conditions": [
{
"key": "content.msgtype",
"kind": "event_match",
"pattern": "m.notice"
}
],
"default": true,
"enabled": true,
"rule_id": ".m.rule.suppress_notices"
}
],
"room": [],
"sender": [],
"underride": [
{
"actions": [
"notify",
{
"set_tweak": "sound",
"value": "ring"
},
{
"set_tweak": "highlight",
"value": false
}
],
"conditions": [
{
"key": "type",
"kind": "event_match",
"pattern": "m.call.invite"
}
],
"default": true,
"enabled": true,
"rule_id": ".m.rule.call"
},
{
"actions": [
"notify",
{
"set_tweak": "sound",
"value": "default"
},
{
"set_tweak": "highlight"
}
],
"conditions": [
{
"kind": "contains_display_name"
}
],
"default": true,
"enabled": true,
"rule_id": ".m.rule.contains_display_name"
},
{
"actions": [
"notify",
{
"set_tweak": "sound",
"value": "default"
},
{
"set_tweak": "highlight",
"value": false
}
],
"conditions": [
{
"is": "2",
"kind": "room_member_count"
}
],
"default": true,
"enabled": true,
"rule_id": ".m.rule.room_one_to_one"
},
{
"actions": [
"notify",
{
"set_tweak": "sound",
"value": "default"
},
{
"set_tweak": "highlight",
"value": false
}
],
"conditions": [
{
"key": "type",
"kind": "event_match",
"pattern": "m.room.member"
},
{
"key": "content.membership",
"kind": "event_match",
"pattern": "invite"
},
{
"key": "state_key",
"kind": "event_match",
"pattern": "@alice:example.com"
}
],
"default": true,
"enabled": true,
"rule_id": ".m.rule.invite_for_me"
},
{
"actions": [
"notify",
{
"set_tweak": "highlight",
"value": false
}
],
"conditions": [
{
"key": "type",
"kind": "event_match",
"pattern": "m.room.member"
}
],
"default": true,
"enabled": true,
"rule_id": ".m.rule.member_event"
},
{
"actions": [
"notify",
{
"set_tweak": "highlight",
"value": false
}
],
"conditions": [
{
"key": "type",
"kind": "event_match",
"pattern": "m.room.message"
}
],
"default": true,
"enabled": true,
"rule_id": ".m.rule.message"
}
]
}
}
"/pushrules/{scope}/{kind}/{ruleId}":
get:
summary: Retrieve a push rule.
description: |-
Retrieve a single specified push rule.
security:
- accessToken: []
parameters:
- in: path
type: string
name: scope
required: true
x-example: "global"
description: |-
Either ``global`` or ``device/<profile_tag>`` to specify global
rules or device rules for the given ``profile_tag``.
- in: path
type: string
name: kind
required: true
x-example: room
enum: ["override", "underride", "sender", "room", "content"]
description: |
The kind of rule
- in: path
type: string
name: ruleId
required: true
x-example: "#spam:example.com"
description: |
The identifier for the rule.
responses:
200:
description: |-
The specific push rule. This will also include keys specific to the
rule itself such as the rule's ``actions`` and ``conditions`` if set.
examples:
application/json: |-
{
"actions": [
"dont_notify"
],
"rule_id": "#spam:matrix.org",
"enabled": true
}
schema:
type: object
description: The push rule.
title: PushRule
allOf: [
"$ref": "definitions/push_rule.json"
]
delete:
summary: Delete a push rule.
description: |-
This endpoint removes the push rule defined in the path.
security:
- accessToken: []
parameters:
- in: path
type: string
name: scope
required: true
x-example: "global"
description: |-
Either ``global`` or ``device/<profile_tag>`` to specify global
rules or device rules for the given ``profile_tag``.
- in: path
type: string
name: kind
required: true
x-example: room
enum: ["override", "underride", "sender", "room", "content"]
description: |
The kind of rule
- in: path
type: string
name: ruleId
required: true
x-example: "#spam:example.com"
description: |
The identifier for the rule.
responses:
200:
description: The push rule was deleted.
examples:
application/json: |-
{}
schema:
type: object # empty json object
put:
summary: Add or change a push rule.
description: |-
This endpoint allows the creation, modification and deletion of pushers
for this user ID. The behaviour of this endpoint varies depending on the
values in the JSON body.
security:
- accessToken: []
parameters:
- in: path
type: string
name: scope
required: true
x-example: "global"
description: |-
Either ``global`` or ``device/<profile_tag>`` to specify global
rules or device rules for the given ``profile_tag``.
- in: path
type: string
name: kind
required: true
x-example: room
enum: ["override", "underride", "sender", "room", "content"]
description: |
The kind of rule
- in: path
type: string
name: ruleId
required: true
x-example: "#spam:example.com"
description: |
The identifier for the rule.
- in: query
type: string
name: before
required: false
x-example: someRuleId
description: |-
Use 'before' with a ``rule_id`` as its value to make the new rule the
next-most important rule with respect to the given rule.
- in: query
type: string
name: after
required: false
x-example: anotherRuleId
description: |-
This makes the new rule the next-less important rule relative to the
given rule.
- in: body
name: pushrule
description: |-
The push rule data. Additional top-level keys may be present depending
on the parameters for the rule ``kind``.
required: true
schema:
type: object
example: |-
{
"pattern": "cake*lie",
"actions": ["notify"]
}
properties:
actions:
type: array
description: |-
The action(s) to perform when the conditions for this rule are met.
items:
type: string
enum: ["notify", "dont_notify", "coalesce", "set_tweak"]
# TODO: type: object e.g. {"set_sound":"beeroclock.wav"} :/
conditions:
type: array
description: |-
The conditions that must hold true for an event in order for a
rule to be applied to an event. A rule with no conditions
always matches.
items:
type: object
title: conditions
allOf: [ "$ref": "definitions/push_condition.json" ]
required: ["actions"]
responses:
200:
description: The pusher was set.
examples:
application/json: |-
{}
schema:
type: object # empty json object
400:
description: There was a problem configuring this push rule.
examples:
application/json: |-
{
"error": "before/after rule not found: someRuleId",
"errcode": "M_UNKNOWN"
}
schema:
type: object
429:
description: This request was rate-limited.
schema:
"$ref": "definitions/error.yaml"
"/pushrules/{scope}/{kind}/{ruleId}/enabled":
put:
summary: "Enable or disable a push rule."
description: |-
This endpoint allows clients to enable or disable the specified push rule.
security:
- accessToken: []
parameters:
- in: path
type: string
name: scope
required: true
x-example: "global"
description: |-
Either ``global`` or ``device/<profile_tag>`` to specify global
rules or device rules for the given ``profile_tag``.
- in: path
type: string
name: kind
required: true
x-example: room
enum: ["override", "underride", "sender", "room", "content"]
description: |
The kind of rule
- in: path
type: string
name: ruleId
required: true
x-example: "#spam:example.com"
description: |
The identifier for the rule.
- in: body
name: <body>
description: |
Whether the push rule is enabled or not.
required: true
schema:
type: boolean
example: |-
true
responses:
200:
description: The push rule was enabled or disabled.
examples:
application/json: |-
{}
schema:
type: object # empty json object

@ -0,0 +1,125 @@
swagger: '2.0'
info:
title: "Matrix Client-Server v1 message event send 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}/send/{eventType}/{txnId}":
put:
summary: Send a message event to the given room.
description: |-
This endpoint is used to send a message event to a room. Message events
allow access to historical events and pagination, making them suited
for "once-off" activity in a room.
The body of the request should be the content object of the event; the
fields in this object will vary depending on the type of event. See
`Room Events`_ for the m. event specification.
security:
- accessToken: []
parameters:
- in: path
type: string
name: roomId
description: The room to send the event to.
required: true
x-example: "!636q39766251:example.com"
- in: path
type: string
name: eventType
description: The type of event to send.
required: true
x-example: "m.room.message"
- in: path
name: txnId
type: string
description: |-
The transaction ID for this event. Clients should generate a
unique ID; it will be used by the server to ensure idempotency of requests.
required: true
x-example: "35"
- in: body
name: body
schema:
type: object
example: |-
{
"msgtype": "m.text",
"body": "hello"
}
responses:
200:
description: "An ID for the sent event."
examples:
application/json: |-
{
"event_id": "YUwRidLecu"
}
schema:
type: object
properties:
event_id:
type: string
description: |-
A unique identifier for the event.
"/rooms/{roomId}/send/{eventType}":
post:
summary: Send a message event to the given room.
description: |-
This endpoint can be used to send a message event to a room; however
the lack of a transaction ID means that it is possible to cause message
duplication if events are resent on error, so it is preferable to use
`PUT /_matrix/client/api/v1/rooms/{roomId}/send/{eventType}/{txnId}`_.
security:
- accessToken: []
parameters:
- in: path
type: string
name: roomId
description: The room to send the event to.
required: true
x-example: "!636q39766251:example.com"
- in: path
type: string
name: eventType
description: The type of event to send.
required: true
x-example: "m.room.message"
- in: body
name: body
schema:
type: object
example: |-
{
"msgtype": "m.text",
"body": "hello"
}
responses:
200:
description: "An ID for the sent event."
examples:
application/json: |-
{
"event_id": "YUwRidLecu"
}
schema:
type: object
properties:
event_id:
type: string
description: |-
A unique identifier for the event.

@ -0,0 +1,80 @@
swagger: '2.0'
info:
title: "Matrix Client-Server v1 state event send 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}/state/{eventType}/{stateKey}":
put:
summary: Send a message event to the given room.
description: |
State events can be sent using this endpoint. These events will be
overwritten if ``<room id>``, ``<event type>`` and ``<state key>`` all
match. If the state event has an empty ``state_key``, it can be
omitted from the path.
Requests to this endpoint **cannot use transaction IDs**
like other ``PUT`` paths because they cannot be differentiated from the
``state_key``. Furthermore, ``POST`` is unsupported on state paths.
The body of the request should be the content object of the event; the
fields in this object will vary depending on the type of event. See
`Room Events`_ for the ``m.`` event specification.
security:
- accessToken: []
parameters:
- in: path
type: string
name: roomId
description: The room to set the state in
required: true
x-example: "!636q39766251:example.com"
- in: path
type: string
name: eventType
description: The type of event to send.
required: true
x-example: "m.room.name"
- in: path
type: string
name: stateKey
description: The state_key for the state to send. Defaults to the empty string.
required: true
x-example: ""
- in: body
name: body
schema:
type: object
example: |-
{
"name": "New name for the room"
}
responses:
200:
description: "An ID for the sent event."
examples:
application/json: |-
{
"event_id": "YUwRidLecu"
}
schema:
type: object
properties:
event_id:
type: string
description: |-
A unique identifier for the event.

@ -0,0 +1,448 @@
swagger: '2.0'
info:
title: "Matrix Client-Server v1 Rooms 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}/state/{eventType}/{stateKey}":
get:
summary: Get the state identified by the type and key.
description: |-
Looks up the contents of a state event in a room. If the user is
joined to the room then the state is taken from the current
state of the room. If the user has left the room then the state is
taken from the state of the room when they left.
security:
- accessToken: []
parameters:
- in: path
type: string
name: roomId
description: The room to look up the state in.
required: true
x-example: "!636q39766251:example.com"
- in: path
type: string
name: eventType
description: The type of state to look up.
required: true
x-example: "m.room.name"
- in: path
type: string
name: stateKey
description: The key of the state to look up. Defaults to the empty string.
required: true
x-example: ""
responses:
200:
description: The content of the state event.
examples:
application/json: |-
{"name": "Example room name"}
schema:
type: object
404:
description: The room has no state with the given type or key.
403:
description: >
You aren't a member of the room and weren't previously a
member of the room.
"/rooms/{roomId}/state":
get:
summary: Get all state events in the current state of a room.
description: |-
Get the state events for the current state of a room.
security:
- accessToken: []
parameters:
- in: path
type: string
name: roomId
description: The room to look up the state for.
required: true
x-example: "!636q39766251:example.com"
responses:
200:
description: The current state of the room
examples:
application/json: |-
[
{
"age": 7148266897,
"content": {
"join_rule": "public"
},
"event_id": "$14259997323TLwtb:example.com",
"origin_server_ts": 1425999732392,
"room_id": "!636q39766251:example.com",
"state_key": "",
"type": "m.room.join_rules",
"user_id": "@alice:example.com"
},
{
"age": 6547561012,
"content": {
"avatar_url": "mxc://example.com/fzysBrHpPEeTGANCVLXWXNMI#auto",
"displayname": null,
"membership": "join"
},
"event_id": "$1426600438280zExKY:example.com",
"membership": "join",
"origin_server_ts": 1426600438277,
"room_id": "!636q39766251:example.com",
"state_key": "@alice:example.com",
"type": "m.room.member",
"user_id": "@alice:example.com"
},
{
"age": 7148267200,
"content": {
"creator": "@alice:example.com"
},
"event_id": "$14259997320KhbwJ:example.com",
"origin_server_ts": 1425999732089,
"room_id": "!636q39766251:example.com",
"state_key": "",
"type": "m.room.create",
"user_id": "@alice:example.com"
},
{
"age": 1622568720,
"content": {
"avatar_url": "mxc://example.com/GCmhgzMPRjqgpODLsNQzVuHZ#auto",
"displayname": "Bob",
"membership": "join"
},
"event_id": "$1431525430134MxlLX:example.com",
"origin_server_ts": 1431525430569,
"replaces_state": "$142652023736BSXcM:example.com",
"room_id": "!636q39766251:example.com",
"state_key": "@bob:example.com",
"type": "m.room.member",
"user_id": "@bob:example.com"
},
{
"age": 7148267004,
"content": {
"ban": 50,
"events": {
"m.room.name": 100,
"m.room.power_levels": 100
},
"events_default": 0,
"kick": 50,
"redact": 50,
"state_default": 50,
"users": {
"@alice:example.com": 100
},
"users_default": 0
},
"event_id": "$14259997322mqfaq:example.com",
"origin_server_ts": 1425999732285,
"room_id": "!636q39766251:example.com",
"state_key": "",
"type": "m.room.power_levels",
"user_id": "@alice:example.com"
}
]
schema:
type: array
title: RoomState
description: |-
If the user is a member of the room this will be the
current state of the room as a list of events. If the user
has left the room then this will be the state of the room
when they left as a list of events.
items:
title: StateEvent
type: object
allOf:
- "$ref": "core-event-schema/state_event.json"
403:
description: >
You aren't a member of the room and weren't previously a
member of the room.
"/rooms/{roomId}/initialSync":
get:
summary: Snapshot the current state of a room and its most recent messages.
description: |-
Get a copy of the current state and the most recent messages in a room.
security:
- accessToken: []
parameters:
- in: path
type: string
name: roomId
description: The room to get the data.
required: true
x-example: "!636q39766251:example.com"
responses:
200:
description: The current state of the room
examples:
application/json: |-
{
"membership": "join",
"messages": {
"chunk": [
{
"age": 343513403,
"content": {
"body": "foo",
"msgtype": "m.text"
},
"event_id": "$14328044851tzTJS:example.com",
"origin_server_ts": 1432804485886,
"room_id": "!636q39766251:example.com",
"type": "m.room.message",
"user_id": "@alice:example.com"
},
{
"age": 343511809,
"content": {
"body": "bar",
"msgtype": "m.text"
},
"event_id": "$14328044872spjFg:example.com",
"origin_server_ts": 1432804487480,
"room_id": "!636q39766251:example.com",
"type": "m.room.message",
"user_id": "@bob:example.com"
}
],
"end": "s3456_9_0",
"start": "t44-3453_9_0"
},
"room_id": "!636q39766251:example.com",
"state": [
{
"age": 7148266897,
"content": {
"join_rule": "public"
},
"event_id": "$14259997323TLwtb:example.com",
"origin_server_ts": 1425999732392,
"room_id": "!636q39766251:example.com",
"state_key": "",
"type": "m.room.join_rules",
"user_id": "@alice:example.com"
},
{
"age": 6547561012,
"content": {
"avatar_url": "mxc://example.com/fzysBrHpPEeTGANCVLXWXNMI#auto",
"displayname": null,
"membership": "join"
},
"event_id": "$1426600438280zExKY:example.com",
"membership": "join",
"origin_server_ts": 1426600438277,
"room_id": "!636q39766251:example.com",
"state_key": "@alice:example.com",
"type": "m.room.member",
"user_id": "@alice:example.com"
},
{
"age": 7148267200,
"content": {
"creator": "@alice:example.com"
},
"event_id": "$14259997320KhbwJ:example.com",
"origin_server_ts": 1425999732089,
"room_id": "!636q39766251:example.com",
"state_key": "",
"type": "m.room.create",
"user_id": "@alice:example.com"
},
{
"age": 1622568720,
"content": {
"avatar_url": "mxc://example.com/GCmhgzMPRjqgpODLsNQzVuHZ#auto",
"displayname": "Bob",
"membership": "join"
},
"event_id": "$1431525430134MxlLX:example.com",
"origin_server_ts": 1431525430569,
"replaces_state": "$142652023736BSXcM:example.com",
"room_id": "!636q39766251:example.com",
"state_key": "@bob:example.com",
"type": "m.room.member",
"user_id": "@bob:example.com"
},
{
"age": 7148267004,
"content": {
"ban": 50,
"events": {
"m.room.name": 100,
"m.room.power_levels": 100
},
"events_default": 0,
"kick": 50,
"redact": 50,
"state_default": 50,
"users": {
"@alice:example.com": 100
},
"users_default": 0
},
"event_id": "$14259997322mqfaq:example.com",
"origin_server_ts": 1425999732285,
"room_id": "!636q39766251:example.com",
"state_key": "",
"type": "m.room.power_levels",
"user_id": "@alice:example.com"
}
],
"visibility": "private"
}
schema:
title: RoomInfo
type: object
properties:
room_id:
type: string
description: "The ID of this room."
membership:
type: string
description: "The user's membership state in this room."
enum: ["invite", "join", "leave", "ban"]
messages:
type: object
title: PaginationChunk
description: "The pagination chunk for this room."
properties:
start:
type: string
description: |-
A token which correlates to the first value in ``chunk``.
Used for pagination.
end:
type: string
description: |-
A token which correlates to the last value in ``chunk``.
Used for pagination.
chunk:
type: array
description: |-
If the user is a member of the room this will be a
list of the most recent messages for this room. If
the user has left the room this will be the
messages that preceeded them leaving. This array
will consist of at most ``limit`` elements.
items:
type: object
title: RoomEvent
allOf:
- "$ref": "core-event-schema/room_event.json"
required: ["start", "end", "chunk"]
state:
type: array
description: |-
If the user is a member of the room this will be the
current state of the room as a list of events. If the
user has left the room this will be the state of the
room when they left it.
items:
title: StateEvent
type: object
allOf:
- "$ref": "core-event-schema/state_event.json"
visibility:
type: string
enum: ["private", "public"]
description: |-
Whether this room is visible to the ``/publicRooms`` API
or not."
required: ["room_id", "membership"]
403:
description: >
You aren't a member of the room and weren't previously a
member of the room.
"/rooms/{roomId}/members":
get:
summary: Get the m.room.member events for the room.
description:
Get the list of members for this room.
parameters:
- in: path
type: string
name: roomId
description: The room to get the member events for.
required: true
x-example: "!636q39766251:example.com"
responses:
200:
description: |-
A list of members of the room. If you are joined to the room then
this will be the current members of the room. If you have left te
room then this will be the members of the room when you left.
examples:
application/json: |-
{
"chunk": [
{
"age": 6547561012,
"content": {
"avatar_url": "mxc://example.com/fzysBrHpPEeTGANCVLXWXNMI#auto",
"displayname": null,
"membership": "join"
},
"event_id": "$1426600438280zExKY:example.com",
"membership": "join",
"origin_server_ts": 1426600438277,
"room_id": "!636q39766251:example.com",
"state_key": "@alice:example.com",
"type": "m.room.member",
"user_id": "@alice:example.com"
},
{
"age": 1622568720,
"content": {
"avatar_url": "mxc://example.com/GCmhgzMPRjqgpODLsNQzVuHZ#auto",
"displayname": "Bob",
"membership": "join"
},
"event_id": "$1431525430134MxlLX:example.com",
"origin_server_ts": 1431525430569,
"replaces_state": "$142652023736BSXcM:example.com",
"room_id": "!636q39766251:example.com",
"state_key": "@bob:example.com",
"type": "m.room.member",
"user_id": "@bob:example.com"
}
]
}
schema:
type: object
properties:
chunk:
type: array
items:
title: MemberEvent
type: object
allOf:
- "$ref": "v1-event-schema/m.room.member"
403:
description: >
You aren't a member of the room and weren't previously a
member of the room.

@ -0,0 +1,146 @@
swagger: '2.0'
info:
title: "Matrix Client-Server v1 Search 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:
"/search":
post:
summary: Search server side for things.
description: |-
Performs a full text search across different categories.
security:
- accessToken: []
parameters:
- in: body
name: body
schema:
type: object
example: |-
{
"search_categories": {
"room_events": {
"keys": [
"content.body"
],
"search_term": "martians and men"
}
}
}
properties:
search_categories:
type: object
title: "Categories"
description: Describes which categories to search in and
their criteria.
properties:
room_events:
type: object
title: "Room Events"
description: Mapping of category name to search criteria.
properties:
search_term:
type: string
description: The string to search events for
keys:
type: array
items:
type: string
enum: ["content.body", "content.name", "content.topic"]
description: The keys to search. Defaults to all.
filter:
type: object
title: Filter
description: |-
The filter to apply to search results.
This has the same format as v2 filter API.
required: ["search_term"]
required: ["search_categories"]
responses:
200:
description: Results of the search.
schema:
type: object
title: Results
required: ["search_categories"]
properties:
search_categories:
type: object
title: Categories
description: Describes which categories to search in and
their criteria.
properties:
room_events:
type: object
title: Room Event Results
description: Mapping of category name to search criteria.
properties:
count:
type: number
description: Total number of results found
results:
type: object
title: Results
description: Mapping of event_id to result.
additionalProperties:
type: object
title: Result
description: The result object.
properties:
rank:
type: number
description: A number that describes how closely
this result matches the search. Higher is
closer.
result:
type: object
title: Event
description: The event that matched.
allOf:
- "$ref": "core-event-schema/room_event.json"
examples:
application/json: |-
{
"search_categories": {
"room_events": {
"count": 24,
"results": {
"$144429830826TWwbB:localhost": {
"rank": 0.00424866,
"result": {
"age": 526228296,
"content": {
"body": "Test content",
"msgtype": "m.text"
},
"event_id": "$144429830826TWwbB:localhost",
"origin_server_ts": 1444298308034,
"room_id": "!qPewotXpIctQySfjSy:localhost",
"type": "m.room.message",
"user_id": "@test:localhost"
}
}
}
}
}
}
400:
description: Part of the request was invalid.
429:
description: This request was rate-limited.
schema:
"$ref": "definitions/error.yaml"

@ -30,7 +30,9 @@ paths:
- in: query
type: string
name: from
description: The token to stream from.
description: |-
The token to stream from. This token is either from a previous
request to this API or from the initial sync API.
required: false
x-example: "s3456_9_0"
- in: query
@ -68,21 +70,21 @@ paths:
start:
type: string
description: |-
A token which correlates to the first value in ``chunk``. Used
for pagination.
A token which correlates to the first value in ``chunk``. This
is usually the same token supplied to ``from=``.
end:
type: string
description: |-
A token which correlates to the last value in ``chunk``. Used
for pagination.
A token which correlates to the last value in ``chunk``. This
token should be used in the next request to ``/events``.
chunk:
type: array
description: "An array of events."
items:
type: object
title: RoomEvent
title: Event
allOf:
- "$ref": "definitions/room_event.yaml"
- "$ref": "core-event-schema/room_event.json"
400:
description: "Bad pagination ``from`` parameter."
"/initialSync":
@ -100,6 +102,16 @@ paths:
description: The maximum number of messages to return for each room.
required: false
x-example: "2"
- in: query
type: boolean
name: archived
description: |-
Whether to include rooms that the user has left. If ``false`` then
only rooms that the user has been invited to or has joined are
included. If set to ``true`` then rooms that the user has left are
included as well. By default this is ``false``.
required: false
x-example: "true"
responses:
200:
description: The user's current state.
@ -253,7 +265,7 @@ paths:
type: object
title: Event
allOf:
- "$ref": "definitions/event.yaml"
- "$ref": "core-event-schema/event.json"
rooms:
type: array
items:
@ -267,6 +279,12 @@ paths:
type: string
description: "The user's membership state in this room."
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:
type: object
title: PaginationChunk
@ -285,24 +303,29 @@ paths:
chunk:
type: array
description: |-
A list of the most recent messages for this room. This
array will consist of at most ``limit`` elements.
If the user is a member of the room this will be a
list of the most recent messages for this room. If
the user has left the room this will be the
messages that preceeded them leaving. This array
will consist of at most ``limit`` elements.
items:
type: object
title: RoomEvent
allOf:
- "$ref": "definitions/room_event.yaml"
- "$ref": "core-event-schema/room_event.json"
required: ["start", "end", "chunk"]
state:
type: array
description: |-
A list of state events representing the current state
of the room.
If the user is a member of the room this will be the
current state of the room as a list of events. If the
user has left the room this will be the state of the
room when they left it.
items:
title: StateEvent
type: object
allOf:
- "$ref": "definitions/state_event.yaml"
- "$ref": "core-event-schema/state_event.json"
visibility:
type: string
enum: ["private", "public"]
@ -338,13 +361,13 @@ paths:
"body": "Hello world!",
"msgtype": "m.text"
},
"room_id:" "!wfgy43Sg4a:matrix.org",
"room_id:": "!wfgy43Sg4a:matrix.org",
"user_id": "@bob:matrix.org",
"event_id": "$asfDuShaf7Gafaw:matrix.org",
"type": "m.room.message"
}
schema:
allOf:
- "$ref": "definitions/event.yaml"
- "$ref": "core-event-schema/event.json"
404:
description: The event was not found or you do not have permission to read this event.
description: The event was not found or you do not have permission to read this event.

@ -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"

@ -0,0 +1 @@
../../../event-schemas/schema/v1

@ -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"

@ -0,0 +1 @@
../../../event-schemas/schema/v1/core-event-schema

@ -0,0 +1,10 @@
type: object
description: A Matrix-level Error
properties:
errcode:
type: string
description: An error code.
error:
type: string
description: A human-readable error message.
required: ["errcode"]

@ -0,0 +1,12 @@
{
"type": "object",
"properties": {
"events": {
"type": "array",
"description": "List of events",
"items": {
"type": "object"
}
}
}
}

@ -0,0 +1,42 @@
{
"type": "object",
"properties": {
"limit": {
"type": "integer",
"description":
"The maximum number of events to return."
},
"types": {
"type": "array",
"description":
"A list of event types to include. If this list is absent then all event types are included. A '*' can be used as a wildcard to match any sequence of characters.",
"items": {
"type": "string"
}
},
"not_types": {
"type": "array",
"description":
"A list of event types to exclude. If this list is absent then no event types are excluded. A matching type will be excluded even if it is listed in the 'types' filter. A '*' can be used as a wildcard to match any sequence of characters.",
"items": {
"type": "string"
}
},
"senders": {
"type": "array",
"description":
"A list of senders IDs to include. If this list is absent then all senders are included. A '*' can be used as a wildcard to match any sequence of characters.",
"items": {
"type": "string"
}
},
"not_senders": {
"type": "array",
"description":
"A list of sender IDs to exclude. If this list is absent then no senders are excluded. A matching sender will be excluded even if it is listed in the 'senders' filter. A '*' can be used as a wildcard to match any sequence of characters.",
"items": {
"type": "string"
}
}
}
}

@ -0,0 +1,12 @@
{
"type": "object",
"properties": {
"events": {
"type": "array",
"description": "List of event ids",
"items": {
"type": "string"
}
}
}
}

@ -0,0 +1,22 @@
{
"type": "object",
"allOf": [{"$ref": "definitions/event_filter.json"}],
"properties": {
"rooms": {
"type": "array",
"description":
"A list of room IDs to include. If this list is absent then all rooms are included. A '*' can be used as a wildcard to match any sequence of characters.",
"items": {
"type": "string"
}
},
"not_rooms": {
"type": "array",
"description":
"A list of room IDs to exclude. If this list is absent then no rooms are excluded. A matching room will be excluded even if it is listed in the 'rooms' filter. A '*' can be used as a wildcard to match any sequence of characters.",
"items": {
"type": "string"
}
}
}
}

@ -0,0 +1,44 @@
{
"type": "object",
"properties": {
"room": {
"type": "object",
"properties": {
"state": {
"description":
"The state events to include for rooms.",
"allOf": [{"$ref": "definitions/room_event_filter.json"}]
},
"timeline": {
"description":
"The message and state update events to include for rooms.",
"allOf": [{"$ref": "definitions/room_event_filter.json"}]
},
"ephemeral": {
"description":
"The events that aren't recorded in the room history, e.g. typing and receipts, to include for rooms.",
"allOf": [{"$ref": "definitions/room_event_filter.json"}]
}
}
},
"presence": {
"description":
"The presence updates to include.",
"allOf": [{"$ref": "definitions/event_filter.json"}]
},
"event_format": {
"description":
"The format to use for events. 'client' will return the events in a format suitable for clients. 'federation' will return the raw event as receieved over federation. The default is 'client'.",
"type": "string",
"enum": ["client", "federation"]
},
"event_fields": {
"type": "array",
"description":
"List of event fields to include. If this list is absent then all fields are included. The entries may include '.' charaters to indicate sub-fields. So ['content.body'] will include the 'body' field of the 'content' object. A literal '.' character in a field name may be escaped using a '\\'. A server may include more fields than were requested.",
"items": {
"type": "string"
}
}
}
}

@ -0,0 +1,14 @@
{
"type": "object",
"allOf": [{"$ref":"definitions/room_event_batch.json"}],
"properties": {
"limited": {
"type": "boolean",
"description": "Whether there are more events on the server"
},
"prev_batch": {
"type": "string",
"description": "If the batch was limited then this is a token that can be supplied to the server to retrieve more events"
}
}
}

@ -0,0 +1,139 @@
swagger: '2.0'
info:
title: "Matrix Client-Server v2 filter API"
version: "1.0.0"
host: localhost:8008
schemes:
- https
basePath: /_matrix/client/v2_alpha
consumes:
- application/json
produces:
- application/json
securityDefinitions:
accessToken:
type: apiKey
description: The user_id or application service access_token
name: access_token
in: query
paths:
"/user/{userId}/filter":
post:
summary: Upload a new filter.
description: |-
Uploads a new filter definition to the homeserver.
Returns a filter ID that may be used in /sync requests to
retrict which events are returned to the client.
security:
- accessToken: []
parameters:
- in: path
type: string
name: userId
required: true
description:
The id of the user uploading the filter. The access token must be
authorized to make requests for this user id.
x-example: "@alice:example.com"
- in: body
name: filter
required: true
description: The filter to upload.
schema:
type: object
allOf:
- $ref: "definitions/sync_filter.json"
example: |-
{
"room": {
"state": {
"types": ["m.room.*"],
"not_rooms": ["!726s6s6q:example.com"]
},
"timeline": {
"limit": 10,
"types": ["m.room.message"],
"not_rooms": ["!726s6s6q:example.com"],
"not_senders": ["@spam:example.com"]
},
"ephemeral": {
"types": ["m.receipt", "m.typing"],
"not_rooms": ["!726s6s6q:example.com"],
"not_senders": ["@spam:example.com"]
}
},
"presence": {
"types": ["m.presence"],
"not_senders": ["@alice:example.com"]
},
"event_format": "client",
"event_fields": ["type", "content", "sender"]
}
responses:
200:
description: The filter was created.
examples:
application/json: |-
{
"filter_id": "66696p746572"
}
schema:
type: object
properties:
filter_id:
type: string
description: |-
The ID of the filter that was created.
"/user/{userId}/filter/{filterId}":
get:
summary: Download a filter
parameters:
- in: path
name: userId
type: string
description: |-
The user ID to download a filter for.
x-example: "@alice:example.com"
required: true
- in: path
name: filterId
type: string
description: |-
The filter ID to download.
x-example: "66696p746572"
required: true
responses:
200:
description: |-
"The filter defintion"
examples:
application/json: |-
{
"room": {
"state": {
"types": ["m.room.*"],
"not_rooms": ["!726s6s6q:example.com"]
},
"timeline": {
"limit": 10,
"types": ["m.room.message"],
"not_rooms": ["!726s6s6q:example.com"],
"not_senders": ["@spam:example.com"]
},
"ephemeral": {
"types": ["m.receipt", "m.typing"],
"not_rooms": ["!726s6s6q:example.com"],
"not_senders": ["@spam:example.com"]
}
},
"presence": {
"types": ["m.presence"],
"not_senders": ["@alice:example.com"]
},
"event_format": "client",
"event_fields": ["type", "content", "sender"]
}
schema:
type: object
allOf:
- $ref: "definitions/sync_filter.json"

@ -0,0 +1,69 @@
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
name: receipt
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"

@ -0,0 +1,106 @@
swagger: '2.0'
info:
title: "Matrix Client-Server v2 Registration API"
version: "1.0.0"
host: localhost:8008
schemes:
- https
- http
basePath: /_matrix/client/api/v2_alpha
consumes:
- application/json
produces:
- application/json
paths:
"/register":
post:
summary: Register for an account on this homeserver.
description: |-
Register for an account on this homeserver.
parameters:
- in: body
name: body
schema:
type: object
example: |-
{
"username": "cheeky_monkey",
"password": "ilovebananas",
"bind_email": false
}
properties:
bind_email:
type: boolean
description: |-
If true, the server binds the email used for authentication to
the Matrix ID with the ID Server.
username:
type: string
description: |-
The local part of the desired Matrix ID. If omitted,
the homeserver MUST generate a Matrix ID local part.
password:
type: string
description: The desired password for the account.
required: ["password"]
responses:
200:
description: The account has been registered.
examples:
application/json: |-
{
"user_id": "@cheeky_monkey:matrix.org",
"access_token": "abc123",
"home_server": "matrix.org",
"refresh_token": "def456"
}
schema:
type: object
properties:
user_id:
type: string
description: The fully-qualified Matrix ID that has been registered.
access_token:
type: string
description: |-
An access token for the account.
This access token can then be used to authorize other requests.
The access token may expire at some point, and if so, it SHOULD come with a ``refresh_token``.
There is no specific error message to indicate that a request has failed because
an access token has expired; instead, if a client has reason to believe its
access token is valid, and it receives an auth error, they should attempt to
refresh for a new token on failure, and retry the request with the new token.
refresh_token:
type: string
# TODO: Work out how to linkify /tokenrefresh
description: |-
(optional) A ``refresh_token`` may be exchanged for a new ``access_token`` using the /tokenrefresh API endpoint.
home_server:
type: string
description: The hostname of the Home Server on which the account has been registered.
400:
description: |-
Part of the request was invalid. This may include one of the following error codes:
* ``M_USER_IN_USE`` : The desired user ID is already taken.
* ``M_EXCLUSIVE`` : The desired user ID is in the exclusive namespace
claimed by an application service.
These errors may be returned at any stage of the registration process,
including after authentication if the requested user ID was registered
whilst the client was performing authentication.
Home Servers MUST perform the relevant checks and return these codes before
performing `User-Interactive Authentication`_, although they may also return
them after authentication is completed if, for example, the requested user ID
was registered whilst the client was performing authentication.
examples:
application/json: |-
{
"errcode": "M_USER_IN_USE",
"error": "Desired user ID is already taken."
}
429:
description: This request was rate-limited.
schema:
"$ref": "definitions/error.yaml"

@ -0,0 +1,307 @@
swagger: '2.0'
info:
title: "Matrix Client-Server v2 sync API"
version: "1.0.0"
host: localhost:8008
schemes:
- https
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:
"/sync":
get:
summary: Synchronise the client's state and receive new messages.
description: |-
Synchronise the client's state with the latest state on the server.
Clients use this API when they first log in to get an initial snapshot
of the state on the server, and then continue to call this API to get
incremental deltas to the state, and to receive new messages.
security:
- accessToken: []
parameters:
- in: query
name: filter
type: string
description: |-
The ID of a filter created using the filter API.
x-example: "66696p746572"
- in: query
name: since
type: string
description: |-
A point in time to continue a sync from.
x-example: "s72594_4483_1934"
- in: query
name: full_state
type: boolean
description: |-
Controls whether to include the full state for all rooms the user
is a member of.
If this is set to ``true``, then all state events will be returned,
even if ``since`` is non-empty. The timeline will still be limited
by the ``since`` parameter. In this case, the ``timeout`` parameter
will be ignored and the query will return immediately, possibly with
an empty timeline.
If ``false``, and ``since`` is non-empty, only state which has
changed since the point indicated by ``since`` will be returned.
By default, this is ``false``.
x-example: "false"
- in: query
name: set_presence
type: string
enum: ["offline"]
description: |-
Controls whether the client is automatically marked as online by
polling this API. If this parameter is omitted then the client is
automatically marked as online when it uses this API. Otherwise if
the parameter is set to "offline" then the client is not marked as
being online when it uses this API.
x-example: "offline"
- in: query
name: timeout
type: integer
description: |-
The maximum time to poll in milliseconds before returning this
request.
x-example: 30000
responses:
200:
description:
The initial snapshot or delta for the client to use to update their
state.
schema:
type: object
properties:
next_batch:
type: string
description: |-
The batch token to supply in the ``since`` param of the next
``/sync`` request.
rooms:
title: Rooms
type: object
description: |-
Updates to rooms.
properties:
joined:
title: Joined
type: object
additionalProperties:
title: Joined Room
type: object
properties:
event_map:
title: EventMap
type: object
description: |-
A map from event ID to events for this room. The
events are referenced from the ``timeline`` and
``state`` keys for this room.
additionalProperties:
title: Event
description: An event object.
type: object
allOf:
- $ref: "core-event-schema/event.json"
state:
title: State
type: object
description: |-
The state updates for the room.
allOf:
- $ref: "definitions/room_event_batch.json"
timeline:
title: Timeline
type: object
description: |-
The timeline of messages and state changes in the
room.
allOf:
- $ref: "definitions/timeline_batch.json"
ephemeral:
title: Ephemeral
type: object
description: |-
The ephemeral events in the room that aren't
recorded in the timeline or state of the room.
e.g. typing.
allOf:
- $ref: "definitions/event_batch.json"
invited:
title: Invited
type: object
description: |-
The rooms that the user has been invited to.
additionalProperties:
title: Invited Room
type: object
properties:
invite_state:
title: InviteState
type: object
description: |-
The state of a room that the user has been invited
to. These state events may only have the ``sender``,
``type``, ``state_key`` and ``content`` keys
present. These events do not replace any state that
the client already has for the room, for example if
the client has archived the room. Instead the
client should keep two separate copies of the
state: the one from the ``invite_state`` and one
from the archived ``state``. If the client joins
the room then the current state will be given as a
delta against the archived ``state`` not the
``invite_state``.
allOf:
- $ref: "definitions/event_batch.json"
archived:
title: Archived
type: object
description: |-
The rooms that the user has left or been banned from. The
entries in the room_map will lack an ``ephemeral`` key.
additionalProperties:
title: Archived Room
type: object
properties:
event_map:
title: EventMap
type: object
description: |-
A map from event ID to events for this room. The
events are referenced from the ``timeline`` and
``state`` keys for this room.
additionalProperties:
title: Event
description: An event object.
type: object
allOf:
- $ref: "core-event-schema/event.json"
state:
title: State
type: object
description: |-
The state updates for the room up to the point when
the user left.
allOf:
- $ref: "definitions/room_event_batch.json"
timeline:
title: Timeline
type: object
description: |-
The timeline of messages and state changes in the
room up to the point when the user left.
allOf:
- $ref: "definitions/timeline_batch.json"
presence:
title: Presence
type: object
description: |-
The updates to the presence status of other users.
allOf:
- $ref: "definitions/event_batch.json"
examples:
application/json: |-
{
"next_batch": "s72595_4483_1934",
"presence": {
"events": [
{
"sender": "@alice:example.com",
"type": "m.presence",
"content": {"presence": "online"}
}
]
},
"rooms": {
"joined": {
"!726s6s6q:example.com": {
"event_map": {
"$66697273743031:example.com": {
"sender": "@alice:example.com",
"type": "m.room.member",
"state_key": "@alice:example.com",
"content": {"membership": "join"},
"origin_server_ts": 1417731086795
},
"$7365636s6r6432:example.com": {
"sender": "@bob:example.com",
"type": "m.room.member",
"state_key": "@bob:example.com",
"content": {"membership": "join"},
"unsigned": {
"prev_content": {"membership": "invite"}
},
"origin_server_ts": 1417731086795
},
"$74686972643033:example.com": {
"sender": "@alice:example.com",
"type": "m.room.message",
"unsigned": {"age": "124524", "txn_id": "1234"},
"content": {
"body": "I am a fish",
"msgtype": "m.text"
},
"origin_server_ts": 1417731086797
}
},
"state": {
"events": [
"$66697273743031:example.com",
"$7365636s6r6432:example.com"
]
},
"timeline": {
"events": [
"$7365636s6r6432:example.com",
"$74686972643033:example.com"
],
"limited": true,
"prev_batch": "t34-23535_0_0"
},
"ephemeral": {
"events": [
{
"room_id": "!726s6s6q:example.com",
"type": "m.typing",
"content": {"user_ids": ["@alice:example.com"]}
}
]
}
}
},
"invited": {
"!696r7674:example.com": {
"invite_state": {
"events": [
{
"sender": "@alice:example.com",
"type": "m.room.name",
"state_key": "",
"content": {"name": "My Room Name"}
},
{
"sender": "@alice:example.com",
"type": "m.room.member",
"state_key": "@bob:example.com",
"content": {"membership": "invite"}
}
]
}
}
},
"archived": {}
}
}

@ -10,6 +10,6 @@
"license": "ISC",
"dependencies": {
"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) {
return;
}
console.log(metadata);
console.error(err);
process.exit(1);
};
@ -44,12 +43,14 @@ if (isDir) {
process.exit(1);
}
files.forEach(function(f) {
if (f.indexOf(".yaml") > 0) {
parser.parse(path.join(opts.schema, f), function(err, api, metadata) {
var suffix = ".yaml";
if (f.indexOf(suffix, f.length - suffix.length) > 0) {
parser.validate(path.join(opts.schema, f), function(err, api, metadata) {
if (!err) {
console.log("%s is valid.", f);
}
else {
console.error("%s is not valid.", f);
errFn(err, api, metadata);
}
});
@ -58,12 +59,12 @@ if (isDir) {
});
}
else{
parser.parse(opts.schema, function(err, api, metadata) {
parser.validate(opts.schema, function(err, api) {
if (!err) {
console.log("%s is valid", opts.schema);
}
else {
errFn(err, api, metadata);
errFn(err, api);
}
});
};

@ -3,10 +3,10 @@ Address book repository
.. NOTE::
This section is a work in progress.
Do we even need it? Clients can use out-of-band addressbook servers for now;
this should definitely not be core.
.. TODO-spec
Do we even need it? Clients can use out-of-band addressbook servers for now;
this should definitely not be core.
- format: POST(?) wodges of json, some possible processing, then return wodges of json on GET.
- processing may remove dupes, merge contacts, pepper with extra info (e.g. matrix-ability of
contacts), etc.

@ -0,0 +1,34 @@
Macaroon Caveats
================
`Macaroons`_ are issued by Matrix servers as authorization tokens. Macaroons may be restricted by adding caveats to them.
.. _Macaroons: http://theory.stanford.edu/~ataly/Papers/macaroons.pdf
Caveats can only be used for reducing the scope of a token, never for increasing it. Servers are required to reject any macroon with a caveat that they do not understand.
Some caveats are specified in this specification, and must be understood by all servers. The use of non-standard caveats is allowed.
All caveats must take the form:
`key` `operator` `value`
where `key` is a non-empty string drawn from the character set [A-Za-z0-9_]
`operator` is a non-empty string which does not contain whitespace
`value` is a non-empty string
And these are joined by single space characters.
Specified caveats:
+-------------+--------------------------------------------------+------------------------------------------------------------------------------------------------+
| Caveat name | Description | Legal Values |
+-------------+--------------------------------------------------+------------------------------------------------------------------------------------------------+
| gen | Generation of the macaroon caveat spec. | 1 |
| user_id | ID of the user for which this macaroon is valid. | Pure equality check. Operator must be =. |
| type | The purpose of this macaroon. | access - used to authorize any action except token refresh |
| refresh - only used to authorize a token refresh |
| time | Time before/after which this macaroon is valid. | A POSIX timestamp in milliseconds (in UTC). |
| Operator < means the macaroon is valid before the timestamp, as interpreted by the server. |
| Operator > means the macaroon is valid after the timestamp, as interpreted by the server. |
| Operator == means the macaroon is valid at exactly the timestamp, as interpreted by the server.|
| Note that exact equality of time is largely meaningless. |
+-------------+--------------------------------------------------+------------------------------------------------------------------------------------------------+

@ -1,5 +1,11 @@
#!/bin/bash -e
# Runs z-schema over all of the schema files (looking for matching examples)
if ! which z-schema; then
echo >&2 "Need to install z-schema; run: sudo npm install -g z-schema"
exit 1
fi
find schema/v1/m.* | while read line
do
split_path=(${line///// })

@ -0,0 +1,76 @@
#! /usr/bin/env python
import sys
import json
import os
import traceback
def import_error(module, package, debian, error):
sys.stderr.write((
"Error importing %(module)s: %(error)r\n"
"To install %(module)s run:\n"
" pip install %(package)s\n"
"or on Debian run:\n"
" sudo apt-get install python-%(debian)s\n"
) % locals())
if __name__ == '__main__':
sys.exit(1)
try:
import jsonschema
except ImportError as e:
import_error("jsonschema", "jsonschema", "jsonschema", e)
raise
try:
import yaml
except ImportError as e:
import_error("yaml", "PyYAML", "yaml", e)
raise
def check_example_file(examplepath, schemapath):
with open(examplepath) as f:
example = yaml.load(f)
with open(schemapath) as f:
schema = yaml.load(f)
fileurl = "file://" + os.path.abspath(schemapath)
print ("Checking schema for: %r %r" % (examplepath, schemapath))
# Setting the 'id' tells jsonschema where the file is so that it
# can correctly resolve relative $ref references in the schema
schema['id'] = fileurl
try:
jsonschema.validate(example, schema)
except Exception as e:
raise ValueError("Error validating JSON schema for %r %r" % (
examplepath, schemapath
), e)
def check_example_dir(exampledir, schemadir):
errors = []
for root, dirs, files in os.walk(exampledir):
for filename in files:
if filename.startswith("."):
# Skip over any vim .swp files.
continue
examplepath = os.path.join(root, filename)
schemapath = examplepath.replace(exampledir, schemadir)
try:
check_example_file(examplepath, schemapath)
except Exception as e:
errors.append(sys.exc_info())
for (exc_type, exc_value, exc_trace) in errors:
traceback.print_exception(exc_type, exc_value, exc_trace)
if errors:
raise ValueError("Error validating examples")
if __name__ == '__main__':
try:
check_example_dir("examples", "schema")
except:
sys.exit(1)

@ -0,0 +1,13 @@
{
"type": "m.receipt",
"room_id": "!KpjVgQyZpzBwvMBsnT:matrix.org",
"content": {
"$1435641916114394fHBLK:matrix.org": {
"m.read": {
"@rikj:jki.re": {
"ts": 1436451550453
}
}
}
}
}

@ -0,0 +1,18 @@
{
"age": 242352,
"content": {
"info": {
"h": 398,
"w": 394,
"mimetype": "image/jpeg",
"size": 31037
},
"url": "mxc://localhost/JWEIFJgwEIhweiWJE"
},
"origin_server_ts": 1431961217939,
"event_id": "$WLGTSEFSEF:localhost",
"type": "m.room.avatar",
"state_key": "",
"room_id": "!Cuyf34gef24t:localhost",
"user_id": "@example:localhost"
}

@ -0,0 +1,12 @@
{
"age": 242352,
"content": {
"alias": "#somewhere:localhost"
},
"state_key": "",
"origin_server_ts": 1431961217939,
"event_id": "$WLGTSEFSEF:localhost",
"type": "m.room.canonical_alias",
"room_id": "!Cuyf34gef24t:localhost",
"user_id": "@example:localhost"
}

@ -0,0 +1,12 @@
{
"age": 242352,
"content": {
"history_visibility": "shared"
},
"state_key": "",
"origin_server_ts": 1431961217939,
"event_id": "$WLGTSEFSEF:localhost",
"type": "m.room.history_visibility",
"room_id": "!Cuyf34gef24t:localhost",
"user_id": "@example:localhost"
}

@ -3,8 +3,39 @@
"content": {
"membership": "join",
"avatar_url": "mxc://localhost/SEsfnsuifSDFSSEF#auto",
"displayname": "Alice Margatroid"
"displayname": "Alice Margatroid",
"third_party_invite": {
"token": "pc98",
"public_key": "abc123",
"key_validity_url": "https://magic.forest/verifykey",
"signed": {
"mxid": "@alice:localhost",
"token": "pc98",
"signatures": {
"magic.forest": {
"ed25519:0": "poi098"
}
}
},
"sender": "@zun:zun.soft"
}
},
"invite_room_state": [
{
"type": "m.room.name",
"state_key": "",
"content": {
"name": "Forest of Magic"
}
},
{
"type": "m.room.join_rules",
"state_key": "",
"content": {
"join_rules": "invite"
}
}
],
"state_key": "@alice:localhost",
"origin_server_ts": 1431961217939,
"event_id": "$WLGTSEFSEF:localhost",

@ -0,0 +1,14 @@
{
"age": 242352,
"content": {
"display_name": "Alice Margatroid",
"key_validity_url": "https://magic.forest/verifykey",
"public_key": "abc123"
},
"state_key": "pc98",
"origin_server_ts": 1431961217939,
"event_id": "$WLGTSEFSEF:localhost",
"type": "m.room.third_party_invite",
"room_id": "!Cuyf34gef24t:localhost",
"sender": "@example:localhost"
}

@ -0,0 +1,7 @@
{
"type": "m.typing",
"room_id": "!z0mnsuiwhifuhwwfw:matrix.org",
"content": {
"user_ids": ["@alice:matrix.org", "@bob:example.com"]
}
}

@ -1,88 +0,0 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"definitions": {
"event": {
"title": "Event",
"description": "The basic set of fields all events must have.",
"type": "object",
"properties": {
"event_id": {
"type": "string",
"description": "The globally unique event identifier."
},
"user_id": {
"type": "string",
"description": "Contains the fully-qualified ID of the user who *sent* this event."
},
"content": {
"type": "object",
"description": "The fields in this object will vary depending on the type of event. When interacting with the REST API, this is the HTTP body."
},
"type": {
"type": "string",
"description": "The type of event. This SHOULD be namespaced similar to Java package naming conventions e.g. 'com.example.subdomain.event.type'"
}
},
"required": ["event_id", "user_id", "content", "type"]
},
"room_event": {
"type": "object",
"title": "Room Event",
"description": "In addition to the Event fields, Room Events MUST have the following additional field.",
"allOf":[{
"$ref": "#/definitions/event"
}],
"properties": {
"room_id": {
"type": "string",
"description": "The ID of the room associated with this event."
}
},
"required": ["room_id"]
},
"state_event": {
"type": "object",
"title": "State Event",
"description": "In addition to the Room Event fields, State Events have the following additional fields.",
"allOf":[{
"$ref": "#/definitions/room_event"
}],
"properties": {
"state_key": {
"type": "string",
"description": "A unique key which defines the overwriting semantics for this piece of room state. This value is often a zero-length string. The presence of this key makes this event a State Event."
},
"prev_content": {
"type": "object",
"description": "Optional. The previous ``content`` for this event. If there is no previous content, this key will be missing."
}
},
"required": ["state_key"]
},
"msgtype_infos": {
"image_info": {
"type": "object",
"title": "ImageInfo",
"description": "Metadata about an image.",
"properties": {
"size": {
"type": "integer",
"description": "Size of the image in bytes."
},
"w": {
"type": "integer",
"description": "The width of the image in pixels."
},
"h": {
"type": "integer",
"description": "The height of the image in pixels."
},
"mimetype": {
"type": "string",
"description": "The mimetype of the image, e.g. ``image/jpeg``."
}
}
}
}
}
}

@ -0,0 +1,15 @@
{
"type": "object",
"title": "Event",
"description": "The basic set of fields all events must have.",
"properties": {
"content": {
"type": "object",
"description": "The fields in this object will vary depending on the type of event. When interacting with the REST API, this is the HTTP body."
},
"type": {
"type": "string",
"description": "The type of event. This SHOULD be namespaced similar to Java package naming conventions e.g. 'com.example.subdomain.event.type'"
}
}
}

@ -0,0 +1,23 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"title": "ImageInfo",
"description": "Metadata about an image.",
"properties": {
"size": {
"type": "integer",
"description": "Size of the image in bytes."
},
"w": {
"type": "integer",
"description": "The width of the image in pixels."
},
"h": {
"type": "integer",
"description": "The height of the image in pixels."
},
"mimetype": {
"type": "string",
"description": "The mimetype of the image, e.g. ``image/jpeg``."
}
}
}

@ -0,0 +1,23 @@
{
"type": "object",
"title": "Room Event",
"description": "In addition to the Event fields, Room Events MUST have the following additional field.",
"allOf":[{
"$ref": "core-event-schema/event.json"
}],
"properties": {
"room_id": {
"type": "string",
"description": "The ID of the room associated with this event."
},
"event_id": {
"type": "string",
"description": "The globally unique event identifier."
},
"user_id": {
"type": "string",
"description": "Contains the fully-qualified ID of the user who *sent* this event."
}
},
"required": ["room_id"]
}

@ -0,0 +1,19 @@
{
"type": "object",
"title": "State Event",
"description": "In addition to the Room Event fields, State Events have the following additional fields.",
"allOf":[{
"$ref": "core-event-schema/room_event.json"
}],
"properties": {
"state_key": {
"type": "string",
"description": "A unique key which defines the overwriting semantics for this piece of room state. This value is often a zero-length string. The presence of this key makes this event a State Event. The key MUST NOT start with '_'."
},
"prev_content": {
"type": "object",
"description": "Optional. The previous ``content`` for this event. If there is no previous content, this key will be missing."
}
},
"required": ["state_key"]
}

@ -1,9 +1,8 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"type": "object",
"description": "This event is sent by the callee when they wish to answer the call.",
"allOf": [{
"$ref": "core#/definitions/room_event"
"$ref": "core-event-schema/room_event.json"
}],
"properties": {
"content": {

@ -1,9 +1,8 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"type": "object",
"description": "This event is sent by callers after sending an invite and by the callee after answering. Its purpose is to give the other party additional ICE candidates to try using to communicate.",
"allOf": [{
"$ref": "core#/definitions/room_event"
"$ref": "core-event-schema/room_event.json"
}],
"properties": {
"content": {

@ -1,9 +1,8 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"type": "object",
"description": "Sent by either party to signal their termination of the call. This can be sent either once the call has has been established or before to abort the call.",
"allOf": [{
"$ref": "core#/definitions/room_event"
"$ref": "core-event-schema/room_event.json"
}],
"properties": {
"content": {

@ -1,9 +1,8 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"type": "object",
"description": "This event is sent by the caller when they wish to establish a call.",
"allOf": [{
"$ref": "core#/definitions/room_event"
"$ref": "core-event-schema/room_event.json"
}],
"properties": {
"content": {

@ -1,5 +1,4 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"type": "object",
"title": "Presence Event",
"description": "Informs the client of a user's presence state change.",

@ -0,0 +1,48 @@
{
"type": "object",
"title": "Receipt Event",
"description": "Informs the client of new receipts.",
"properties": {
"content": {
"type": "object",
"patternProperties": {
"^\\$": {
"type": "object",
"x-pattern": "$EVENT_ID",
"title": "Receipts",
"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.",
"properties": {
"m.read": {
"type": "object",
"title": "Users",
"description": "A collection of users who have sent ``m.read`` receipts for this event.",
"patternProperties": {
"^@": {
"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
},
"type": {
"type": "string",
"enum": ["m.receipt"]
},
"room_id": {
"type": "string"
}
},
"required": ["room_id", "type", "content"]
}

@ -1,10 +1,9 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"type": "object",
"title": "Informs the room about what room aliases it has been given.",
"description": "This event is sent by a homeserver directly to inform of changes to the list of aliases it knows about for that room. The ``state_key`` for this event is set to the homeserver which owns the room alias. The entire set of known aliases for the room is the union of all the ``m.room.aliases`` events, one for each homeserver. Clients **should** check the validity of any room alias given in this list before presenting it to the user as trusted fact. The lists given by this event should be considered simply as advice on which aliases might exist, for which the client can perform the lookup to confirm whether it receives the correct room ID.",
"allOf": [{
"$ref": "core#/definitions/state_event"
"$ref": "core-event-schema/state_event.json"
}],
"properties": {
"content": {

@ -0,0 +1,64 @@
{
"title": "RoomAvatar",
"description": "A picture that is associated with the room. This can be displayed alongside the room information.",
"type": "object",
"allOf": [{
"$ref": "core-event-schema/state_event.json"
}],
"properties": {
"content": {
"type": "object",
"properties": {
"url": {
"type": "string",
"description": "The URL to the image."
},
"thumbnail_url": {
"type": "string",
"description": "The URL to the thumbnail of the image."
},
"thumbnail_info": {
"type": "object",
"title": "ImageInfo",
"description": "Metadata about the image referred to in ``thumbnail_url``.",
"allOf": [{
"$ref": "core-event-schema/msgtype_infos/image_info.json"
}]
},
"info": {
"type": "object",
"title": "ImageInfo",
"description": "Metadata about the image referred to in ``url``.",
"properties": {
"size": {
"type": "integer",
"description": "Size of the image in bytes."
},
"w": {
"type": "integer",
"description": "The width of the image in pixels."
},
"h": {
"type": "integer",
"description": "The height of the image in pixels."
},
"mimetype": {
"type": "string",
"description": "The mimetype of the image, e.g. ``image/jpeg``."
}
}
}
},
"required": ["url"]
},
"state_key": {
"type": "string",
"description": "A zero-length string.",
"pattern": "^$"
},
"type": {
"type": "string",
"enum": ["m.room.avatar"]
}
}
}

@ -0,0 +1,29 @@
{
"type": "object",
"title": "Informs the room as to which alias is the canonical one.",
"description": "This event is used to inform the room about which alias should be considered the canonical one. This could be for display purposes or as suggestion to users which alias to use to advertise the room.",
"allOf": [{
"$ref": "core-event-schema/state_event.json"
}],
"properties": {
"content": {
"type": "object",
"properties": {
"alias": {
"type": "string",
"description": "The canonical alias."
}
},
"required": ["alias"]
},
"state_key": {
"type": "string",
"description": "A zero-length string.",
"pattern": "^$"
},
"type": {
"type": "string",
"enum": ["m.room.canonical_alias"]
}
}
}

@ -1,10 +1,9 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"type": "object",
"title": "The first event in the room.",
"description": "This is the first event in a room and cannot be changed. It acts as the root of all other events.",
"allOf": [{
"$ref": "core#/definitions/state_event"
"$ref": "core-event-schema/state_event.json"
}],
"properties": {
"content": {
@ -13,6 +12,10 @@
"creator": {
"type": "string",
"description": "The ``user_id`` of the room creator. This is set by the homeserver."
},
"m.federate": {
"type": "boolean",
"description": "Whether users on other servers can join this room. Defaults to ``true`` if key does not exist."
}
},
"required": ["creator"]

@ -0,0 +1,30 @@
{
"type": "object",
"title": "Controls visibility of history.",
"description": "This event controls whether a user can see the events that happened in a room from before they joined.",
"allOf": [{
"$ref": "core-event-schema/state_event.json"
}],
"properties": {
"content": {
"type": "object",
"properties": {
"history_visibility": {
"type": "string",
"description": "Who can see the room history.",
"enum": ["invited","joined","shared","world_readable"]
}
},
"required": ["history_visibility"]
},
"state_key": {
"type": "string",
"description": "A zero-length string.",
"pattern": "^$"
},
"type": {
"type": "string",
"enum": ["m.room.history_visibility"]
}
}
}

@ -1,10 +1,9 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"type": "object",
"title": "Describes how users are allowed to join the room.",
"description": "A room may be ``public`` meaning anyone can join the room without any prior action. Alternatively, it can be ``invite`` meaning that a user who wishes to join the room must first receive an invite to the room from someone already inside of the room. Currently, ``knock`` and ``private`` are reserved keywords which are not implemented.",
"allOf": [{
"$ref": "core#/definitions/state_event"
"$ref": "core-event-schema/state_event.json"
}],
"properties": {
"content": {

@ -1,10 +1,9 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"type": "object",
"title": "The current membership state of a user in the room.",
"description": "Adjusts the membership state for a user in a room. It is preferable to use the membership APIs (``/rooms/<room id>/invite`` etc) when performing membership actions rather than adjusting the state directly as there are a restricted set of valid transformations. For example, user A cannot force user B to join a room, and trying to force this state change directly will fail.",
"description": "Adjusts the membership state for a user in a room. It is preferable to use the membership APIs (``/rooms/<room id>/invite`` etc) when performing membership actions rather than adjusting the state directly as there are a restricted set of valid transformations. For example, user A cannot force user B to join a room, and trying to force this state change directly will fail. \n\nThe ``third_party_invite`` property will be set if the invite was an ``m.room.third_party_invite`` event, and absent if the invite was an ``m.room.member`` event.\n\nThis event also includes an ``invite_room_state`` key **outside the** ``content`` **key**. This contains an array of ``StrippedState`` Events. These events provide information on a few select state events such as the room name.",
"allOf": [{
"$ref": "core#/definitions/state_event"
"$ref": "core-event-schema/state_event.json"
}],
"properties": {
"content": {
@ -20,8 +19,51 @@
"description": "The avatar URL for this user, if any. This is added by the homeserver."
},
"displayname": {
"type": "string",
"type": ["string", "null"],
"description": "The display name for this user, if any. This is added by the homeserver."
},
"third_party_invite": {
"type": "object",
"title": "Invite",
"properties": {
"token": {
"type": "string",
"description": "A token which must be correctly signed, in order to join the room."
},
"key_validity_url": {
"type": "string",
"description": "A URL which can be fetched, with querystring ``public_key=public_key``, to validate whether the key has been revoked. The URL must return a JSON object containing a boolean property named 'valid'."
},
"public_key": {
"type": "string",
"description": "A base64-encoded ed25519 key with which token must be signed."
},
"signed": {
"type": "object",
"title": "signed",
"properties": {
"mxid": {
"type": "string",
"description": "The invited matrix user ID. Must be equal to the user_id property of the event."
},
"token": {
"type": "string",
"description": "The token property of the containing third_party_invite object."
},
"signatures": {
"type": "object",
"description": "A single signature from the verifying server, in the format specified by the Signing Events section.",
"title": "Signatures"
}
},
"required": ["mxid", "signatures", "token"]
},
"sender": {
"type": "string",
"description": "The matrix user ID of the user who send the invite which is being used."
}
},
"required": ["token", "key_validity_url", "public_key", "sender", "signed"]
}
},
"required": ["membership"]
@ -33,6 +75,32 @@
"type": {
"type": "string",
"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": "StrippedState",
"description": "A stripped down state event, with only the ``type``, ``state_key`` and ``content`` keys.",
"required": ["type", "state_key", "content"],
"properties": {
"type": {
"type": "string",
"description": "The ``type`` for the event.",
"enum": ["m.room.join_rules", "m.room.canonical_alias", "m.room.avatar", "m.room.name"]
},
"state_key": {
"type": "string",
"description": "The ``state_key`` for the event."
},
"content": {
"title": "EventContent",
"type": "object",
"description": "The ``content`` for the event."
}
}
}
}
}
}

@ -1,10 +1,9 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"type": "object",
"title": "Message",
"description": "This event is used when sending messages in a room. Messages are not limited to be text. The ``msgtype`` key outlines the type of message, e.g. text, audio, image, video, etc. The ``body`` key is text and MUST be used with every kind of ``msgtype`` as a fallback mechanism for when a client cannot render a message.",
"description": "This event is used when sending messages in a room. Messages are not limited to be text. The ``msgtype`` key outlines the type of message, e.g. text, audio, image, video, etc. The ``body`` key is text and MUST be used with every kind of ``msgtype`` as a fallback mechanism for when a client cannot render a message. This allows clients to display *something* even if it is just plain text.",
"allOf": [{
"$ref": "core#/definitions/room_event"
"$ref": "core-event-schema/room_event.json"
}],
"properties": {
"content": {

@ -1,10 +1,9 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"type": "object",
"title": "AudioMessage",
"description": "This message represents a single audio clip.",
"allOf": [{
"$ref": "core#/definitions/room_event"
"$ref": "core-event-schema/room_event.json"
}],
"properties": {
"content": {

@ -1,10 +1,9 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"type": "object",
"title": "EmoteMessage",
"description": "This message is similar to ``m.text`` except that the sender is 'performing' the action contained in the ``body`` key, similar to ``/me`` in IRC. This message should be prefixed by the name of the sender. This message could also be represented in a different colour to distinguish it from regular ``m.text`` messages.",
"allOf": [{
"$ref": "core#/definitions/room_event"
"$ref": "core-event-schema/room_event.json"
}],
"properties": {
"content": {

@ -1,10 +1,9 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"type": "object",
"title": "FileMessage",
"description": "This message represents a generic file.",
"allOf": [{
"$ref": "core#/definitions/room_event"
"$ref": "core-event-schema/room_event.json"
}],
"properties": {
"content": {
@ -50,7 +49,7 @@
"title": "ImageInfo",
"description": "Metadata about the image referred to in ``thumbnail_url``.",
"allOf": [{
"$ref": "core#/definitions/msgtype_infos/image_info"
"$ref": "core-event-schema/msgtype_infos/image_info.json"
}]
}
},

@ -1,10 +1,9 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"type": "object",
"title": "ImageMessage",
"description": "This message represents a single image and an optional thumbnail.",
"allOf": [{
"$ref": "core#/definitions/room_event"
"$ref": "core-event-schema/room_event.json"
}],
"properties": {
"content": {
@ -31,7 +30,7 @@
"title": "ImageInfo",
"description": "Metadata about the image referred to in ``thumbnail_url``.",
"allOf": [{
"$ref": "core#/definitions/msgtype_infos/image_info"
"$ref": "core-event-schema/msgtype_infos/image_info.json"
}]
},
"info": {

@ -1,10 +1,9 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"type": "object",
"title": "LocationMessage",
"description": "This message represents a real-world location.",
"allOf": [{
"$ref": "core#/definitions/room_event"
"$ref": "core-event-schema/room_event.json"
}],
"properties": {
"content": {
@ -30,7 +29,7 @@
"type": "object",
"title": "ImageInfo",
"allOf": [{
"$ref": "core#/definitions/msgtype_infos/image_info"
"$ref": "core-event-schema/msgtype_infos/image_info.json"
}]
}
},

@ -1,10 +1,9 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"type": "object",
"title": "NoticeMessage",
"description": "A m.notice message should be considered similar to a plain m.text message except that clients should visually distinguish it in some way. It is intended to be used by automated clients, such as bots, bridges, and other entities, rather than humans. Additionally, such automated agents which watch a room for messages and respond to them ought to ignore m.notice messages. This helps to prevent infinite-loop situations where two automated clients continuously exchange messages, as each responds to the other.",
"allOf": [{
"$ref": "core#/definitions/room_event"
"$ref": "core-event-schema/room_event.json"
}],
"properties": {
"content": {

@ -1,10 +1,9 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"type": "object",
"title": "TextMessage",
"description": "This message is the most basic message and is used to represent text.",
"allOf": [{
"$ref": "core#/definitions/room_event"
"$ref": "core-event-schema/room_event.json"
}],
"properties": {
"content": {

@ -1,10 +1,9 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"type": "object",
"title": "VideoMessage",
"description": "This message represents a single video clip.",
"allOf": [{
"$ref": "core#/definitions/room_event"
"$ref": "core-event-schema/room_event.json"
}],
"properties": {
"content": {
@ -55,7 +54,7 @@
"type": "object",
"title": "ImageInfo",
"allOf": [{
"$ref": "core#/definitions/msgtype_infos/image_info"
"$ref": "core-event-schema/msgtype_infos/image_info.json"
}]
}
}

@ -1,10 +1,9 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"type": "object",
"title": "MessageFeedback",
"description": "Feedback events are events sent to acknowledge a message in some way. There are two supported acknowledgements: ``delivered`` (sent when the event has been received) and ``read`` (sent when the event has been observed by the end-user). The ``target_event_id`` should reference the ``m.room.message`` event being acknowledged. N.B. not implemented in Synapse, and superceded in v2 CS API by the ``relates_to`` event field.",
"description": "**NB: Usage of this event is discouraged in favour of the** `receipts module`_. **Most clients will not recognise this event.** Feedback events are events sent to acknowledge a message in some way. There are two supported acknowledgements: ``delivered`` (sent when the event has been received) and ``read`` (sent when the event has been observed by the end-user). The ``target_event_id`` should reference the ``m.room.message`` event being acknowledged.",
"allOf": [{
"$ref": "core#/definitions/room_event"
"$ref": "core-event-schema/room_event.json"
}],
"properties": {
"content": {

@ -1,10 +1,9 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"title": "RoomName",
"description": "A room has an opaque room ID which is not human-friendly to read. A room alias is human-friendly, but not all rooms have room aliases. The room name is a human-friendly string designed to be displayed to the end-user. The room name is not unique, as multiple rooms can have the same room name set. The room name can also be set when creating a room using ``/createRoom`` with the ``name`` key.",
"type": "object",
"allOf": [{
"$ref": "core#/definitions/state_event"
"$ref": "core-event-schema/state_event.json"
}],
"properties": {
"content": {
@ -12,7 +11,7 @@
"properties": {
"name": {
"type": "string",
"description": "The name of the room."
"description": "The name of the room. This MUST NOT exceed 255 bytes."
}
},
"required": ["name"]

@ -1,10 +1,9 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"type": "object",
"title": "Defines the power levels (privileges) of users in the room.",
"description": "This event specifies the minimum level a user must have in order to perform a certain action. It also specifies the levels of each user in the room. If a ``user_id`` is in the ``users`` list, then that ``user_id`` has the associated power level. Otherwise they have the default level ``users_default``. If ``users_default`` is not supplied, it is assumed to be 0. The level required to send a certain event is governed by ``events``, ``state_default`` and ``events_default``. If an event type is specified in ``events``, then the user must have at least the level specified in order to send that event. If the event type is not supplied, it defaults to ``events_default`` for Message Events and ``state_default`` for State Events.",
"allOf": [{
"$ref": "core#/definitions/state_event"
"$ref": "core-event-schema/state_event.json"
}],
"properties": {
"content": {

@ -1,10 +1,9 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"type": "object",
"title": "Redaction",
"description": "Events can be redacted by either room or server admins. Redacting an event means that all keys not required by the protocol are stripped off, allowing admins to remove offensive or illegal content that may have been attached to any event. This cannot be undone, allowing server owners to physically delete the offending data. There is also a concept of a moderator hiding a message event, which can be undone, but cannot be applied to state events. The event that has been redacted is specified in the ``redacts`` event level key.",
"allOf": [{
"$ref": "core#/definitions/room_event"
"$ref": "core-event-schema/room_event.json"
}],
"properties": {
"content": {

@ -0,0 +1,37 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"type": "object",
"title": "An invitation to a room issued to a third party identifier, rather than a matrix user ID.",
"description": "Acts as an ``m.room.member`` invite event, where there isn't a target user_id to invite. This event contains a token and a public key whose private key must be used to sign the token. Any user who can present that signature may use this invitation to join the target room.",
"allOf": [{
"$ref": "core-event-schema/state_event.json"
}],
"properties": {
"content": {
"type": "object",
"properties": {
"display_name": {
"type": "string",
"description": "A user-readable string which represents the user who has been invited. This should not contain the user's third party ID, as otherwise when the invite is accepted it would leak the association between the matrix ID and the third party ID."
},
"key_validity_url": {
"type": "string",
"description": "A URL which can be fetched, with querystring public_key=public_key, to validate whether the key has been revoked. The URL must return a JSON object containing a boolean property named 'valid'."
},
"public_key": {
"type": "string",
"description": "A base64-encoded ed25519 key with which token must be signed."
}
},
"required": ["display_name", "key_validity_url", "public_key"]
},
"state_key": {
"type": "string",
"description": "The token, of which a signature must be produced in order to join the room."
},
"type": {
"type": "string",
"enum": ["m.room.third_party_invite"]
}
}
}

@ -1,10 +1,9 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"type": "object",
"title": "Topic",
"description": "A topic is a short message detailing what is currently being discussed in the room. It can also be used as a way to display extra information about the room, which may not be suitable for the room name. The room topic can also be set when creating a room using ``/createRoom`` with the ``topic`` key.",
"allOf": [{
"$ref": "core#/definitions/state_event"
"$ref": "core-event-schema/state_event.json"
}],
"properties": {
"content": {

@ -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"]
}

@ -0,0 +1,9 @@
#! /bin/bash
set -ex
(cd event-schemas/ && ./check_examples.py)
(cd api && ./check_examples.py)
(cd scripts && ./gendoc.py -v)
(cd api && npm install && node validator.js -s "client-server/v1" && node validator.js -s "client-server/v2_alpha")
(cd event-schemas/ && ./check.sh)

@ -0,0 +1,6 @@
continuserv proactively re-generates the spec on filesystem changes, and serves it over HTTP.
To run it, you must install the `go` tool. You will also need to install fsnotify by running:
`go get gopkg.in/fsnotify.v1`
You can then run continuserv by running:
`go run main.go`

@ -0,0 +1,161 @@
// continuserv proactively re-generates the spec on filesystem changes, and serves it over HTTP.
// It will always serve the most recent version of the spec, and may block an HTTP request until regeneration is finished.
// It does not currently pre-empt stale generations, but will block until they are complete.
package main
import (
"bytes"
"flag"
"fmt"
"io/ioutil"
"log"
"net/http"
"os"
"os/exec"
"path"
"path/filepath"
"strings"
"sync"
"sync/atomic"
"time"
fsnotify "gopkg.in/fsnotify.v1"
)
var (
port = flag.Int("port", 8000, "Port on which to serve HTTP")
toServe atomic.Value // Always contains valid []byte to serve. May be stale unless wg is zero.
wg sync.WaitGroup // Indicates how many updates are pending.
mu sync.Mutex // Prevent multiple updates in parallel.
)
func main() {
flag.Parse()
w, err := fsnotify.NewWatcher()
if err != nil {
log.Fatalf("Error making watcher: %v", err)
}
dir, err := os.Getwd()
if err != nil {
log.Fatalf("Error getting wd: %v", err)
}
for ; !exists(path.Join(dir, ".git")); dir = path.Dir(dir) {
if dir == "/" {
log.Fatalf("Could not find git root")
}
}
filepath.Walk(dir, makeWalker(w))
wg.Add(1)
populateOnce(dir)
ch := make(chan struct{}, 100) // Buffered to ensure we can multiple-increment wg for pending writes
go doPopulate(ch, dir)
go watchFS(ch, w)
fmt.Printf("Listening on port %d\n", *port)
http.HandleFunc("/", serve)
log.Fatal(http.ListenAndServe(fmt.Sprintf(":%d", *port), nil))
}
func watchFS(ch chan struct{}, w *fsnotify.Watcher) {
for {
select {
case e := <-w.Events:
if filter(e) {
fmt.Printf("Noticed change to %s, re-generating spec\n", e.Name)
ch <- struct{}{}
}
}
}
}
func makeWalker(w *fsnotify.Watcher) filepath.WalkFunc {
return func(path string, _ os.FileInfo, err error) error {
if err != nil {
log.Fatalf("Error walking: %v", err)
}
if err := w.Add(path); err != nil {
log.Fatalf("Failed to add watch: %v", err)
}
return nil
}
}
// Return true if event should trigger re-population
func filter(e fsnotify.Event) bool {
// vim is *really* noisy about how it writes files
if e.Op != fsnotify.Write {
return false
}
// Avoid some temp files that vim writes
if strings.HasSuffix(e.Name, "~") || strings.HasSuffix(e.Name, ".swp") || strings.HasPrefix(e.Name, ".") {
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
if strings.Contains(e.Name, "/tmp/") || strings.Contains(e.Name, "/gen/") {
return false
}
return true
}
func serve(w http.ResponseWriter, req *http.Request) {
wg.Wait()
b := toServe.Load().([]byte)
w.Write(b)
}
func populateOnce(dir string) {
defer wg.Done()
mu.Lock()
defer mu.Unlock()
cmd := exec.Command("python", "gendoc.py")
cmd.Dir = path.Join(dir, "scripts")
var b bytes.Buffer
cmd.Stderr = &b
err := cmd.Run()
if err != nil {
toServe.Store([]byte(fmt.Errorf("error generating spec: %v\nOutput from gendoc:\n%v", err, b.String()).Error()))
return
}
specBytes, err := ioutil.ReadFile(path.Join(dir, "scripts", "gen", "specification.html"))
if err != nil {
toServe.Store([]byte(fmt.Errorf("error reading spec: %v", err).Error()))
return
}
toServe.Store(specBytes)
}
func doPopulate(ch chan struct{}, dir string) {
var pending int
for {
select {
case <-ch:
if pending == 0 {
wg.Add(1)
}
pending++
case <-time.After(10 * time.Millisecond):
if pending > 0 {
pending = 0
populateOnce(dir)
}
}
}
}
func exists(path string) bool {
_, err := os.Stat(path)
return !os.IsNotExist(err)
}

@ -0,0 +1,5 @@
blockquote {
margin: 20px 0 30px;
border-left: 5px solid;
padding-left: 20px;
}

@ -0,0 +1,16 @@
pre.code .comment, code .comment { color: green }
pre.code .keyword, code .keyword { color: darkred; font-weight: bold }
pre.code .name.builtin, code .name.builtin { color: darkred; font-weight: bold }
pre.code .name.tag, code .name.tag { color: darkgreen }
pre.code .literal, code .literal { color: darkblue }
pre.code .literal.number, code .literal.number { color: blue }
/* HTTP Methods have class "name function" */
pre.code.http .name.function, code.http .name.function { color: black; font-weight: bold }
/* HTTP Paths have class "name namespace" */
pre.code.http .name.namespace, code.http .name.namespace { color: darkgreen }
/* HTTP "HTTP" strings have class "keyword reserved" */
pre.code.http .keyword.reserved, code.http .keyword.reserved { color: black; font-weight: bold }
/* HTTP Header names have class "name attribute" */
pre.code.http .name.attribute, code.http .name.attribute { color: black; font-weight: bold }

@ -244,10 +244,6 @@ div.viewcode-block:target {
border-bottom: 1px solid #ac9;
}
p {
margin: 0;
}
ul li dd {
margin-top: 0;
}
@ -282,3 +278,9 @@ td[colspan]:not([colspan="1"]) {
thead {
background: #eeeeee;
}
div.admonition-rationale {
background-color: #efe;
border: 1px solid #ccc;
}

@ -1,22 +1,228 @@
#! /usr/bin/env python
from argparse import ArgumentParser
from docutils.core import publish_file
import copy
import fileinput
import glob
import os
import re
import shutil
import subprocess
import sys
import yaml
os.chdir(os.path.dirname(os.path.abspath(__file__)))
stylesheets = {
"stylesheet_path": ["basic.css", "nature.css"]
"stylesheet_path": glob.glob("css/*.css"),
}
def glob_spec_to(out_file_name):
with open(out_file_name, "wb") as outfile:
for f in sorted(glob.glob("../specification/*.rst")):
with open(f, "rb") as infile:
outfile.write(infile.read())
VERBOSE = False
"""
Read a RST file and replace titles with a different title level if required.
Args:
filename: The name of the file being read (for debugging)
file_stream: The open file stream to read from.
title_level: The integer which determines the offset to *start* from.
title_styles: An array of characters detailing the right title styles to use
e.g. ["=", "-", "~", "+"]
Returns:
string: The file contents with titles adjusted.
Example:
Assume title_styles = ["=", "-", "~", "+"], title_level = 1, and the file
when read line-by-line encounters the titles "===", "---", "---", "===", "---".
This function will bump every title encountered down a sub-heading e.g.
"=" to "-" and "-" to "~" because title_level = 1, so the output would be
"---", "~~~", "~~~", "---", "~~~". There is no bumping "up" a title level.
"""
def load_with_adjusted_titles(filename, file_stream, title_level, title_styles):
rst_lines = []
title_chars = "".join(title_styles)
title_regex = re.compile("^[" + re.escape(title_chars) + "]{3,}$")
prev_line_title_level = 0 # We expect the file to start with '=' titles
file_offset = None
prev_non_title_line = None
for i, line in enumerate(file_stream, 1):
# ignore anything which isn't a title (e.g. '===============')
if not title_regex.match(line):
rst_lines.append(line)
prev_non_title_line = line
continue
# The title underline must match at a minimum the length of the title
if len(prev_non_title_line) > len(line):
rst_lines.append(line)
prev_non_title_line = line
continue
line_title_style = line[0]
line_title_level = title_styles.index(line_title_style)
# Not all files will start with "===" and we should be flexible enough
# to allow that. The first title we encounter sets the "file offset"
# which is added to the title_level desired.
if file_offset is None:
file_offset = line_title_level
if file_offset != 0:
logv((" WARNING: %s starts with a title style of '%s' but '%s' " +
"is preferable.") % (filename, line_title_style, title_styles[0]))
# Sanity checks: Make sure that this file is obeying the title levels
# specified and bail if it isn't.
# The file is allowed to go 1 deeper or any number shallower
if prev_line_title_level - line_title_level < -1:
raise Exception(
("File '%s' line '%s' has a title " +
"style '%s' which doesn't match one of the " +
"allowed title styles of %s because the " +
"title level before this line was '%s'") %
(filename, (i + 1), line_title_style, title_styles,
title_styles[prev_line_title_level])
)
prev_line_title_level = line_title_level
adjusted_level = (
title_level + line_title_level - file_offset
)
# Sanity check: Make sure we can bump down the title and we aren't at the
# lowest level already
if adjusted_level >= len(title_styles):
raise Exception(
("Files '%s' line '%s' has a sub-title level too low and it " +
"cannot be adjusted to fit. You can add another level to the " +
"'title_styles' key in targets.yaml to fix this.") %
(filename, (i + 1))
)
if adjusted_level == line_title_level:
# no changes required
rst_lines.append(line)
continue
# Adjusting line levels
logv(
"File: %s Adjusting %s to %s because file_offset=%s title_offset=%s" %
(filename, line_title_style, title_styles[adjusted_level],
file_offset, title_level)
)
rst_lines.append(line.replace(
line_title_style,
title_styles[adjusted_level]
))
return "".join(rst_lines)
def get_rst(file_info, title_level, title_styles, spec_dir, adjust_titles):
# string are file paths to RST blobs
if isinstance(file_info, basestring):
log("%s %s" % (">" * (1 + title_level), file_info))
with open(os.path.join(spec_dir, file_info), "r") as f:
rst = None
if adjust_titles:
rst = load_with_adjusted_titles(
file_info, f, title_level, title_styles
)
else:
rst = f.read()
rst += "\n\n"
return rst
# dicts look like {0: filepath, 1: filepath} where the key is the title level
elif isinstance(file_info, dict):
levels = sorted(file_info.keys())
rst = []
for l in levels:
rst.append(get_rst(file_info[l], l, title_styles, spec_dir, adjust_titles))
return "".join(rst)
# lists are multiple file paths e.g. [filepath, filepath]
elif isinstance(file_info, list):
rst = []
for f in file_info:
rst.append(get_rst(f, title_level, title_styles, spec_dir, adjust_titles))
return "".join(rst)
raise Exception(
"The following 'file' entry in this target isn't a string, list or dict. " +
"It really really should be. Entry: %s" % (file_info,)
)
def build_spec(target, out_filename):
with open(out_filename, "wb") as outfile:
for file_info in target["files"]:
section = get_rst(
file_info=file_info,
title_level=0,
title_styles=target["title_styles"],
spec_dir="../specification/",
adjust_titles=True
)
outfile.write(section)
"""
Replaces relative title styles with actual title styles.
The templating system has no idea what the right title style is when it produces
RST because it depends on the build target. As a result, it uses relative title
styles defined in targets.yaml to say "down a level, up a level, same level".
This function replaces these relative titles with actual title styles from the
array in targets.yaml.
"""
def fix_relative_titles(target, filename, out_filename):
title_styles = target["title_styles"]
relative_title_chars = [
target["relative_title_styles"]["subtitle"],
target["relative_title_styles"]["sametitle"],
target["relative_title_styles"]["supertitle"]
]
relative_title_matcher = re.compile(
"^[" + re.escape("".join(relative_title_chars)) + "]{3,}$"
)
title_matcher = re.compile(
"^[" + re.escape("".join(title_styles)) + "]{3,}$"
)
current_title_style = None
with open(filename, "r") as infile:
with open(out_filename, "w") as outfile:
for line in infile.readlines():
if not relative_title_matcher.match(line):
if title_matcher.match(line):
current_title_style = line[0]
outfile.write(line)
continue
line_char = line[0]
replacement_char = None
current_title_level = title_styles.index(current_title_style)
if line_char == target["relative_title_styles"]["subtitle"]:
if (current_title_level + 1) == len(title_styles):
raise Exception(
"Encountered sub-title line style but we can't go " +
"any lower."
)
replacement_char = title_styles[current_title_level + 1]
elif line_char == target["relative_title_styles"]["sametitle"]:
replacement_char = title_styles[current_title_level]
elif line_char == target["relative_title_styles"]["supertitle"]:
if (current_title_level - 1) < 0:
raise Exception(
"Encountered super-title line style but we can't go " +
"any higher."
)
replacement_char = title_styles[current_title_level - 1]
else:
raise Exception(
"Unknown relative line char %s" % (line_char,)
)
outfile.write(
line.replace(line_char, replacement_char)
)
def rst2html(i, o):
@ -31,18 +237,127 @@ def rst2html(i, o):
settings_overrides=stylesheets
)
def run_through_template(input):
null = open(os.devnull, 'w')
subprocess.check_output(
[
'python', 'build.py',
"-i", "matrix_templates",
"-o", "../scripts/tmp",
"../scripts/"+input
],
stderr=null,
cwd="../templating",
)
def run_through_template(input, set_verbose):
tmpfile = './tmp/output'
try:
with open(tmpfile, 'w') as out:
args = [
'python', 'build.py',
"-i", "matrix_templates",
"-o", "../scripts/tmp",
"../scripts/"+input
]
if set_verbose:
args.insert(2, "-v")
log("EXEC: %s" % " ".join(args))
log(" ==== build.py output ==== ")
print subprocess.check_output(
args,
stderr=out,
cwd="../templating"
)
except subprocess.CalledProcessError as e:
print e.output
with open(tmpfile, 'r') as f:
sys.stderr.write(f.read() + "\n")
raise
"""
Extract and resolve groups for the given target in the given targets listing.
Args:
targets_listing (str): The path to a YAML file containing a list of targets
target_name (str): The name of the target to extract from the listings.
Returns:
dict: Containing "filees" (a list of file paths), "relative_title_styles"
(a dict of relative style keyword to title character) and "title_styles"
(a list of characters which represent the global title style to follow,
with the top section title first, the second section second, and so on.)
"""
def get_build_target(targets_listing, target_name):
build_target = {
"title_styles": [],
"relative_title_styles": {},
"files": []
}
with open(targets_listing, "r") as targ_file:
all_targets = yaml.load(targ_file.read())
build_target["title_styles"] = all_targets["title_styles"]
build_target["relative_title_styles"] = all_targets["relative_title_styles"]
target = all_targets["targets"].get(target_name)
if not target:
raise Exception(
"No target by the name '" + target_name + "' exists in '" +
targets_listing + "'."
)
if not isinstance(target.get("files"), list):
raise Exception(
"Found target but 'files' key is not a list."
)
def get_group(group_id, depth):
group_name = group_id[len("group:"):]
group = all_targets.get("groups", {}).get(group_name)
if not group:
raise Exception(
"Tried to find group '%s' but it doesn't exist." % group_name
)
if not isinstance(group, list):
raise Exception(
"Expected group '%s' to be a list but it isn't." % group_name
)
# deep copy so changes to depths don't contaminate multiple uses of this group
group = copy.deepcopy(group)
# swap relative depths for absolute ones
for i, entry in enumerate(group):
if isinstance(entry, dict):
group[i] = {
(rel_depth + depth): v for (rel_depth, v) in entry.items()
}
return group
resolved_files = []
for file_entry in target["files"]:
# file_entry is a group id
if isinstance(file_entry, basestring) and file_entry.startswith("group:"):
group = get_group(file_entry, 0)
# The group may be resolved to a list of file entries, in which case
# we want to extend the array to insert each of them rather than
# insert the entire list as a single element (which is what append does)
if isinstance(group, list):
resolved_files.extend(group)
else:
resolved_files.append(group)
# file_entry is a dict which has more file entries as values
elif isinstance(file_entry, dict):
resolved_entry = {}
for (depth, entry) in file_entry.iteritems():
if not isinstance(entry, basestring):
raise Exception(
"Double-nested depths are not supported. Entry: %s" % (file_entry,)
)
if entry.startswith("group:"):
resolved_entry[depth] = get_group(entry, depth)
else:
# map across without editing (e.g. normal file path)
resolved_entry[depth] = entry
resolved_files.append(resolved_entry)
continue
# file_entry is just a plain ol' file path
else:
resolved_files.append(file_entry)
build_target["files"] = resolved_files
return build_target
def log(line):
print "gendoc: %s" % line
def logv(line):
if VERBOSE:
print "gendoc:V: %s" % line
def prepare_env():
try:
@ -53,30 +368,49 @@ def prepare_env():
os.makedirs("./tmp")
except OSError:
pass
def cleanup_env():
shutil.rmtree("./tmp")
def main():
def main(target_name, keep_intermediates):
prepare_env()
glob_spec_to("tmp/full_spec.rst")
run_through_template("tmp/full_spec.rst")
log("Building spec [target=%s]" % target_name)
target = get_build_target("../specification/targets.yaml", target_name)
build_spec(target=target, out_filename="tmp/templated_spec.rst")
run_through_template("tmp/templated_spec.rst", VERBOSE)
fix_relative_titles(
target=target, filename="tmp/templated_spec.rst",
out_filename="tmp/full_spec.rst"
)
shutil.copy("../supporting-docs/howtos/client-server.rst", "tmp/howto.rst")
run_through_template("tmp/howto.rst")
run_through_template("tmp/howto.rst", False) # too spammy to mark -v on this
rst2html("tmp/full_spec.rst", "gen/specification.html")
rst2html("tmp/howto.rst", "gen/howtos.html")
cleanup_env()
if not keep_intermediates:
cleanup_env()
if __name__ == '__main__':
if len(sys.argv) > 1:
# we accept no args, so they don't know what they're doing!
print "gendoc.py - Generate the Matrix specification as HTML."
print "Usage:"
print " python gendoc.py"
print ""
print "The specification can then be found in the gen/ folder."
print ""
print "Requirements:"
print " - This script requires Jinja2 and rst2html (docutils)."
sys.exit(0)
main()
parser = ArgumentParser(
"gendoc.py - Generate the Matrix specification as HTML to the gen/ folder."
)
parser.add_argument(
"--nodelete", "-n", action="store_true",
help="Do not delete intermediate files. They will be found in tmp/"
)
parser.add_argument(
"--target", "-t", default="main",
help="Specify the build target to build from specification/targets.yaml"
)
parser.add_argument(
"--verbose", "-v", action="store_true",
help="Turn on verbose mode."
)
args = parser.parse_args()
if not args.target:
parser.print_help()
sys.exit(1)
VERBOSE = args.verbose
main(args.target, args.nodelete)

@ -1,21 +1,63 @@
#! /bin/bash
if [ -z "$1" ]; then
echo "Expected /includes/nav.html file as arg."
echo "Expected /includes/head.html file as 1st arg."
exit 1
fi
NAV_BAR=$1
if [ -z "$2" ]; then
echo "Expected /includes/nav.html file as 2nd arg."
exit 1
fi
if [ -z "$3" ]; then
echo "Expected /includes/footer.html file as 3rd arg."
exit 1
fi
HEADER=$1
NAV_BAR=$2
FOOTER=$3
if [ ! -f $HEADER ]; then
echo $HEADER " does not exist"
exit 1
fi
if [ ! -f $NAV_BAR ]; then
echo $NAV_BAR " does not exist"
exit 1
fi
if [ ! -f $FOOTER ]; then
echo $FOOTER " does not exist"
exit 1
fi
python gendoc.py
perl -pi -e 's#<head>#<head><link rel="stylesheet" href="/site.css">#' gen/specification.html gen/howtos.html
perl -MFile::Slurp -pi -e 'BEGIN { $header = read_file("'$HEADER'") } s#<head>#<head>$header
<link rel="stylesheet" href="//matrix.org/docs/guides/css/docs_overrides.css">
#' gen/specification.html gen/howtos.html
perl -MFile::Slurp -pi -e 'BEGIN { $nav = read_file("'$NAV_BAR'") } s#<body>#<body><div id="header"><div id="headerContent">$nav</div></div><div id="page"><div id="wrapper"><div style="text-align: center; padding: 40px;"><a href="/"><img src="/matrix.png" width="305" height="130" alt="[matrix]"/></a></div>#' gen/specification.html gen/howtos.html
perl -MFile::Slurp -pi -e 'BEGIN { $nav = read_file("'$NAV_BAR'") } s#<body># <body class="blog et_fixed_nav et_cover_background et_right_sidebar">
<div id="page-wrapper">
<div class="page-content" id="page-container">
$nav
<div id="main-content">
<div class="wrapper" id="wrapper">
<div class="document_foo" id="document">
#' gen/specification.html gen/howtos.html
perl -pi -e 's#</body>#</div></div><div id="footer"><div id="footerContent">&copy 2014-2015 Matrix.org</div></div></body>#' gen/specification.html gen/howtos.html
perl -MFile::Slurp -pi -e 'BEGIN { $footer = read_file("'$FOOTER'") } s#</body>#
</div>
</div>
<div class="push">
</div>
</div>
</div>
$footer
</div>
</div>
</body>#' gen/specification.html gen/howtos.html

@ -0,0 +1,18 @@
speculator allows you to preview pull requests to the matrix.org specification.
It serves the following HTTP endpoints:
- / lists open pull requests
- /spec/123 which renders the spec as html at pull request 123.
- /diff/rst/123 which gives a diff of the spec's rst at pull request 123.
- /diff/html/123 which gives a diff of the spec's HTML at pull request 123.
The build or run, you need a working `go` installation.
Then fetch dependencies:
` go get github.com/hashicorp/golang-lru`
To run it, then run:
`go run main.go`
To build the binary (which is necessary for deployment to the matrix.org
servers), you must again install `go` and dependencies, and then run:
`go build`

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save