Merge branch 'master' of github.com:matrix-org/matrix-doc
commit
10f3e7e84f
@ -1,5 +1,8 @@
|
||||
scripts/gen
|
||||
scripts/continuserv/continuserv
|
||||
scripts/speculator/speculator
|
||||
templating/out
|
||||
*.pyc
|
||||
supporting-docs/_site
|
||||
supporting-docs/.sass-cache
|
||||
api/node_modules
|
||||
|
@ -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 %r" % (
|
||||
request, code
|
||||
), 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 @@
|
||||
v1-event-schema/core-event-schema
|
@ -1,7 +0,0 @@
|
||||
type: object
|
||||
description: A Matrix Event
|
||||
properties:
|
||||
event_id:
|
||||
type: string
|
||||
description: An event ID.
|
||||
required: ["event_id"]
|
@ -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,147 @@
|
||||
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: |-
|
||||
{
|
||||
"username": "cheeky_monkey",
|
||||
"password": "ilovebananas"
|
||||
}
|
||||
properties:
|
||||
username:
|
||||
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: ["username", "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.
|
||||
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,128 @@
|
||||
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}
|
||||
|
||||
"/rooms/{roomId}/invite":
|
||||
post:
|
||||
summary: Invite a user to participate in a particular room.
|
||||
description: |-
|
||||
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.
|
||||
|
||||
This serves two purposes; firstly, to notify the user that the room
|
||||
exists (and that their presence is requested). Secondly, some rooms can
|
||||
only be joined if a user is invited to join it; sending the invite gives
|
||||
that user permission to join the room.
|
||||
|
||||
Only users currently in a particular room can invite other users to
|
||||
join that 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: user_id
|
||||
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 # empty json 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,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,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 @@
|
||||
.
|
@ -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"]
|
||||
},
|
||||
"emphemeral": {
|
||||
"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"]
|
||||
},
|
||||
"emphemeral": {
|
||||
"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,68 @@
|
||||
swagger: '2.0'
|
||||
info:
|
||||
title: "Matrix Client-Server v2 Receipts API"
|
||||
version: "1.0.0"
|
||||
host: localhost:8008
|
||||
schemes:
|
||||
- https
|
||||
- http
|
||||
basePath: /_matrix/client/v2_alpha
|
||||
consumes:
|
||||
- application/json
|
||||
produces:
|
||||
- application/json
|
||||
securityDefinitions:
|
||||
accessToken:
|
||||
type: apiKey
|
||||
description: The user_id or application service access_token
|
||||
name: access_token
|
||||
in: query
|
||||
paths:
|
||||
"/rooms/{roomId}/receipt/{receiptType}/{eventId}":
|
||||
post:
|
||||
summary: Send a receipt for the given event ID.
|
||||
description: |-
|
||||
This API updates the marker for the given receipt type to the event ID
|
||||
specified.
|
||||
security:
|
||||
- accessToken: []
|
||||
parameters:
|
||||
- in: path
|
||||
type: string
|
||||
name: roomId
|
||||
description: The room in which to send the event.
|
||||
required: true
|
||||
x-example: "!wefuh21ffskfuh345:example.com"
|
||||
- in: path
|
||||
type: string
|
||||
name: receiptType
|
||||
description: The type of receipt to send.
|
||||
required: true
|
||||
x-example: "m.read"
|
||||
enum: ["m.read"]
|
||||
- in: path
|
||||
type: string
|
||||
name: eventId
|
||||
description: The event ID to acknowledge up to.
|
||||
required: true
|
||||
x-example: "$1924376522eioj:example.com"
|
||||
- in: body
|
||||
description: |-
|
||||
Extra receipt information to attach to ``content`` if any. The
|
||||
server will automatically set the ``ts`` field.
|
||||
schema:
|
||||
type: object
|
||||
example: |-
|
||||
{}
|
||||
responses:
|
||||
200:
|
||||
description: The receipt was sent.
|
||||
examples:
|
||||
application/json: |-
|
||||
{}
|
||||
schema:
|
||||
type: object # empty json object
|
||||
429:
|
||||
description: This request was rate-limited.
|
||||
schema:
|
||||
"$ref": "definitions/error.yaml"
|
@ -0,0 +1,257 @@
|
||||
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.
|
||||
Client's 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: 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:
|
||||
type: object
|
||||
description: |-
|
||||
Updates to rooms.
|
||||
properties:
|
||||
joined:
|
||||
type: object
|
||||
additionalProperties:
|
||||
type: object
|
||||
properties:
|
||||
event_map:
|
||||
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:
|
||||
description: An event object.
|
||||
type: object
|
||||
state:
|
||||
description: |-
|
||||
The state updates for the room.
|
||||
allOf:
|
||||
- $ref: "definitions/room_event_batch.json"
|
||||
timeline:
|
||||
description: |-
|
||||
The timeline of messages and state changes in the
|
||||
room.
|
||||
allOf:
|
||||
- $ref: "definitions/timeline_batch.json"
|
||||
ephemeral:
|
||||
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:
|
||||
type: object
|
||||
description: |-
|
||||
The rooms that the user has been invited to.
|
||||
additionalProperties:
|
||||
type: object
|
||||
properties:
|
||||
invite_state:
|
||||
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:
|
||||
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:
|
||||
type: object
|
||||
properties:
|
||||
event_map:
|
||||
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:
|
||||
description: An event object.
|
||||
type: object
|
||||
state:
|
||||
description: |-
|
||||
The state updates for the room up to the point when
|
||||
the user left.
|
||||
allOf:
|
||||
- $ref: "definitions/room_event_batch.json"
|
||||
timeline:
|
||||
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:
|
||||
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"},
|
||||
"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": {}
|
||||
}
|
||||
}
|
@ -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. |
|
||||
+-------------+--------------------------------------------------+------------------------------------------------------------------------------------------------+
|
@ -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,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 @@
|
||||
.
|
@ -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."
|
||||
},
|
||||
"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"]
|
||||
}
|
@ -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"]
|
||||
}
|
@ -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 @@
|
||||
.
|
@ -0,0 +1,9 @@
|
||||
#! /bin/bash
|
||||
|
||||
set -ex
|
||||
|
||||
(cd event-schemas/ && ./check_examples.py)
|
||||
(cd api && ./check_examples.py)
|
||||
(cd scripts && ./gendoc.py)
|
||||
(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 @@
|
||||
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 .literal.number, code .literal.number { color: blue }
|
||||
pre.code .name.tag, code .name.tag { color: darkgreen }
|
||||
pre.code .literal.string, code .literal.string { color: darkblue }
|
@ -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,10 @@
|
||||
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.
|
||||
|
||||
To run it, you must install the `go` tool, and run:
|
||||
`go run main.go`
|
@ -0,0 +1,564 @@
|
||||
#!/usr/bin/perl
|
||||
#
|
||||
# htmldiff - present a diff marked version of two html documents
|
||||
#
|
||||
# Copyright (c) 1998-2006 MACS, Inc.
|
||||
#
|
||||
# Copyright (c) 2007 SiSco, Inc.
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining
|
||||
# a copy of this software and associated documentation files (the
|
||||
# "Software"), to deal in the Software without restriction, including
|
||||
# without limitation the rights to use, copy, modify, merge, publish,
|
||||
# distribute, sublicense, and/or sell copies of the Software, and to
|
||||
# permit persons to whom the Software is furnished to do so, subject to
|
||||
# the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be
|
||||
# included in all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
#
|
||||
# See http://www.themacs.com for more information.
|
||||
#
|
||||
# usage: htmldiff [[-c] [-l] [-o] oldversion newversion [output]]
|
||||
#
|
||||
# -c - disable metahtml comment processing
|
||||
# -o - disable outputting of old text
|
||||
# -l - use navindex to create sequence of diffs
|
||||
# oldversion - the previous version of the document
|
||||
# newversion - the newer version of the document
|
||||
# output - a filename to place the output in. If omitted, the output goes to
|
||||
# standard output.
|
||||
#
|
||||
# if invoked with no options or arguments, operates as a CGI script. It then
|
||||
# takes the following parameters:
|
||||
#
|
||||
# oldfile - the URL of the original file
|
||||
# newfile - the URL of the new file
|
||||
# mhtml - a flag to indicate whether it should be aware of MetaHTML comments.
|
||||
#
|
||||
# requires GNU diff utility
|
||||
# also requires the perl modules Getopt::Std
|
||||
#
|
||||
# NOTE: The markup created by htmldiff may not validate against the HTML 4.0
|
||||
# DTD. This is because the algorithm is realtively simple, and there are
|
||||
# places in the markup content model where the span element is not allowed.
|
||||
# Htmldiff is NOT aware of these places.
|
||||
#
|
||||
# $Source: /u/sources/public/2009/htmldiff/htmldiff.pl,v $
|
||||
# $Revision: 1.1 $
|
||||
#
|
||||
# $Log: htmldiff.pl,v $
|
||||
# Revision 1.1 2014/01/06 08:04:51 dom
|
||||
# added copy of htmldiff perl script since aptest.com repo no longer available
|
||||
#
|
||||
# Revision 1.5 2008/03/05 13:23:16 ahby
|
||||
# Fixed a problem with leading whitespace before markup.
|
||||
#
|
||||
# Revision 1.4 2007/12/13 13:09:16 ahby
|
||||
# Updated copyright and license.
|
||||
#
|
||||
# Revision 1.3 2007/12/13 12:53:34 ahby
|
||||
# Changed use of span to ins and del
|
||||
#
|
||||
# Revision 1.2 2002/02/13 16:27:23 ahby
|
||||
# Changed processing model.
|
||||
# Improved handling of old text and changed styles.
|
||||
#
|
||||
# Revision 1.1 2000/07/12 12:20:04 ahby
|
||||
# Updated to remove empty spans - this fixes validation problems under
|
||||
# strict.
|
||||
#
|
||||
# Revision 1.11 1999/12/08 19:46:45 ahby
|
||||
# Fixed validation errors introduced by placing markup where it didn't
|
||||
# belong.
|
||||
#
|
||||
# Revision 1.10 1999/10/18 13:42:58 ahby
|
||||
# Added -o to the usage message.
|
||||
#
|
||||
# Revision 1.9 1999/05/04 12:29:11 ahby
|
||||
# Added an option to turn off the display of old text.
|
||||
#
|
||||
# Revision 1.8 1999/04/09 14:37:27 ahby
|
||||
# Fixed a perl syntax error.
|
||||
#
|
||||
# Revision 1.7 1999/04/09 14:35:49 ahby
|
||||
# Added reference to MACS homepage.
|
||||
#
|
||||
# Revision 1.6 1999/04/09 14:35:09 ahby
|
||||
# Added comment about validity of generated markup.
|
||||
#
|
||||
# Revision 1.5 1999/02/22 22:17:54 ahby
|
||||
# Changed to use stylesheets.
|
||||
# Changed to rely upon span.
|
||||
# Changed to work around content model problems.
|
||||
#
|
||||
# Revision 1.4 1999/02/08 02:32:22 ahby
|
||||
# Added a copyright statement.
|
||||
#
|
||||
# Revision 1.3 1999/02/08 02:30:40 ahby
|
||||
# Added header processing.
|
||||
#
|
||||
# Revision 1.2 1998/12/10 17:31:31 ahby
|
||||
# Fixed to escape less-thans in change blocks and to not permit change
|
||||
# markup within specific elements (like TITLE).
|
||||
#
|
||||
# Revision 1.1 1998/11/26 00:09:22 ahby
|
||||
# Initial revision
|
||||
#
|
||||
#
|
||||
|
||||
use Getopt::Std;
|
||||
|
||||
sub usage {
|
||||
print STDERR "htmldiff [-c] [-o] oldversion newversion [output]\n";
|
||||
exit;
|
||||
}
|
||||
|
||||
sub url_encode {
|
||||
my $str = shift;
|
||||
$str =~ s/([\x00-\x1f\x7F-\xFF])/
|
||||
sprintf ('%%%02x', ord ($1))/eg;
|
||||
return $str;
|
||||
}
|
||||
|
||||
# markit - diff-mark the streams
|
||||
#
|
||||
# markit(file1, file2)
|
||||
#
|
||||
# markit relies upon GNUdiff to mark up the text.
|
||||
#
|
||||
# The markup is encoded using special control sequences:
|
||||
#
|
||||
# a block wrapped in control-a is deleted text
|
||||
# a block wrapped in control-b is old text
|
||||
# a block wrapped in control-c is new text
|
||||
#
|
||||
# The main processing loop attempts to wrap the text blocks in appropriate
|
||||
# SPANs based upon the type of text that it is.
|
||||
#
|
||||
# When the loop encounters a < in the text, it stops the span. Then it outputs
|
||||
# the element that is defined, then it restarts the span.
|
||||
|
||||
sub markit {
|
||||
my $retval = "";
|
||||
my($file1) = shift;
|
||||
my($file2) = shift;
|
||||
# my $old="<span class=\\\"diff-old-a\\\">deleted text: </span>%c'\012'%c'\001'%c'\012'%<%c'\012'%c'\001'%c'\012'";
|
||||
my $old="%c'\012'%c'\001'%c'\012'%<%c'\012'%c'\001'%c'\012'";
|
||||
my $new="%c'\012'%c'\003'%c'\012'%>%c'\012'%c'\003'%c'\012'";
|
||||
my $unchanged="%=";
|
||||
my $changed="%c'\012'%c'\001'%c'\012'%<%c'\012'%c'\001'%c'\012'%c'\004'%c'\012'%>%c'\012'%c'\004'%c'\012'";
|
||||
if ($opt_o) {
|
||||
$old = "";
|
||||
$changed = "%c'\012'%c'\004'%c'\012'%>%c'\012'%c'\004'%c'\012'";
|
||||
}
|
||||
# my $old="%c'\002'<font color=\\\"purple\\\" size=\\\"-2\\\">deleted text:</font><s>%c'\012'%c'\001'%c'\012'%<%c'\012'%c'\001'%c'\012'</s>%c'\012'%c'\002'";
|
||||
# my $new="%c'\002'<font color=\\\"purple\\\"><u>%c'\012'%c'\002'%>%c'\002'</u></font>%c'\002'%c'\012'";
|
||||
# my $unchanged="%=";
|
||||
# my $changed="%c'\002'<s>%c'\012'%c'\001'%c'\012'%<%c'\012'%c'\001'%c'\012'</s><font color=\\\"purple\\\"><u>%c'\002'%c'\012'%>%c'\012'%c'\002'</u></font>%c'\002'%c'\012'";
|
||||
|
||||
my @span;
|
||||
$span[0]="</span>";
|
||||
$span[1]="<del class=\"diff-old\">";
|
||||
$span[2]="<del class=\"diff-old\">";
|
||||
$span[3]="<ins class=\"diff-new\">";
|
||||
$span[4]="<ins class=\"diff-chg\">";
|
||||
|
||||
my @diffEnd ;
|
||||
$diffEnd[1] = '</del>';
|
||||
$diffEnd[2] = '</del>';
|
||||
$diffEnd[3] = '</ins>';
|
||||
$diffEnd[4] = '</ins>';
|
||||
|
||||
my $diffcounter = 0;
|
||||
|
||||
open(FILE, qq(diff -d --old-group-format="$old" --new-group-format="$new" --changed-group-format="$changed" --unchanged-group-format="$unchanged" $file1 $file2 |)) || die("Diff failed: $!");
|
||||
# system (qq(diff --old-group-format="$old" --new-group-format="$new" --changed-group-format="$changed" --unchanged-group-format="$unchanged" $file1 $file2 > /tmp/output));
|
||||
|
||||
my $state = 0;
|
||||
my $inblock = 0;
|
||||
my $temp = "";
|
||||
my $lineCount = 0;
|
||||
|
||||
# strategy:
|
||||
#
|
||||
# process the output of diff...
|
||||
#
|
||||
# a link with control A-D means the start/end of the corresponding ordinal
|
||||
# state (1-4). Resting state is state 0.
|
||||
#
|
||||
# While in a state, accumulate the contents for that state. When exiting the
|
||||
# state, determine if it is appropriate to emit the contents with markup or
|
||||
# not (basically, if the accumulated buffer contains only empty lines or lines
|
||||
# with markup, then we don't want to emit the wrappers. We don't need them.
|
||||
#
|
||||
# Note that if there is markup in the "old" block, that markup is silently
|
||||
# removed. It isn't really that interesting, and it messes up the output
|
||||
# something fierce.
|
||||
|
||||
while (<FILE>) {
|
||||
my $anchor = $opt_l ? qq[<a tabindex="$diffcounter">] : "" ;
|
||||
my $anchorEnd = $opt_l ? q[</a>] : "" ;
|
||||
$lineCount ++;
|
||||
if ($state == 0) { # if we are resting and we find a marker,
|
||||
# then we must be entering a block
|
||||
if (m/^([\001-\004])/) {
|
||||
$state = ord($1);
|
||||
$_ = "";
|
||||
}
|
||||
# if (m/^\001/) {
|
||||
# $state = 1;
|
||||
# s/^/$span[1]/;
|
||||
# } elsif (m/^\002/) {
|
||||
# $state = 2;
|
||||
# s/^/$span[2]/;
|
||||
# } elsif (m/^\003/) {
|
||||
# $state = 3;
|
||||
# s/^/$span[3]/;
|
||||
# } elsif (m/^\004/) {
|
||||
# $state = 4;
|
||||
# s/^/$span[4]/;
|
||||
# }
|
||||
} else {
|
||||
# if we are in "old" state, remove markup
|
||||
if (($state == 1) || ($state == 2)) {
|
||||
s/\<.*\>//; # get rid of any old markup
|
||||
s/\</</g; # escape any remaining STAG or ETAGs
|
||||
s/\>/>/g;
|
||||
}
|
||||
# if we found another marker, we must be exiting the state
|
||||
if (m/^([\001-\004])/) {
|
||||
if ($temp ne "") {
|
||||
$_ = $span[$state] . $anchor . $temp . $anchorEnd . $diffEnd[$state] . "\n";
|
||||
$temp = "";
|
||||
} else {
|
||||
$_ = "" ;
|
||||
}
|
||||
$state = 0;
|
||||
} elsif (m/^\s*\</) { # otherwise, is this line markup?
|
||||
# if it is markup AND we haven't seen anything else yet,
|
||||
# then we will emit the markup
|
||||
if ($temp eq "") {
|
||||
$retval .= $_;
|
||||
$_ = "";
|
||||
} else { # we wrap it with the state switches and hold it
|
||||
s/^/$anchorEnd$diffEnd[$state]/;
|
||||
s/$/$span[$state]$anchor/;
|
||||
$temp .= $_;
|
||||
$_ = "";
|
||||
}
|
||||
} else {
|
||||
if (m/.+/) {
|
||||
$temp .= $_;
|
||||
$_ = "";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
s/\001//g;
|
||||
s/\002//g;
|
||||
s/\003//g;
|
||||
s/\004//g;
|
||||
if ($_ !~ m/^$/) {
|
||||
$retval .= $_;
|
||||
}
|
||||
$diffcounter++;
|
||||
}
|
||||
close FILE;
|
||||
$retval =~ s/$span[1]\n+$diffEnd[1]//g;
|
||||
$retval =~ s/$span[2]\n+$diffEnd[2]//g;
|
||||
$retval =~ s/$span[3]\n+$diffEnd[3]//g;
|
||||
$retval =~ s/$span[4]\n+$diffEnd[4]//g;
|
||||
$retval =~ s/$span[1]\n*$//g;
|
||||
$retval =~ s/$span[2]\n*$//g;
|
||||
$retval =~ s/$span[3]\n*$//g;
|
||||
$retval =~ s/$span[4]\n*$//g;
|
||||
return $retval;
|
||||
}
|
||||
|
||||
sub splitit {
|
||||
my $filename = shift;
|
||||
my $headertmp = shift;
|
||||
my $inheader=0;
|
||||
my $preformatted=0;
|
||||
my $inelement=0;
|
||||
my $retval = "";
|
||||
my $styles = q(<style type='text/css'>
|
||||
.diff-old-a {
|
||||
font-size: smaller;
|
||||
color: red;
|
||||
}
|
||||
|
||||
.diff-new { background-color: yellow; }
|
||||
.diff-chg { background-color: lime; }
|
||||
.diff-new:before,
|
||||
.diff-new:after
|
||||
{ content: "\2191" }
|
||||
.diff-chg:before, .diff-chg:after
|
||||
{ content: "\2195" }
|
||||
.diff-old { text-decoration: line-through; background-color: #FBB; }
|
||||
.diff-old:before,
|
||||
.diff-old:after
|
||||
{ content: "\2193" }
|
||||
:focus { border: thin red solid}
|
||||
</style>
|
||||
);
|
||||
if ($opt_t) {
|
||||
$styles .= q(
|
||||
<script type="text/javascript">
|
||||
<!--
|
||||
function setOldDisplay() {
|
||||
for ( var s = 0; s < document.styleSheets.length; s++ ) {
|
||||
var css = document.styleSheets[s];
|
||||
var mydata ;
|
||||
try { mydata = css.cssRules ;
|
||||
if ( ! mydata ) mydata = css.rules;
|
||||
for ( var r = 0; r < mydata.length; r++ ) {
|
||||
if ( mydata[r].selectorText == '.diff-old' ) {
|
||||
mydata[r].style.display = ( mydata[r].style.display == '' ) ? 'none'
|
||||
: '';
|
||||
return;
|
||||
}
|
||||
}
|
||||
} catch(e) {} ;
|
||||
}
|
||||
}
|
||||
-->
|
||||
</script>
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
if ($stripheader) {
|
||||
open(HEADER, ">$headertmp");
|
||||
}
|
||||
|
||||
my $incomment = 0;
|
||||
my $inhead = 1;
|
||||
open(FILE, $filename) || die("File $filename cannot be opened: $!");
|
||||
while (<FILE>) {
|
||||
if ($inhead == 1) {
|
||||
if (m/\<\/head/i) {
|
||||
print HEADER $styles;
|
||||
}
|
||||
if (m/\<body/i) {
|
||||
$inhead = 0;
|
||||
print HEADER;
|
||||
if ($opt_t) {
|
||||
print HEADER q(
|
||||
<form action=""><input type="button" onclick="setOldDisplay()" value="Show/Hide Old Content" /></form>
|
||||
);
|
||||
}
|
||||
close HEADER;
|
||||
} else {
|
||||
print HEADER;
|
||||
}
|
||||
} else {
|
||||
if ($incomment) {
|
||||
if (m;-->;) {
|
||||
$incomment = 0;
|
||||
s/.*-->//;
|
||||
} else {
|
||||
next;
|
||||
}
|
||||
}
|
||||
if (m;<!--;) {
|
||||
while (m;<!--.*-->;) {
|
||||
s/<!--.*?-->//;
|
||||
}
|
||||
if (m;<!--; ) {
|
||||
$incomment = 1;
|
||||
s/<!--.*//;
|
||||
}
|
||||
}
|
||||
if (m/\<pre/i) {
|
||||
$preformatted = 1;
|
||||
}
|
||||
if (m/\<\/pre\>/i) {
|
||||
$preformatted = 0;
|
||||
}
|
||||
if ($preformatted) {
|
||||
$retval .= $_;
|
||||
} elsif ($mhtmlcomments && /^;;;/) {
|
||||
$retval .= $_;
|
||||
} else {
|
||||
my @list = split(' ');
|
||||
foreach $element (@list) {
|
||||
if ($element =~ m/\<H[1-6]/i) {
|
||||
# $inheader = 1;
|
||||
}
|
||||
if ($inheader == 0) {
|
||||
$element =~ s/</\n</g;
|
||||
$element =~ s/^\n//;
|
||||
$element =~ s/>/>\n/g;
|
||||
$element =~ s/\n$//;
|
||||
$element =~ s/>\n([.,:!]+)/>$1/g;
|
||||
}
|
||||
if ($element =~ m/\<\/H[1-6]\>/i) {
|
||||
$inheader = 0;
|
||||
}
|
||||
$retval .= "$element";
|
||||
$inelement += ($element =~ s/</</g);
|
||||
$inelement -= ($element =~ s/>/>/g);
|
||||
if ($inelement < 0) {
|
||||
$inelement = 0;
|
||||
}
|
||||
if (($inelement == 0) && ($inheader == 0)) {
|
||||
$retval .= "\n";
|
||||
} else {
|
||||
$retval .= " ";
|
||||
}
|
||||
}
|
||||
undef @list;
|
||||
}
|
||||
}
|
||||
}
|
||||
$retval .= "\n";
|
||||
close FILE;
|
||||
return $retval;
|
||||
}
|
||||
|
||||
$mhtmlcomments = 1;
|
||||
|
||||
sub cli {
|
||||
getopts("clto") || usage();
|
||||
|
||||
if ($opt_c) {$mhtmlcomments = 0;}
|
||||
|
||||
if (@ARGV < 2) { usage(); }
|
||||
|
||||
$file1 = $ARGV[0];
|
||||
$file2 = $ARGV[1];
|
||||
$file3 = $ARGV[2];
|
||||
|
||||
$tmp = splitit($file1, $headertmp1);
|
||||
open (FILE, ">$tmp1");
|
||||
print FILE $tmp;
|
||||
close FILE;
|
||||
|
||||
$tmp = splitit($file2, $headertmp2);
|
||||
open (FILE, ">$tmp2");
|
||||
print FILE $tmp;
|
||||
close FILE;
|
||||
|
||||
$output = "";
|
||||
|
||||
if ($stripheader) {
|
||||
open(FILE, $headertmp2);
|
||||
while (<FILE>) {
|
||||
$output .= $_;
|
||||
}
|
||||
close(FILE);
|
||||
}
|
||||
|
||||
$output .= markit($tmp1, $tmp2);
|
||||
|
||||
if ($file3) {
|
||||
open(FILE, ">$file3");
|
||||
print FILE $output;
|
||||
close FILE;
|
||||
} else {
|
||||
print $output;
|
||||
}
|
||||
}
|
||||
|
||||
sub cgi {
|
||||
# use LWP::UserAgent;
|
||||
# use CGI;
|
||||
|
||||
my $query = new CGI;
|
||||
my $url1 = $query->param("oldfile");
|
||||
my $url2 = $query->param("newfile");
|
||||
my $mhtml = $query->param("mhtml");
|
||||
|
||||
my $file1 = "/tmp/htdcgi1.$$";
|
||||
my $file2 = "/tmp/htdcgi2.$$";
|
||||
|
||||
my $ua = new LWP::UserAgent;
|
||||
$ua->agent("MACS, Inc. HTMLdiff/0.9 " . $ua->agent);
|
||||
|
||||
# Create a request
|
||||
|
||||
my $req1 = new HTTP::Request GET => $url1;
|
||||
|
||||
my $res1 = $ua->request($req1, $file1);
|
||||
if ($res1->is_error) {
|
||||
print $res1->error_as_HTML();
|
||||
print "<p>The URL $url1 could not be found. Please check it and try again.</p>";
|
||||
return;
|
||||
}
|
||||
|
||||
my $req2 = new HTTP::Request GET => $url2;
|
||||
|
||||
my $res2 = $ua->request($req2, $file2);
|
||||
if ($res2->is_error) {
|
||||
print $res2->error_as_HTML();
|
||||
print "<p>The URL $url2 could not be found. Please check it and try again.</p>";
|
||||
return;
|
||||
}
|
||||
|
||||
$split1 = splitit($file1, $headertmp1);
|
||||
open (FILE, ">$tmp1");
|
||||
print FILE $split1;
|
||||
close FILE;
|
||||
|
||||
$split2 = splitit($file2, $headertmp2);
|
||||
open (FILE, ">$tmp2");
|
||||
print FILE $split2;
|
||||
close FILE;
|
||||
|
||||
$output = "";
|
||||
|
||||
if ($stripheader) {
|
||||
open(FILE, $headertmp2);
|
||||
while (<FILE>) {
|
||||
$output .= $_;
|
||||
}
|
||||
close(FILE);
|
||||
}
|
||||
|
||||
$output .= markit($tmp1, $tmp2);
|
||||
|
||||
my $base=$res2->base;
|
||||
|
||||
if ($base !~ /\/$/) {
|
||||
$base =~ s/[^\/]*$//;
|
||||
}
|
||||
|
||||
if ( $output !~ /<base/i ) {
|
||||
$output =~ s/<head>/<head>\n<base href="$base">/i ||
|
||||
$output =~ s/<html>/<html>\n<base href="$base">/i ;
|
||||
}
|
||||
|
||||
print $query->header(-type=>'text/html',-nph=>1);
|
||||
print $output;
|
||||
|
||||
unlink $file1;
|
||||
unlink $file2;
|
||||
|
||||
}
|
||||
|
||||
$tmp1="/tmp/htdtmp1.$$";
|
||||
$headertmp1="/tmp/htdhtmp1.$$";
|
||||
$tmp2="/tmp/htdtmp2.$$";
|
||||
$headertmp2="/tmp/htdhtmp2.$$";
|
||||
$stripheader = 1;
|
||||
|
||||
if (@ARGV == 0) {
|
||||
cgi(); # if no arguments, we must be operating as a cgi script
|
||||
} else {
|
||||
cli(); # if there are arguments, then we are operating as a CLI
|
||||
}
|
||||
|
||||
unlink $tmp1;
|
||||
unlink $headertmp1;
|
||||
unlink $tmp2;
|
||||
unlink $headertmp2;
|
@ -0,0 +1,374 @@
|
||||
// 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.
|
||||
// It is currently woefully inefficient, and there is a lot of low hanging fruit for improvement.
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"math/rand"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path"
|
||||
"strconv"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
)
|
||||
|
||||
type PullRequest struct {
|
||||
Number int
|
||||
Base Commit
|
||||
Head Commit
|
||||
Title string
|
||||
User User
|
||||
HTMLURL string `json:"html_url"`
|
||||
}
|
||||
|
||||
type Commit struct {
|
||||
SHA string
|
||||
Repo RequestRepo
|
||||
}
|
||||
|
||||
type RequestRepo struct {
|
||||
CloneURL string `json:"clone_url"`
|
||||
}
|
||||
|
||||
type User struct {
|
||||
Login string
|
||||
HTMLURL string `json:"html_url"`
|
||||
}
|
||||
|
||||
var (
|
||||
port = flag.Int("port", 9000, "Port on which to listen for HTTP")
|
||||
allowedMembers map[string]bool
|
||||
)
|
||||
|
||||
func (u *User) IsTrusted() bool {
|
||||
return allowedMembers[u.Login]
|
||||
}
|
||||
|
||||
const (
|
||||
pullsPrefix = "https://api.github.com/repos/matrix-org/matrix-doc/pulls"
|
||||
matrixDocCloneURL = "https://github.com/matrix-org/matrix-doc.git"
|
||||
)
|
||||
|
||||
func gitClone(url string, shared bool) (string, error) {
|
||||
directory := path.Join("/tmp/matrix-doc", strconv.FormatInt(rand.Int63(), 10))
|
||||
cmd := exec.Command("git", "clone", url, directory)
|
||||
if shared {
|
||||
cmd.Args = append(cmd.Args, "--shared")
|
||||
}
|
||||
|
||||
err := cmd.Run()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error cloning repo: %v", err)
|
||||
}
|
||||
return directory, nil
|
||||
}
|
||||
|
||||
func gitCheckout(path, sha string) error {
|
||||
return runGitCommand(path, []string{"checkout", sha})
|
||||
}
|
||||
|
||||
func gitFetchAndMerge(path string) error {
|
||||
if err := runGitCommand(path, []string{"fetch"}); err != nil {
|
||||
return err
|
||||
}
|
||||
return runGitCommand(path, []string{"merge"})
|
||||
}
|
||||
|
||||
func runGitCommand(path string, args []string) error {
|
||||
cmd := exec.Command("git", args...)
|
||||
cmd.Dir = path
|
||||
err := cmd.Run()
|
||||
if err != nil {
|
||||
return fmt.Errorf("error running %q: %v", strings.Join(cmd.Args, " "), err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func lookupPullRequest(url url.URL, pathPrefix string) (*PullRequest, error) {
|
||||
if !strings.HasPrefix(url.Path, pathPrefix+"/") {
|
||||
return nil, fmt.Errorf("invalid path passed: %s expect %s/123", url.Path, pathPrefix)
|
||||
}
|
||||
prNumber := url.Path[len(pathPrefix)+1:]
|
||||
if strings.Contains(prNumber, "/") {
|
||||
return nil, fmt.Errorf("invalid path passed: %s expect %s/123", url.Path, pathPrefix)
|
||||
}
|
||||
|
||||
resp, err := http.Get(fmt.Sprintf("%s/%s", pullsPrefix, prNumber))
|
||||
defer resp.Body.Close()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error getting pulls: %v", err)
|
||||
}
|
||||
dec := json.NewDecoder(resp.Body)
|
||||
var pr PullRequest
|
||||
if err := dec.Decode(&pr); err != nil {
|
||||
return nil, fmt.Errorf("error decoding pulls: %v", err)
|
||||
}
|
||||
return &pr, nil
|
||||
}
|
||||
|
||||
func generate(dir string) error {
|
||||
cmd := exec.Command("python", "gendoc.py", "--nodelete")
|
||||
cmd.Dir = path.Join(dir, "scripts")
|
||||
var b bytes.Buffer
|
||||
cmd.Stderr = &b
|
||||
err := cmd.Run()
|
||||
if err != nil {
|
||||
return fmt.Errorf("error generating spec: %v\nOutput from gendoc:\n%v", err, b.String())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func writeError(w http.ResponseWriter, code int, err error) {
|
||||
w.WriteHeader(code)
|
||||
io.WriteString(w, fmt.Sprintf("%v\n", err))
|
||||
}
|
||||
|
||||
type server struct {
|
||||
matrixDocCloneURL string
|
||||
}
|
||||
|
||||
// generateAt generates spec from repo at sha.
|
||||
// Returns the path where the generation was done.
|
||||
func (s *server) generateAt(sha string) (dst string, err error) {
|
||||
err = gitFetchAndMerge(s.matrixDocCloneURL)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
dst, err = gitClone(s.matrixDocCloneURL, true)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if err = gitCheckout(dst, sha); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err = generate(dst)
|
||||
return
|
||||
}
|
||||
|
||||
func (s *server) serveSpec(w http.ResponseWriter, req *http.Request) {
|
||||
var sha string
|
||||
|
||||
if strings.ToLower(req.URL.Path) == "/spec/head" {
|
||||
sha = "HEAD"
|
||||
} else {
|
||||
pr, err := lookupPullRequest(*req.URL, "/spec")
|
||||
if err != nil {
|
||||
writeError(w, 400, err)
|
||||
return
|
||||
}
|
||||
|
||||
// We're going to run whatever Python is specified in the pull request, which
|
||||
// may do bad things, so only trust people we trust.
|
||||
if err := checkAuth(pr); err != nil {
|
||||
writeError(w, 403, err)
|
||||
return
|
||||
}
|
||||
sha = pr.Head.SHA
|
||||
}
|
||||
|
||||
dst, err := s.generateAt(sha)
|
||||
defer os.RemoveAll(dst)
|
||||
if err != nil {
|
||||
writeError(w, 500, err)
|
||||
return
|
||||
}
|
||||
|
||||
b, err := ioutil.ReadFile(path.Join(dst, "scripts/gen/specification.html"))
|
||||
if err != nil {
|
||||
writeError(w, 500, fmt.Errorf("Error reading spec: %v", err))
|
||||
return
|
||||
}
|
||||
w.Write(b)
|
||||
}
|
||||
|
||||
func checkAuth(pr *PullRequest) error {
|
||||
if !pr.User.IsTrusted() {
|
||||
return fmt.Errorf("%q is not a trusted pull requester", pr.User.Login)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *server) serveRSTDiff(w http.ResponseWriter, req *http.Request) {
|
||||
pr, err := lookupPullRequest(*req.URL, "/diff/rst")
|
||||
if err != nil {
|
||||
writeError(w, 400, err)
|
||||
return
|
||||
}
|
||||
|
||||
// We're going to run whatever Python is specified in the pull request, which
|
||||
// may do bad things, so only trust people we trust.
|
||||
if err := checkAuth(pr); err != nil {
|
||||
writeError(w, 403, err)
|
||||
return
|
||||
}
|
||||
|
||||
base, err := s.generateAt(pr.Base.SHA)
|
||||
defer os.RemoveAll(base)
|
||||
if err != nil {
|
||||
writeError(w, 500, err)
|
||||
return
|
||||
}
|
||||
|
||||
head, err := s.generateAt(pr.Head.SHA)
|
||||
defer os.RemoveAll(head)
|
||||
if err != nil {
|
||||
writeError(w, 500, err)
|
||||
return
|
||||
}
|
||||
|
||||
diffCmd := exec.Command("diff", "-u", path.Join(base, "scripts", "tmp", "full_spec.rst"), path.Join(head, "scripts", "tmp", "full_spec.rst"))
|
||||
var diff bytes.Buffer
|
||||
diffCmd.Stdout = &diff
|
||||
if err := ignoreExitCodeOne(diffCmd.Run()); err != nil {
|
||||
writeError(w, 500, fmt.Errorf("error running diff: %v", err))
|
||||
return
|
||||
}
|
||||
w.Write(diff.Bytes())
|
||||
}
|
||||
|
||||
func (s *server) serveHTMLDiff(w http.ResponseWriter, req *http.Request) {
|
||||
pr, err := lookupPullRequest(*req.URL, "/diff/html")
|
||||
if err != nil {
|
||||
writeError(w, 400, err)
|
||||
return
|
||||
}
|
||||
|
||||
// We're going to run whatever Python is specified in the pull request, which
|
||||
// may do bad things, so only trust people we trust.
|
||||
if err := checkAuth(pr); err != nil {
|
||||
writeError(w, 403, err)
|
||||
return
|
||||
}
|
||||
|
||||
base, err := s.generateAt(pr.Base.SHA)
|
||||
defer os.RemoveAll(base)
|
||||
if err != nil {
|
||||
writeError(w, 500, err)
|
||||
return
|
||||
}
|
||||
|
||||
head, err := s.generateAt(pr.Head.SHA)
|
||||
defer os.RemoveAll(head)
|
||||
if err != nil {
|
||||
writeError(w, 500, err)
|
||||
return
|
||||
}
|
||||
|
||||
htmlDiffer, err := findHTMLDiffer()
|
||||
if err != nil {
|
||||
writeError(w, 500, fmt.Errorf("could not find HTML differ"))
|
||||
return
|
||||
}
|
||||
|
||||
cmd := exec.Command(htmlDiffer, path.Join(base, "scripts", "gen", "specification.html"), path.Join(head, "scripts", "gen", "specification.html"))
|
||||
var b bytes.Buffer
|
||||
cmd.Stdout = &b
|
||||
if err := cmd.Run(); err != nil {
|
||||
writeError(w, 500, fmt.Errorf("error running HTML differ: %v", err))
|
||||
return
|
||||
}
|
||||
w.Write(b.Bytes())
|
||||
}
|
||||
|
||||
func findHTMLDiffer() (string, error) {
|
||||
wd, err := os.Getwd()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
differ := path.Join(wd, "htmldiff.pl")
|
||||
if _, err := os.Stat(differ); err == nil {
|
||||
return differ, nil
|
||||
}
|
||||
return "", fmt.Errorf("unable to find htmldiff.pl")
|
||||
}
|
||||
|
||||
func listPulls(w http.ResponseWriter, req *http.Request) {
|
||||
resp, err := http.Get(pullsPrefix)
|
||||
if err != nil {
|
||||
writeError(w, 500, err)
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
dec := json.NewDecoder(resp.Body)
|
||||
var pulls []PullRequest
|
||||
if err := dec.Decode(&pulls); err != nil {
|
||||
writeError(w, 500, err)
|
||||
return
|
||||
}
|
||||
if len(pulls) == 0 {
|
||||
io.WriteString(w, "No pull requests found")
|
||||
return
|
||||
}
|
||||
s := "<body><ul>"
|
||||
for _, pull := range pulls {
|
||||
s += fmt.Sprintf(`<li>%d: <a href="%s">%s</a>: <a href="%s">%s</a>: <a href="spec/%d">spec</a> <a href="diff/html/%d">spec diff</a> <a href="diff/rst/%d">rst diff</a></li>`,
|
||||
pull.Number, pull.User.HTMLURL, pull.User.Login, pull.HTMLURL, pull.Title, pull.Number, pull.Number, pull.Number)
|
||||
}
|
||||
s += `</ul><div><a href="spec/head">View the spec at head</a></div></body>`
|
||||
io.WriteString(w, s)
|
||||
}
|
||||
|
||||
func ignoreExitCodeOne(err error) error {
|
||||
if err == nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if exiterr, ok := err.(*exec.ExitError); ok {
|
||||
if status, ok := exiterr.Sys().(syscall.WaitStatus); ok {
|
||||
if status.ExitStatus() == 1 {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func main() {
|
||||
flag.Parse()
|
||||
// It would be great to read this from github, but there's no convenient way to do so.
|
||||
// Most of these memberships are "private", so would require some kind of auth.
|
||||
allowedMembers = map[string]bool{
|
||||
"dbkr": true,
|
||||
"erikjohnston": true,
|
||||
"illicitonion": true,
|
||||
"Kegsay": true,
|
||||
"NegativeMjark": true,
|
||||
}
|
||||
rand.Seed(time.Now().Unix())
|
||||
masterCloneDir, err := gitClone(matrixDocCloneURL, false)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
s := server{masterCloneDir}
|
||||
http.HandleFunc("/spec/", s.serveSpec)
|
||||
http.HandleFunc("/diff/rst/", s.serveRSTDiff)
|
||||
http.HandleFunc("/diff/html/", s.serveHTMLDiff)
|
||||
http.HandleFunc("/healthz", serveText("ok"))
|
||||
http.HandleFunc("/", listPulls)
|
||||
|
||||
fmt.Printf("Listening on port %d\n", *port)
|
||||
log.Fatal(http.ListenAndServe(fmt.Sprintf(":%d", *port), nil))
|
||||
}
|
||||
|
||||
func serveText(s string) func(http.ResponseWriter, *http.Request) {
|
||||
return func(w http.ResponseWriter, req *http.Request) {
|
||||
io.WriteString(w, s)
|
||||
}
|
||||
}
|
@ -0,0 +1,38 @@
|
||||
Events
|
||||
======
|
||||
|
||||
All communication in Matrix is expressed in the form of data objects called
|
||||
Events. These are the fundamental building blocks common to the client-server,
|
||||
server-server and application-service APIs, and are described below.
|
||||
|
||||
{{common_event_fields}}
|
||||
|
||||
{{common_room_event_fields}}
|
||||
|
||||
{{common_state_event_fields}}
|
||||
|
||||
|
||||
Room Events
|
||||
-----------
|
||||
.. NOTE::
|
||||
This section is a work in progress.
|
||||
|
||||
This specification outlines several standard event types, all of which are
|
||||
prefixed with ``m.``
|
||||
|
||||
{{m_room_aliases_event}}
|
||||
|
||||
{{m_room_canonical_alias_event}}
|
||||
|
||||
{{m_room_create_event}}
|
||||
|
||||
{{m_room_history_visibility_event}}
|
||||
|
||||
{{m_room_join_rules_event}}
|
||||
|
||||
{{m_room_member_event}}
|
||||
|
||||
{{m_room_power_levels_event}}
|
||||
|
||||
{{m_room_redaction_event}}
|
||||
|
@ -0,0 +1,93 @@
|
||||
Feature Profiles
|
||||
================
|
||||
|
||||
.. sect:feature-profiles:
|
||||
|
||||
Matrix supports many different kinds of clients: from embedded IoT devices to
|
||||
desktop clients. Not all clients can provide the same feature sets as other
|
||||
clients e.g. due to lack of physical hardware such as not having a screen.
|
||||
Clients can fall into one of several profiles and each profile contains a set
|
||||
of features that the client MUST support. This section details a set of
|
||||
"feature profiles". Clients are expected to implement a profile in its entirety
|
||||
in order for it to be classified as that profile.
|
||||
|
||||
Summary
|
||||
-------
|
||||
|
||||
===================================== ========== ========== ========== ========== ==========
|
||||
Module / Profile Web Mobile Desktop CLI Embedded
|
||||
===================================== ========== ========== ========== ========== ==========
|
||||
`Instant Messaging`_ Required Required Required Required Optional
|
||||
`Presence`_ Required Required Required Required Optional
|
||||
`Push Notifications`_ Optional Required Optional Optional Optional
|
||||
`Receipts`_ Required Required Required Required Optional
|
||||
`Typing Notifications`_ Required Required Required Required Optional
|
||||
`VoIP`_ Required Required Required Optional Optional
|
||||
`Content Repository`_ Required Required Required Optional Optional
|
||||
`Managing History Visibility`_ Required Required Required Required Optional
|
||||
`End-to-End Encryption`_ Optional Optional Optional Optional Optional
|
||||
===================================== ========== ========== ========== ========== ==========
|
||||
|
||||
*Please see each module for more details on what clients need to implement.*
|
||||
|
||||
.. _End-to-End Encryption: `module:e2e`_
|
||||
.. _Instant Messaging: `module:im`_
|
||||
.. _Presence: `module:presence`_
|
||||
.. _Push Notifications: `module:push`_
|
||||
.. _Receipts: `module:receipts`_
|
||||
.. _Typing Notifications: `module:typing`_
|
||||
.. _VoIP: `module:voip`_
|
||||
.. _Content Repository: `module:content`_
|
||||
.. _Managing History Visibility: `module:history-visibility`_
|
||||
|
||||
Clients
|
||||
-------
|
||||
|
||||
Stand-alone web (``Web``)
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
This is a web page which heavily uses Matrix for communication. Single-page web
|
||||
apps would be classified as a stand-alone web client, as would multi-page web
|
||||
apps which use Matrix on nearly every page.
|
||||
|
||||
Mobile (``Mobile``)
|
||||
~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
This is a Matrix client specifically designed for consumption on mobile devices.
|
||||
This is typically a mobile app but need not be so provided the feature set can
|
||||
be reached (e.g. if a mobile site could display push notifications it could be
|
||||
classified as a mobile client).
|
||||
|
||||
Desktop (``Desktop``)
|
||||
~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
This is a native GUI application which can run in its own environment outside a
|
||||
browser.
|
||||
|
||||
Command Line Interface (``CLI``)
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
This is a client which is used via a text-based terminal.
|
||||
|
||||
Embedded (``Embedded``)
|
||||
~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
This is a client which is embedded into another application or an embedded
|
||||
device.
|
||||
|
||||
Application
|
||||
+++++++++++
|
||||
|
||||
This is a Matrix client which is embedded in another website, e.g. using
|
||||
iframes. These embedded clients are typically for a single purpose
|
||||
related to the website in question, and are not intended to be fully-fledged
|
||||
communication apps.
|
||||
|
||||
Device
|
||||
++++++
|
||||
|
||||
This is a client which is typically running on an embedded device such as a
|
||||
kettle, fridge or car. These clients tend to perform a few operations and run
|
||||
in a resource constrained environment. Like embedded applications, they are
|
||||
not intended to be fully-fledged communication systems.
|
||||
|
@ -0,0 +1,3 @@
|
||||
Modules
|
||||
=======
|
||||
|
@ -1,157 +0,0 @@
|
||||
Events
|
||||
======
|
||||
|
||||
All communication in Matrix is expressed in the form of data objects called
|
||||
Events. These are the fundamental building blocks common to the client-server,
|
||||
server-server and application-service APIs, and are described below.
|
||||
|
||||
{{common_event_fields}}
|
||||
|
||||
{{common_room_event_fields}}
|
||||
|
||||
{{common_state_event_fields}}
|
||||
|
||||
|
||||
Room Events
|
||||
-----------
|
||||
.. NOTE::
|
||||
This section is a work in progress.
|
||||
|
||||
This specification outlines several standard event types, all of which are
|
||||
prefixed with ``m.``
|
||||
|
||||
{{room_events}}
|
||||
|
||||
m.room.message msgtypes
|
||||
~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. TODO-spec
|
||||
How a client should handle unknown message types.
|
||||
|
||||
|
||||
Each `m.room.message`_ MUST have a ``msgtype`` key which identifies the type
|
||||
of message being sent. Each type has their own required and optional keys, as
|
||||
outlined below.
|
||||
|
||||
{{msgtype_events}}
|
||||
|
||||
Presence Events
|
||||
~~~~~~~~~~~~~~~
|
||||
|
||||
{{presence_events}}
|
||||
|
||||
Each user has the concept of presence information. This encodes the
|
||||
"availability" of that user, suitable for display on other user's clients.
|
||||
This is transmitted as an ``m.presence`` event and is one of the few events
|
||||
which are sent *outside the context of a room*. The basic piece of presence
|
||||
information is represented by the ``presence`` key, which is an enum of one
|
||||
of the following:
|
||||
|
||||
- ``online`` : The default state when the user is connected to an event
|
||||
stream.
|
||||
- ``unavailable`` : The user is not reachable at this time.
|
||||
- ``offline`` : The user is not connected to an event stream.
|
||||
- ``free_for_chat`` : The user is generally willing to receive messages
|
||||
moreso than default.
|
||||
- ``hidden`` : Behaves as offline, but allows the user to see the client
|
||||
state anyway and generally interact with client features. (Not yet
|
||||
implemented in synapse).
|
||||
|
||||
In addition, the server maintains a timestamp of the last time it saw a
|
||||
pro-active event from the user; either sending a message to a room, or
|
||||
changing presence state from a lower to a higher level of availability
|
||||
(thus: changing state from ``unavailable`` to ``online`` counts as a
|
||||
proactive event, whereas in the other direction it will not). This timestamp
|
||||
is presented via a key called ``last_active_ago``, which gives the relative
|
||||
number of milliseconds since the message is generated/emitted that the user
|
||||
was last seen active.
|
||||
|
||||
|
||||
Events on Change of Profile Information
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
Because the profile displayname and avatar information are likely to be used in
|
||||
many places of a client's display, changes to these fields cause an automatic
|
||||
propagation event to occur, informing likely-interested parties of the new
|
||||
values. This change is conveyed using two separate mechanisms:
|
||||
|
||||
- a ``m.room.member`` event is sent to every room the user is a member of,
|
||||
to update the ``displayname`` and ``avatar_url``.
|
||||
- a ``m.presence`` presence status update is sent, again containing the new values of the
|
||||
``displayname`` and ``avatar_url`` keys, in addition to the required
|
||||
``presence`` key containing the current presence state of the user.
|
||||
|
||||
Both of these should be done automatically by the home server when a user
|
||||
successfully changes their displayname or avatar URL fields.
|
||||
|
||||
Additionally, when home servers emit room membership events for their own
|
||||
users, they should include the displayname and avatar URL fields in these
|
||||
events so that clients already have these details to hand, and do not have to
|
||||
perform extra roundtrips to query it.
|
||||
|
||||
Voice over IP
|
||||
-------------
|
||||
Matrix can also be used to set up VoIP calls. This is part of the core
|
||||
specification, although is at a relatively early stage. Voice (and video) over
|
||||
Matrix is built on the WebRTC 1.0 standard.
|
||||
|
||||
Call events are sent to a room, like any other event. This means that clients
|
||||
must only send call events to rooms with exactly two participants as currently
|
||||
the WebRTC standard is based around two-party communication.
|
||||
|
||||
{{voip_events}}
|
||||
|
||||
Message Exchange
|
||||
~~~~~~~~~~~~~~~~
|
||||
A call is set up with messages exchanged as follows:
|
||||
|
||||
::
|
||||
|
||||
Caller Callee
|
||||
m.call.invite ----------->
|
||||
m.call.candidate -------->
|
||||
[more candidates events]
|
||||
User answers call
|
||||
<------ m.call.answer
|
||||
[...]
|
||||
<------ m.call.hangup
|
||||
|
||||
Or a rejected call:
|
||||
|
||||
::
|
||||
|
||||
Caller Callee
|
||||
m.call.invite ----------->
|
||||
m.call.candidate -------->
|
||||
[more candidates events]
|
||||
User rejects call
|
||||
<------- m.call.hangup
|
||||
|
||||
Calls are negotiated according to the WebRTC specification.
|
||||
|
||||
|
||||
Glare
|
||||
~~~~~
|
||||
This specification aims to address the problem of two users calling each other
|
||||
at roughly the same time and their invites crossing on the wire. It is a far
|
||||
better experience for the users if their calls are connected if it is clear
|
||||
that their intention is to set up a call with one another.
|
||||
|
||||
In Matrix, calls are to rooms rather than users (even if those rooms may only
|
||||
contain one other user) so we consider calls which are to the same room.
|
||||
|
||||
The rules for dealing with such a situation are as follows:
|
||||
|
||||
- If an invite to a room is received whilst the client is preparing to send an
|
||||
invite to the same room, the client should cancel its outgoing call and
|
||||
instead automatically accept the incoming call on behalf of the user.
|
||||
- If an invite to a room is received after the client has sent an invite to
|
||||
the same room and is waiting for a response, the client should perform a
|
||||
lexicographical comparison of the call IDs of the two calls and use the
|
||||
lesser of the two calls, aborting the greater. If the incoming call is the
|
||||
lesser, the client should accept this call on behalf of the user.
|
||||
|
||||
The call setup should appear seamless to the user as if they had simply placed
|
||||
a call and the other party had accepted. Thusly, any media stream that had been
|
||||
setup for use on a call should be transferred and used for the call that
|
||||
replaces it.
|
||||
|
@ -1,76 +0,0 @@
|
||||
Content repository
|
||||
==================
|
||||
|
||||
HTTP API
|
||||
--------
|
||||
|
||||
Uploads are POSTed to a resource which returns a token which is used to GET
|
||||
the download. Uploads are POSTed to the sender's local homeserver, but are
|
||||
downloaded from the recipient's local homeserver, which must thus first transfer
|
||||
the content from the origin homeserver using the same API (unless the origin
|
||||
and destination homeservers are the same). The upload/download API is::
|
||||
|
||||
=> POST /_matrix/media/v1/upload HTTP/1.1
|
||||
Content-Type: <media-type>
|
||||
|
||||
<media>
|
||||
|
||||
<= HTTP/1.1 200 OK
|
||||
Content-Type: application/json
|
||||
|
||||
{ "content-uri": "mxc://<server-name>/<media-id>" }
|
||||
|
||||
=> GET /_matrix/media/v1/download/<server-name>/<media-id> HTTP/1.1
|
||||
|
||||
<= HTTP/1.1 200 OK
|
||||
Content-Type: <media-type>
|
||||
Content-Disposition: attachment;filename=<upload-filename>
|
||||
|
||||
<media>
|
||||
|
||||
Clients can get thumbnails by supplying a desired width and height and
|
||||
thumbnailing method::
|
||||
|
||||
=> GET /_matrix/media/v1/thumbnail/<server_name>
|
||||
/<media-id>?width=<w>&height=<h>&method=<m> HTTP/1.1
|
||||
|
||||
<= HTTP/1.1 200 OK
|
||||
Content-Type: image/jpeg or image/png
|
||||
|
||||
<thumbnail>
|
||||
|
||||
The thumbnail methods are "crop" and "scale". "scale" trys to return an
|
||||
image where either the width or the height is smaller than the requested
|
||||
size. The client should then scale and letterbox the image if it needs to
|
||||
fit within a given rectangle. "crop" trys to return an image where the
|
||||
width and height are close to the requested size and the aspect matches
|
||||
the requested size. The client should scale the image if it needs to fit
|
||||
within a given rectangle.
|
||||
|
||||
Homeservers may generate thumbnails for content uploaded to remote
|
||||
homeservers themselves or may rely on the remote homeserver to thumbnail
|
||||
the content. Homeservers may return thumbnails of a different size to that
|
||||
requested. However homeservers should provide exact matches where reasonable.
|
||||
Homeservers must never upscale images.
|
||||
|
||||
Security
|
||||
--------
|
||||
|
||||
Clients may try to upload very large files. Homeservers should not store files
|
||||
that are too large and should not serve them to clients.
|
||||
|
||||
Clients may try to upload very large images. Homeservers should not attempt to
|
||||
generate thumbnails for images that are too large.
|
||||
|
||||
Remote homeservers may host very large files or images. Homeserver should not
|
||||
proxy or thumbnail large files or images from remote homeservers.
|
||||
|
||||
Clients may try to upload a large number of files. Homeservers should limit the
|
||||
number and total size of media that can be uploaded by clients.
|
||||
|
||||
Clients may try to access a large number of remote files through a homeserver.
|
||||
Homeservers should restrict the number and size of remote files that it caches.
|
||||
|
||||
Clients or remote homeservers may try to upload malicious files targeting
|
||||
vulnerabilities in either the homeserver thumbnailing or the client decoders.
|
||||
|
@ -1,58 +0,0 @@
|
||||
Typing Notifications
|
||||
====================
|
||||
|
||||
Client APIs
|
||||
-----------
|
||||
|
||||
To set "I am typing for the next N msec"::
|
||||
PUT .../rooms/<room_id>/typing/<user_id>
|
||||
Content: { "typing": true, "timeout": N }
|
||||
# timeout is in msec; I suggest no more than 20 or 30 seconds
|
||||
|
||||
This should be re-sent by the client to continue informing the server the user
|
||||
is still typing; I suggest a safety margin of 5 seconds before the expected
|
||||
timeout runs out. Just keep declaring a new timeout, it will replace the old
|
||||
one.
|
||||
|
||||
To set "I am no longer typing"::
|
||||
PUT ../rooms/<room_id>/typing/<user_id>
|
||||
Content: { "typing": false }
|
||||
|
||||
Client Events
|
||||
-------------
|
||||
|
||||
All room members will receive an event on the event stream::
|
||||
|
||||
{
|
||||
"type": "m.typing",
|
||||
"room_id": "!room-id-here:matrix.org",
|
||||
"content": {
|
||||
"user_ids": ["list of", "every user", "who is", "currently typing"]
|
||||
}
|
||||
}
|
||||
|
||||
The client must use this list to *REPLACE* its knowledge of every user who is
|
||||
currently typing. The reason for this is that the server DOES NOT remember
|
||||
users who are not currently typing, as that list gets big quickly. The client
|
||||
should mark as not typing, any user ID who is not in that list.
|
||||
|
||||
Server APIs
|
||||
-----------
|
||||
|
||||
Servers will emit EDUs in the following form::
|
||||
|
||||
{
|
||||
"type": "m.typing",
|
||||
"content": {
|
||||
"room_id": "!room-id-here:matrix.org",
|
||||
"user_id": "@user-id-here:matrix.org",
|
||||
"typing": true/false,
|
||||
}
|
||||
}
|
||||
|
||||
Server EDUs don't (currently) contain timing information; it is up to
|
||||
originating HSes to ensure they eventually send "stop" notifications.
|
||||
|
||||
((This will eventually need addressing, as part of the wider typing/presence
|
||||
timer addition work))
|
||||
|
@ -0,0 +1,8 @@
|
||||
Identity Servers
|
||||
================
|
||||
.. NOTE::
|
||||
This section is a work in progress.
|
||||
|
||||
.. TODO-doc Dave
|
||||
- 3PIDs and identity server, functions
|
||||
|
@ -0,0 +1,56 @@
|
||||
Module Heading
|
||||
==============
|
||||
|
||||
.. _module:short-name:
|
||||
|
||||
A short summary of the module. What features does this module provide? An anchor
|
||||
should be specified at the top of the module using the format ``module:name``.
|
||||
|
||||
Complicated modules may wish to have architecture diagrams or event flows
|
||||
(e.g. VoIP call flows) here. Custom subsections can be included but they should
|
||||
be used *sparingly* to reduce the risk of putting client or server behaviour
|
||||
information in these custom sections.
|
||||
|
||||
Events
|
||||
------
|
||||
List the new event types introduced by this module, if any. If there are no
|
||||
new events, this section can be omitted. Event types should be done as
|
||||
subsections. This section is intended to document the "common shared event
|
||||
structure" between client and server. Deviations from this shared structure
|
||||
should be documented in the relevant behaviour section.
|
||||
|
||||
``m.example.event.type``
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
There should be JSON Schema docs for this event. Once there is JSON schema,
|
||||
there will be a template variable with dots in the event type replaced with
|
||||
underscores and the suffix ``_event``. You can insert a template like so:
|
||||
|
||||
{{m_example_event_type_event}}
|
||||
|
||||
Client behaviour
|
||||
----------------
|
||||
List any new HTTP endpoints. These endpoints should be documented using Swagger.
|
||||
Once there is Swagger, there will be a template variable based on the name of
|
||||
the YAML file with the suffix ``_http_api``. You can insert a template for
|
||||
swagger docs like so:
|
||||
|
||||
{{name-of-yaml-file-without-file-ext_http_api}}
|
||||
|
||||
List the steps the client needs to take to
|
||||
correctly process this module. List what data structures the client should be
|
||||
storing in order to aid implementation.
|
||||
|
||||
Server behaviour
|
||||
----------------
|
||||
Does the server need to handle any of the new events in a special way (e.g.
|
||||
typing timeouts, presence). Advice on how to persist events and/or requests are
|
||||
recommended to aid implementation. Federation-specific logic should be included
|
||||
here.
|
||||
|
||||
Security considerations
|
||||
-----------------------
|
||||
This includes privacy leaks: for example leaking presence info. How do
|
||||
misbehaving clients or servers impact this module? This section should always be
|
||||
included, if only to say "we've thought about it but there isn't anything to do
|
||||
here".
|
||||
|
@ -0,0 +1,74 @@
|
||||
Content repository
|
||||
==================
|
||||
|
||||
.. _module:content:
|
||||
|
||||
This module allows users to upload content to their homeserver which is
|
||||
retrievable from other homeservers. Its' purpose is to allow users to share
|
||||
attachments in a room. Content locations are represented as Matrix Content (MXC)
|
||||
URIs. They look like::
|
||||
|
||||
mxc://<server-name>/<media-id>
|
||||
|
||||
<server-name> : The name of the homeserver where this content originated, e.g. matrix.org
|
||||
<media-id> : An opaque ID which identifies the content.
|
||||
|
||||
Uploads are POSTed to a resource on the user's local homeserver which returns a
|
||||
token which is used to GET the download. Content is downloaded from the
|
||||
recipient's local homeserver, which must first transfer the content from the
|
||||
origin homeserver using the same API (unless the origin and destination
|
||||
homeservers are the same).
|
||||
|
||||
Client behaviour
|
||||
----------------
|
||||
|
||||
Clients can upload and download content using the following HTTP APIs.
|
||||
|
||||
{{content_repo_http_api}}
|
||||
|
||||
Thumbnails
|
||||
~~~~~~~~~~
|
||||
The thumbnail methods are "crop" and "scale". "scale" tries to return an
|
||||
image where either the width or the height is smaller than the requested
|
||||
size. The client should then scale and letterbox the image if it needs to
|
||||
fit within a given rectangle. "crop" tries to return an image where the
|
||||
width and height are close to the requested size and the aspect matches
|
||||
the requested size. The client should scale the image if it needs to fit
|
||||
within a given rectangle.
|
||||
|
||||
Server behaviour
|
||||
----------------
|
||||
|
||||
Homeservers may generate thumbnails for content uploaded to remote
|
||||
homeservers themselves or may rely on the remote homeserver to thumbnail
|
||||
the content. Homeservers may return thumbnails of a different size to that
|
||||
requested. However homeservers should provide exact matches where reasonable.
|
||||
Homeservers must never upscale images.
|
||||
|
||||
Security considerations
|
||||
-----------------------
|
||||
|
||||
The HTTP GET endpoint does not require any authentication. Knowing the URL of
|
||||
the content is sufficient to retrieve the content, even if the entity isn't in
|
||||
the room.
|
||||
|
||||
Homeservers have additional concerns:
|
||||
|
||||
- Clients may try to upload very large files. Homeservers should not store files
|
||||
that are too large and should not serve them to clients.
|
||||
|
||||
- Clients may try to upload very large images. Homeservers should not attempt to
|
||||
generate thumbnails for images that are too large.
|
||||
|
||||
- Remote homeservers may host very large files or images. Homeservers should not
|
||||
proxy or thumbnail large files or images from remote homeservers.
|
||||
|
||||
- Clients may try to upload a large number of files. Homeservers should limit the
|
||||
number and total size of media that can be uploaded by clients.
|
||||
|
||||
- Clients may try to access a large number of remote files through a homeserver.
|
||||
Homeservers should restrict the number and size of remote files that it caches.
|
||||
|
||||
- Clients or remote homeservers may try to upload malicious files targeting
|
||||
vulnerabilities in either the homeserver thumbnailing or the client decoders.
|
||||
|
@ -1,6 +1,8 @@
|
||||
End-to-End Encryption
|
||||
=====================
|
||||
|
||||
.. _module:e2e:
|
||||
|
||||
.. TODO-doc
|
||||
- Why is this needed.
|
||||
- Overview of process
|
@ -1,5 +1,7 @@
|
||||
Room History Visibility
|
||||
=======================
|
||||
-----------------------
|
||||
|
||||
.. _module:history-visibility:
|
||||
|
||||
Whether a member of a room can see the events that happened in a room from
|
||||
before they joined the room is controlled by the ``history_visibility`` key
|
@ -0,0 +1,29 @@
|
||||
Instant Messaging
|
||||
=================
|
||||
|
||||
.. _module:im:
|
||||
|
||||
Events
|
||||
------
|
||||
|
||||
{{m_room_message_event}}
|
||||
|
||||
{{m_room_message_feedback_event}}
|
||||
|
||||
{{m_room_name_event}}
|
||||
|
||||
{{m_room_topic_event}}
|
||||
|
||||
m.room.message msgtypes
|
||||
-----------------------
|
||||
|
||||
.. TODO-spec
|
||||
How a client should handle unknown message types.
|
||||
|
||||
|
||||
Each `m.room.message`_ MUST have a ``msgtype`` key which identifies the type
|
||||
of message being sent. Each type has their own required and optional keys, as
|
||||
outlined below.
|
||||
|
||||
{{msgtype_events}}
|
||||
|
@ -0,0 +1,112 @@
|
||||
Presence
|
||||
========
|
||||
|
||||
.. _module:presence:
|
||||
|
||||
Each user has the concept of presence information. This encodes:
|
||||
|
||||
* Whether the user is currently online
|
||||
* How recently the user was last active (as seen by the server)
|
||||
* Whether a given client considers the user to be currently idle
|
||||
* Arbitrary information about the user's current status (e.g. "in a meeting").
|
||||
|
||||
This information is collated from both per-device (``online``, ``idle``,
|
||||
``last_active``) and per-user (status) data, aggregated by the user's homeserver
|
||||
and transmitted as an ``m.presence`` event. This is one of the few events which
|
||||
are sent *outside the context of a room*. Presence events are sent to all users
|
||||
who subscribe to this user's presence through a presence list or by sharing
|
||||
membership of a room.
|
||||
|
||||
A presence list is a list of user IDs whose presence the user wants to follow.
|
||||
To be added to this list, the user being added must be invited by the list owner
|
||||
who must accept the invitation.
|
||||
|
||||
User's presence state is represented by the ``presence`` key, which is an enum
|
||||
of one of the following:
|
||||
|
||||
- ``online`` : The default state when the user is connected to an event
|
||||
stream.
|
||||
- ``unavailable`` : The user is not reachable at this time e.g. they are
|
||||
idle.
|
||||
- ``offline`` : The user is not connected to an event stream or is
|
||||
explicitly suppressing their profile information from being sent.
|
||||
- ``free_for_chat`` : The user is generally willing to receive messages
|
||||
moreso than default.
|
||||
|
||||
Events
|
||||
------
|
||||
|
||||
{{presence_events}}
|
||||
|
||||
Client behaviour
|
||||
----------------
|
||||
|
||||
Clients can manually set/get their presence/presence list using the HTTP APIs
|
||||
listed below.
|
||||
|
||||
{{presence_http_api}}
|
||||
|
||||
Idle timeout
|
||||
~~~~~~~~~~~~
|
||||
|
||||
Clients SHOULD implement an "idle timeout". This is a timer which fires after
|
||||
a period of inactivity on the client. The definition of inactivity varies
|
||||
depending on the client. For example, web implementations may determine
|
||||
inactivity to be not moving the mouse for a certain period of time. When this
|
||||
timer fires it should set the presence state to ``unavailable``. When the user
|
||||
becomes active again (e.g. by moving the mouse) the client should set the
|
||||
presence state to ``online``. A timeout value between 1 and 5 minutes is
|
||||
recommended.
|
||||
|
||||
Server behaviour
|
||||
----------------
|
||||
|
||||
Each user's home server stores a "presence list" per user. Once a user accepts
|
||||
a presence list, both user's HSes must track the subscription.
|
||||
|
||||
Propagating profile information
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Because the profile display name and avatar information are likely to be used in
|
||||
many places of a client's display, changes to these fields SHOULD cause an
|
||||
automatic propagation event to occur, informing likely-interested parties of the
|
||||
new values. One of these change mechanisms SHOULD be via ``m.presence`` events.
|
||||
These events should set ``displayname`` and ``avatar_url`` to the new values
|
||||
along with the presence-specific keys. This SHOULD be done automatically by the
|
||||
home server when a user successfully changes their display name or avatar URL.
|
||||
|
||||
.. admonition:: Rationale
|
||||
|
||||
The intention for sending this information in ``m.presence`` is so that any
|
||||
"user list" can display the *current* name/presence for a user ID outside the
|
||||
scope of a room e.g. for a user page. This is bundled into a single event for
|
||||
several reasons. The user's display name can change per room. This
|
||||
event provides the "canonical" name for the user. In addition, the name is
|
||||
bundled into a single event for the ease of client implementations. If this
|
||||
was not done, the client would need to search all rooms for their own
|
||||
membership event to pull out the display name.
|
||||
|
||||
|
||||
Last active ago
|
||||
~~~~~~~~~~~~~~~
|
||||
The server maintains a timestamp of the last time it saw a
|
||||
pro-active event from the user. A pro-active event may be sending a message to a
|
||||
room or changing presence state to a higher level of availability. Levels of
|
||||
availability are defined from low to high as follows:
|
||||
|
||||
- ``offline``
|
||||
- ``unavailable``
|
||||
- ``online``
|
||||
- ``free_for_chat``
|
||||
|
||||
Based on this list, changing state from ``unavailable`` to ``online`` counts as
|
||||
a pro-active event, whereas ``online`` to ``unavailable`` does not. This
|
||||
timestamp is presented via a key called ``last_active_ago`` which gives the
|
||||
relative number of milliseconds since the pro-active event.
|
||||
|
||||
Security considerations
|
||||
-----------------------
|
||||
|
||||
Presence information is shared with all users who share a room with the target
|
||||
user. In large public rooms this could be undesirable.
|
||||
|
@ -1,6 +1,8 @@
|
||||
Push Notifications
|
||||
==================
|
||||
|
||||
.. _module:push:
|
||||
|
||||
Overview
|
||||
--------
|
||||
|
@ -0,0 +1,84 @@
|
||||
Receipts
|
||||
========
|
||||
|
||||
.. _module:receipts:
|
||||
|
||||
This module adds in support for receipts. These receipts are a form of
|
||||
acknowledgement of an event. This module defines a single acknowledgement:
|
||||
``m.read`` which indicates that the user has read up to a given event.
|
||||
|
||||
Sending a receipt for each event can result in sending large amounts of traffic
|
||||
to a homeserver. To prevent this from becoming a problem, receipts are implemented
|
||||
using "up to" markers. This marker indicates that the acknowledgement applies
|
||||
to all events "up to and including" the event specified. For example, marking
|
||||
an event as "read" would indicate that the user had read all events *up to* the
|
||||
referenced event.
|
||||
|
||||
Events
|
||||
------
|
||||
Each ``user_id``, ``receipt_type`` pair must be associated with only a
|
||||
single ``event_id``.
|
||||
|
||||
{{m_receipt_event}}
|
||||
|
||||
Client behaviour
|
||||
----------------
|
||||
|
||||
In v1 ``/initialSync``, receipts are listed in a separate top level ``receipts``
|
||||
key. In v2 ``/sync``, receipts are contained in the ``ephemeral`` block for a
|
||||
room. New receipts that come down the event streams are deltas which update
|
||||
existing mappings. Clients should replace older receipt acknowledgements based
|
||||
on ``user_id`` and ``receipt_type`` pairs. For example::
|
||||
|
||||
Client receives m.receipt:
|
||||
user = @alice:example.com
|
||||
receipt_type = m.read
|
||||
event_id = $aaa:example.com
|
||||
|
||||
Client receives another m.receipt:
|
||||
user = @alice:example.com
|
||||
receipt_type = m.read
|
||||
event_id = $bbb:example.com
|
||||
|
||||
The client should replace the older acknowledgement for $aaa:example.com with
|
||||
this one for $bbb:example.com
|
||||
|
||||
Clients should send read receipts when there is some certainty that the event in
|
||||
question has been **displayed** to the user. Simply receiving an event does not
|
||||
provide enough certainty that the user has seen the event. The user SHOULD need
|
||||
to *take some action* such as viewing the room that the event was sent to or
|
||||
dismissing a notification in order for the event to count as "read".
|
||||
|
||||
A client can update the markers for its user by interacting with the following
|
||||
HTTP APIs.
|
||||
|
||||
{{v2_receipts_http_api}}
|
||||
|
||||
Server behaviour
|
||||
----------------
|
||||
|
||||
For efficiency, receipts SHOULD be batched into one event per room before
|
||||
delivering them to clients.
|
||||
|
||||
Receipts are sent across federation as EDUs with type ``m.receipt``. The
|
||||
format of the EDUs are::
|
||||
|
||||
{
|
||||
<room_id>: {
|
||||
<receipt_type>: {
|
||||
<user_id>: { <content> }
|
||||
},
|
||||
...
|
||||
},
|
||||
...
|
||||
}
|
||||
|
||||
These are always sent as deltas to previously sent receipts. Currently only a
|
||||
single ``<receipt_type>`` should be used: ``m.read``.
|
||||
|
||||
Security considerations
|
||||
-----------------------
|
||||
|
||||
As receipts are sent outside the context of the event graph, there are no
|
||||
integrity checks performed on the contents of ``m.receipt`` events.
|
||||
|
@ -0,0 +1,66 @@
|
||||
Typing Notifications
|
||||
====================
|
||||
|
||||
.. _module:typing:
|
||||
|
||||
Users may wish to be informed when another user is typing in a room. This can be
|
||||
achieved using typing notifications. These are ephemeral events scoped to a
|
||||
``room_id``. This means they do not form part of the `Event Graph`_ but still
|
||||
have a ``room_id`` key.
|
||||
|
||||
.. _Event Graph: `sect:event-graph`_
|
||||
|
||||
Events
|
||||
------
|
||||
|
||||
{{m_typing_event}}
|
||||
|
||||
Client behaviour
|
||||
----------------
|
||||
|
||||
When a client receives an ``m.typing`` event, it MUST use the user ID list to
|
||||
**REPLACE** its knowledge of every user who is currently typing. The reason for
|
||||
this is that the server *does not remember* users who are not currently typing
|
||||
as that list gets big quickly. The client should mark as not typing any user ID
|
||||
who is not in that list.
|
||||
|
||||
It is recommended that clients store a ``boolean`` indicating whether the user
|
||||
is typing or not. Whilst this value is ``true`` a timer should fire periodically
|
||||
every N seconds to send a typing HTTP request. The value of N is recommended to
|
||||
be no more than 20-30 seconds. This request should be re-sent by the client to
|
||||
continue informing the server the user is still typing. As subsequent
|
||||
requests will replace older requests, a safety margin of 5 seconds before the
|
||||
expected timeout runs out is recommended. When the user stops typing, the
|
||||
state change of the ``boolean`` to ``false`` should trigger another HTTP request
|
||||
to inform the server that the user has stopped typing.
|
||||
|
||||
{{typing_http_api}}
|
||||
|
||||
Server behaviour
|
||||
----------------
|
||||
|
||||
Servers MUST emit typing EDUs in a different form to ``m.typing`` events which
|
||||
are shown to clients. This form looks like::
|
||||
|
||||
{
|
||||
"type": "m.typing",
|
||||
"content": {
|
||||
"room_id": "!room-id-here:matrix.org",
|
||||
"user_id": "@user-id-here:matrix.org",
|
||||
"typing": true/false
|
||||
}
|
||||
}
|
||||
|
||||
This does not contain timing information so it is up to originating homeservers
|
||||
to ensure they eventually send "stop" notifications.
|
||||
|
||||
.. TODO
|
||||
((This will eventually need addressing, as part of the wider typing/presence
|
||||
timer addition work))
|
||||
|
||||
Security considerations
|
||||
-----------------------
|
||||
|
||||
Clients may not wish to inform everyone in a room that they are typing and
|
||||
instead only specific users in the room.
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue