Merge pull request #80 from matrix-org/module-receipts

Receipts module
pull/977/head
Kegsay 9 years ago
commit 8aad238cda

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

@ -3,7 +3,7 @@
"room_id": "!KpjVgQyZpzBwvMBsnT:matrix.org", "room_id": "!KpjVgQyZpzBwvMBsnT:matrix.org",
"content": { "content": {
"$1435641916114394fHBLK:matrix.org": { "$1435641916114394fHBLK:matrix.org": {
"read": { "m.read": {
"@rikj:jki.re": { "@rikj:jki.re": {
"ts": 1436451550453 "ts": 1436451550453
} }

@ -5,26 +5,32 @@
"properties": { "properties": {
"content": { "content": {
"type": "object", "type": "object",
"description": "The event ids which the receipts relate to.",
"patternProperties": { "patternProperties": {
"^\\$": { "^\\$": {
"type": "object", "type": "object",
"description": "The types of the receipts.", "x-pattern": "$EVENT_ID",
"additionalProperties": { "title": "Receipts",
"type": "object", "description": "The mapping of event ID to a collection of receipts for this event ID. The event ID is the ID of the event being acknowledged and *not* an ID for the receipt itself.",
"description": "User ids of the receipts", "properties": {
"patternProperties": { "m.read": {
"^@": { "type": "object",
"type": "object", "title": "Users",
"properties": { "description": "A collection of users who have sent ``m.read`` receipts for this event.",
"ts": { "patternProperties": {
"type": "number", "^@": {
"description": "The timestamp the receipt was sent at" "type": "object",
"title": "Receipt",
"description": "The mapping of user ID to receipt. The user ID is the entity who sent this receipt.",
"x-pattern": "$USER_ID",
"properties": {
"ts": {
"type": "number",
"description": "The timestamp the receipt was sent at."
}
} }
} }
} }
}, }
"additionalProperties": false
} }
} }
}, },

@ -1,66 +1,64 @@
Receipts Receipts
-------- ========
.. _module:receipts: .. _module:receipts:
Receipts are used to publish which events in a room the user or their devices This module adds in support for receipts. These receipts are a form of
have interacted with. For example, which events the user has read. For acknowledgement of an event. This module defines a single acknowledgement:
efficiency this is done as "up to" markers, i.e. marking a particular event ``m.read`` which indicates that the user has read up to a given event.
as, say, ``read`` indicates the user has read all events *up to* that event.
Client-Server API
~~~~~~~~~~~~~~~~~
Clients will receive receipts in the following format::
{
"type": "m.receipt",
"room_id": <room_id>,
"content": {
<event_id>: {
<receipt_type>: {
<user_id>: { "ts": <ts>, ... },
...
}
},
...
}
}
For example:: Sending a receipt for each event can result in sending large amounts of traffic
to a homeserver. To prevent this from becoming a problem, receipts are implemented
using "up to" markers. This marker indicates that the acknowledgement applies
to all events "up to and including" the event specified. For example, marking
an event as "read" would indicate that the user had read all events *up to* the
referenced event.
{ Events
"type": "m.receipt", ------
"room_id": "!KpjVgQyZpzBwvMBsnT:matrix.org", Each ``user_id``, ``receipt_type`` pair must be associated with only a
"content": { single ``event_id``.
"$1435641916114394fHBLK:matrix.org": {
"read": {
"@erikj:jki.re": { "ts": 1436451550453 },
...
}
},
...
}
}
For efficiency, receipts are batched into one event per room. In the initialSync {{m_receipt_event}}
and v2 sync APIs the receipts are listed in a separate top level ``receipts``
key. Each ``user_id``, ``receipt_type`` pair must be associated with only a
single ``event_id``. New receipts that come down the event streams are deltas.
Deltas update existing mappings, clobbering based on ``user_id``,
``receipt_type`` pairs.
Client behaviour
----------------
A client can update the markers for its user by issuing a request:: 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::
POST /_matrix/client/v2_alpha/rooms/<room_id>/receipt/read/<event_id> Client receives m.receipt:
user = @alice:example.com
receipt_type = m.read
event_id = $aaa:example.com
Where the contents of the ``POST`` will be included in the content sent to Client receives another m.receipt:
other users. The server will automatically set the ``ts`` field. 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
Server-Server API 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 Receipts are sent across federation as EDUs with type ``m.receipt``. The
format of the EDUs are:: format of the EDUs are::
@ -75,5 +73,12 @@ format of the EDUs are::
... ...
} }
These are always sent as deltas to previously sent receipts. These are always sent as deltas to previously sent receipts. Currently only a
single ``<receipt_type>`` should be used: ``m.read``.
Security considerations
-----------------------
As receipts are sent outside the context of the event graph, there are no
integrity checks performed on the contents of ``m.receipt`` events.

@ -19,6 +19,7 @@ import yaml
V1_CLIENT_API = "../api/client-server/v1" V1_CLIENT_API = "../api/client-server/v1"
V1_EVENT_EXAMPLES = "../event-schemas/examples/v1" V1_EVENT_EXAMPLES = "../event-schemas/examples/v1"
V1_EVENT_SCHEMA = "../event-schemas/schema/v1" V1_EVENT_SCHEMA = "../event-schemas/schema/v1"
V2_CLIENT_API = "../api/client-server/v2_alpha"
CORE_EVENT_SCHEMA = "../event-schemas/schema/v1/core-event-schema" CORE_EVENT_SCHEMA = "../event-schemas/schema/v1/core-event-schema"
CHANGELOG = "../CHANGELOG.rst" CHANGELOG = "../CHANGELOG.rst"
TARGETS = "../specification/targets.yaml" TARGETS = "../specification/targets.yaml"
@ -49,8 +50,17 @@ def get_json_schema_object_fields(obj, enforce_title=False):
} }
tables = [fields] tables = [fields]
props = obj.get("properties", obj.get("patternProperties"))
parents = obj.get("allOf") parents = obj.get("allOf")
props = obj.get("properties")
if not props:
props = obj.get("patternProperties")
if props:
# try to replace horrible regex key names with pretty x-pattern ones
for key_name in props.keys():
pretty_key = props[key_name].get("x-pattern")
if pretty_key:
props[pretty_key] = props[key_name]
del props[key_name]
if not props and not parents: if not props and not parents:
raise Exception( raise Exception(
"Object %s has no properties or parents." % obj "Object %s has no properties or parents." % obj
@ -70,10 +80,17 @@ def get_json_schema_object_fields(obj, enforce_title=False):
if props[key_name]["type"] == "object": if props[key_name]["type"] == "object":
if props[key_name].get("additionalProperties"): if props[key_name].get("additionalProperties"):
# not "really" an object, just a KV store # not "really" an object, just a KV store
value_type = ( prop_val = props[key_name]["additionalProperties"]["type"]
"{string: %s}" % if prop_val == "object":
props[key_name]["additionalProperties"]["type"] nested_object = get_json_schema_object_fields(
) props[key_name]["additionalProperties"],
enforce_title=True
)
value_type = "{string: %s}" % nested_object[0]["title"]
if not nested_object[0].get("no-table"):
tables += nested_object
else:
value_type = "{string: %s}" % prop_val
else: else:
nested_object = get_json_schema_object_fields( nested_object = get_json_schema_object_fields(
props[key_name], props[key_name],
@ -320,18 +337,27 @@ class MatrixUnits(Units):
} }
def load_swagger_apis(self): def load_swagger_apis(self):
path = V1_CLIENT_API paths = [
V1_CLIENT_API, V2_CLIENT_API
]
apis = {} apis = {}
for filename in os.listdir(path): for path in paths:
if not filename.endswith(".yaml"): is_v2 = (path == V2_CLIENT_API)
if not os.path.exists(V2_CLIENT_API):
self.log("Skipping v2 apis: %s does not exist." % V2_CLIENT_API)
continue continue
self.log("Reading swagger API: %s" % filename) for filename in os.listdir(path):
with open(os.path.join(path, filename), "r") as f: if not filename.endswith(".yaml"):
# strip .yaml continue
group_name = filename[:-5] self.log("Reading swagger API: %s" % filename)
api = yaml.load(f.read()) with open(os.path.join(path, filename), "r") as f:
api["__meta"] = self._load_swagger_meta(api, group_name) # strip .yaml
apis[group_name] = api group_name = filename[:-5]
if is_v2:
group_name = "v2_" + group_name
api = yaml.load(f.read())
api["__meta"] = self._load_swagger_meta(api, group_name)
apis[group_name] = api
return apis return apis
def load_common_event_fields(self): def load_common_event_fields(self):

Loading…
Cancel
Save