OpenSlides/openslides/utils/websocket.py
2018-10-29 12:33:15 +01:00

156 lines
5.2 KiB
Python

from collections import defaultdict
from typing import Any, Dict, List, Optional
import jsonschema
from channels.generic.websocket import AsyncJsonWebsocketConsumer
from .cache import element_cache
from .collection import AutoupdateFormat, CollectionElement
from .utils import split_element_id
class ProtocollAsyncJsonWebsocketConsumer(AsyncJsonWebsocketConsumer):
"""
Mixin for JSONWebsocketConsumers, that speaks the a special protocol.
"""
async def send_json(self, type: str, content: Any, id: Optional[str] = None, in_response: Optional[str] = None) -> None:
"""
Sends the data with the type.
"""
out = {'type': type, 'content': content}
if id:
out['id'] = id
if in_response:
out['in_response'] = in_response
await super().send_json(out)
async def receive_json(self, content: Any) -> None:
"""
Receives the json data, parses it and calls receive_content.
"""
try:
jsonschema.validate(content, schema)
except jsonschema.ValidationError as err:
try:
in_response = content['id']
except (TypeError, KeyError):
# content is not a dict (TypeError) or has not the key id (KeyError)
in_response = None
await self.send_json(
type='error',
content=str(err),
in_response=in_response)
return
await websocket_client_messages[content['type']].receive_content(self, content['content'], id=content['id'])
schema: Dict[str, Any] = {
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "OpenSlidesWebsocketProtocol",
"description": "The packages that OpenSlides sends between the server and the client.",
"type": "object",
"properties": {
"type": {
"description": "Defines what kind of packages is packed.",
"type": "string",
},
"content": {
"description": "The content of the package.",
},
"id": {
"description": "An identifier of the package.",
"type": "string",
},
"in_response": {
"description": "The id of another package that the other part sent before.",
"type": "string",
},
},
"required": ["type", "content", "id"],
"anyOf": [], # This will be filled in register_client_message()
}
class BaseWebsocketClientMessage:
schema: Dict[str, object] = {}
"""
Optional schema.
If schema is not set, any value in content is accepted.
"""
identifier: str = ""
"""
A unique identifier for the websocket message.
This is used as value in the 'type' property in the websocket message.
"""
content_required = True
"""
Desiedes, if the content property is required.
"""
async def receive_content(self, consumer: "ProtocollAsyncJsonWebsocketConsumer", message: Any, id: str) -> None:
raise NotImplementedError("WebsocketClientMessage needs the method receive_content().")
websocket_client_messages: Dict[str, BaseWebsocketClientMessage] = {}
"""
Saves all websocket client message object ordered by there identifier.
"""
def register_client_message(websocket_client_message: BaseWebsocketClientMessage) -> None:
"""
Registers one websocket client message class.
"""
if not websocket_client_message.identifier or websocket_client_message.identifier in websocket_client_messages:
raise NotImplementedError("WebsocketClientMessage needs a unique identifier.")
websocket_client_messages[websocket_client_message.identifier] = websocket_client_message
# Add the message schema to the schema
message_schema: Dict[str, Any] = {
'properties': {
'type': {'const': websocket_client_message.identifier},
'content': websocket_client_message.schema,
}
}
if websocket_client_message.content_required:
message_schema['required'] = ['content']
schema['anyOf'].append(message_schema)
async def get_element_data(user: Optional[CollectionElement], change_id: int = 0) -> AutoupdateFormat:
"""
Returns all element data since a change_id.
"""
current_change_id = await element_cache.get_current_change_id()
if change_id > current_change_id:
raise ValueError("Requested change_id is higher this highest change_id.")
try:
changed_elements, deleted_element_ids = await element_cache.get_restricted_data(user, change_id, current_change_id)
except RuntimeError:
# The change_id is lower the the lowerst change_id in redis. Return all data
changed_elements = await element_cache.get_all_restricted_data(user)
all_data = True
deleted_elements: Dict[str, List[int]] = {}
else:
all_data = False
deleted_elements = defaultdict(list)
for element_id in deleted_element_ids:
collection_string, id = split_element_id(element_id)
deleted_elements[collection_string].append(id)
return AutoupdateFormat(
changed=changed_elements,
deleted=deleted_elements,
from_change_id=change_id,
to_change_id=current_change_id,
all_data=all_data)