示例#1
0
    def open(self, document_id):
        print('Websocket opened')
        current_user = self.get_current_user()
        self.user_info = SessionUserInfo()
        doc_db, can_access = self.user_info.init_access(
            document_id, current_user)

        if can_access:
            if doc_db.id in DocumentWS.sessions:
                self.doc = DocumentWS.sessions[doc_db.id]
                self.id = max(self.doc['participants']) + 1
                print "id when opened %s" % self.id
            else:
                self.id = 0
                self.doc = dict()
                self.doc['db'] = doc_db
                self.doc['participants'] = dict()
                self.doc['last_diffs'] = json_decode(doc_db.last_diffs)
                self.doc['comments'] = json_decode(doc_db.comments)
                self.doc['settings'] = json_decode(doc_db.settings)
                self.doc['contents'] = json_decode(doc_db.contents)
                self.doc['metadata'] = json_decode(doc_db.metadata)
                self.doc['version'] = doc_db.version
                self.doc['diff_version'] = doc_db.diff_version
                self.doc['comment_version'] = doc_db.comment_version
                self.doc['title'] = doc_db.title
                self.doc['id'] = doc_db.id
                DocumentWS.sessions[doc_db.id] = self.doc
            self.doc['participants'][self.id] = self
            response = dict()
            response['type'] = 'welcome'
            self.write_message(response)
示例#2
0
 def subscribe_doc(self, connection_count=0):
     self.user_info = SessionUserInfo(self.user)
     doc_db, can_access = self.user_info.init_access(
         self.document_id
     )
     if not can_access or float(doc_db.doc_version) != FW_DOCUMENT_VERSION:
         self.access_denied()
         return
     if (
         doc_db.id in WebSocket.sessions and
         len(WebSocket.sessions[doc_db.id]['participants']) > 0
     ):
         logger.debug(
             f"Action:Serving already opened document. "
             f"URL:{self.endpoint} User:{self.user.id} "
             f" ParticipantID:{self.id}")
         self.doc = WebSocket.sessions[doc_db.id]
         self.id = max(self.doc['participants']) + 1
         self.doc['participants'][self.id] = self
     else:
         logger.debug(
             f"Action:Opening document from DB. "
             f"URL:{self.endpoint} User:{self.user.id} "
             f"ParticipantID:{self.id}")
         self.id = 0
         self.doc = {
             'db': doc_db,
             'participants': {
                 0: self
             },
             'last_diffs': json_decode(doc_db.last_diffs),
             'comments': json_decode(doc_db.comments),
             'bibliography': json_decode(doc_db.bibliography),
             'contents': json_decode(doc_db.contents),
             'version': doc_db.version,
             'title': doc_db.title,
             'id': doc_db.id,
             'template': {
                 'id': doc_db.template.id,
                 'definition': json_decode(doc_db.template.definition)
             }
         }
         WebSocket.sessions[doc_db.id] = self.doc
     logger.debug(
         f"Action:Participant ID Assigned. URL:{self.endpoint} "
         f"User:{self.user.id} ParticipantID:{self.id}")
     self.send_message({
         'type': 'subscribed'
     })
     if connection_count < 1:
         self.send_styles()
         self.send_document()
     if self.can_communicate():
         self.handle_participant_update()
示例#3
0
    def open(self, document_id):
        print('Websocket opened')
        current_user = self.get_current_user()
        self.user_info = SessionUserInfo()
        doc_db, can_access = self.user_info.init_access(
            document_id, current_user)

        if can_access:
            if doc_db.id in DocumentWS.sessions:
                self.doc = DocumentWS.sessions[doc_db.id]
                self.id = max(self.doc['participants']) + 1
                print("id when opened %s" % self.id)
            else:
                self.id = 0
                self.doc = dict()
                self.doc['db'] = doc_db
                self.doc['participants'] = dict()
                self.doc['last_diffs'] = json_decode(doc_db.last_diffs)
                self.doc['comments'] = json_decode(doc_db.comments)
                self.doc['settings'] = json_decode(doc_db.settings)
                self.doc['contents'] = json_decode(doc_db.contents)
                self.doc['metadata'] = json_decode(doc_db.metadata)
                self.doc['version'] = doc_db.version
                self.doc['diff_version'] = doc_db.diff_version
                self.doc['comment_version'] = doc_db.comment_version
                self.doc['title'] = doc_db.title
                self.doc['id'] = doc_db.id
                DocumentWS.sessions[doc_db.id] = self.doc
            self.doc['participants'][self.id] = self
            response = dict()
            response['type'] = 'welcome'
            self.write_message(response)
示例#4
0
 def subscribe_doc(self, connection_count=0):
     self.user_info = SessionUserInfo(self.user)
     doc_db, can_access = self.user_info.init_access(
         self.document_id
     )
     if not can_access or float(doc_db.doc_version) != FW_DOCUMENT_VERSION:
         self.access_denied()
         return
     if (
         doc_db.id in WebSocket.sessions and
         len(WebSocket.sessions[doc_db.id]['participants']) > 0
     ):
         logger.debug("Serving already opened file")
         self.doc = WebSocket.sessions[doc_db.id]
         self.id = max(self.doc['participants']) + 1
         self.doc['participants'][self.id] = self
         logger.debug("id when opened %s" % self.id)
     else:
         logger.debug("Opening file")
         self.id = 0
         self.doc = {
             'db': doc_db,
             'participants': {
                 0: self
             },
             'last_diffs': json_decode(doc_db.last_diffs),
             'comments': json_decode(doc_db.comments),
             'bibliography': json_decode(doc_db.bibliography),
             'contents': json_decode(doc_db.contents),
             'version': doc_db.version,
             'title': doc_db.title,
             'id': doc_db.id,
             'template': {
                 'title': doc_db.template.title,
                 'definition': json_decode(doc_db.template.definition)
             }
         }
         WebSocket.sessions[doc_db.id] = self.doc
     self.send_message({
         'type': 'subscribed'
     })
     if connection_count < 1:
         self.send_styles()
         self.send_document()
     if self.can_communicate():
         self.handle_participant_update()
示例#5
0
class DocumentWS(BaseWebSocketHandler):
    sessions = dict()

    def open(self, document_id):
        print('Websocket opened')
        current_user = self.get_current_user()
        self.user_info = SessionUserInfo()
        doc_db, can_access = self.user_info.init_access(
            document_id, current_user)

        if can_access:
            if doc_db.id in DocumentWS.sessions:
                self.doc = DocumentWS.sessions[doc_db.id]
                self.id = max(self.doc['participants']) + 1
                print("id when opened %s" % self.id)
            else:
                self.id = 0
                self.doc = dict()
                self.doc['db'] = doc_db
                self.doc['participants'] = dict()
                self.doc['last_diffs'] = json_decode(doc_db.last_diffs)
                self.doc['comments'] = json_decode(doc_db.comments)
                self.doc['settings'] = json_decode(doc_db.settings)
                self.doc['contents'] = json_decode(doc_db.contents)
                self.doc['metadata'] = json_decode(doc_db.metadata)
                self.doc['version'] = doc_db.version
                self.doc['diff_version'] = doc_db.diff_version
                self.doc['comment_version'] = doc_db.comment_version
                self.doc['title'] = doc_db.title
                self.doc['id'] = doc_db.id
                DocumentWS.sessions[doc_db.id] = self.doc
            self.doc['participants'][self.id] = self
            response = dict()
            response['type'] = 'welcome'
            self.write_message(response)

    def confirm_diff(self, request_id):
        response = dict()
        response['type'] = 'confirm_diff'
        response['request_id'] = request_id
        self.write_message(response)

    def send_document(self):
        response = dict()
        response['type'] = 'document_data'
        response['document'] = dict()
        response['document']['id'] = self.doc['id']
        response['document']['version'] = self.doc['version']
        if self.doc['diff_version'] < self.doc['version']:
            print('!!!diff version issue!!!')
            self.doc['diff_version'] = self.doc['version']
            self.doc["last_diffs"] = []
        response['document']['title'] = self.doc['title']
        response['document']['contents'] = self.doc['contents']
        response['document']['metadata'] = self.doc['metadata']
        response['document']['settings'] = self.doc['settings']
        document_owner = self.doc['db'].owner
        access_rights = get_accessrights(
            AccessRight.objects.filter(
                document__owner=document_owner))
        response['document']['access_rights'] = access_rights

        # TODO: switch on filtering when choose workflow and have UI for
        # assigning roles to users
        # filtered_comments = filter_comments_by_role(
        #     DocumentWS.sessions[self.user_info.document_id]["comments"],
        #     access_rights,
        #     'editing',
        #     self.user_info
        # )
        response['document']['comments'] = self.doc["comments"]
        # response['document']['comments'] = filtered_comments
        response['document']['comment_version'] = self.doc["comment_version"]
        response['document']['access_rights'] = get_accessrights(
            AccessRight.objects.filter(document__owner=document_owner))
        response['document']['owner'] = dict()
        response['document']['owner']['id'] = document_owner.id
        response['document']['owner']['name'] = document_owner.readable_name
        response['document']['owner'][
            'avatar'] = avatar_url(document_owner, 80)
        response['document']['owner']['team_members'] = []

        for team_member in document_owner.leader.all():
            tm_object = dict()
            tm_object['id'] = team_member.member.id
            tm_object['name'] = team_member.member.readable_name
            tm_object['avatar'] = avatar_url(team_member.member, 80)
            response['document']['owner']['team_members'].append(tm_object)
        response['document_values'] = dict()
        response['document_values']['is_owner'] = self.user_info.is_owner
        response['document_values']['rights'] = self.user_info.access_rights
        if self.doc['version'] > self.doc['diff_version']:
            print('!!!diff version issue!!!')
            self.doc['diff_version'] = self.doc['version']
            self.doc["last_diffs"] = []
        elif self.doc['diff_version'] > self.doc['version']:
            needed_diffs = self.doc['diff_version'] - self.doc['version']
            response['document_values']['last_diffs'] = self.doc[
                "last_diffs"][-needed_diffs:]
        else:
            response['document_values']['last_diffs'] = []
        if self.user_info.is_new:
            response['document_values']['is_new'] = True
        if not self.user_info.is_owner:
            response['user'] = dict()
            response['user']['id'] = self.user_info.user.id
            response['user']['name'] = self.user_info.user.readable_name
            response['user']['avatar'] = avatar_url(self.user_info.user, 80)
#        if self.doc['in_control'] == self.id:
#            response['document_values']['control']=True
        response['document_values']['session_id'] = self.id
        self.write_message(response)

    def on_message(self, message):
        if self.user_info.document_id not in DocumentWS.sessions:
            print('receiving message for closed document')
            return
        parsed = json_decode(message)
        print(parsed["type"])
        if parsed["type"] == 'get_document':
            self.send_document()
        elif parsed["type"] == 'participant_update':
            self.handle_participant_update()
        elif parsed["type"] == 'chat':
            self.handle_chat(parsed)
        elif parsed["type"] == 'check_diff_version':
            self.check_diff_version(parsed)
        elif parsed["type"] == 'selection_change':
            self.handle_selection_change(message, parsed)
        elif (
            parsed["type"] == 'update_document' and
            self.can_update_document()
        ):
            self.handle_document_update(parsed)
        elif parsed["type"] == 'update_title' and self.can_update_document():
            self.handle_title_update(parsed)
        elif parsed["type"] == 'setting_change' and self.can_update_document():
            self.handle_settings_change(message, parsed)
        elif parsed["type"] == 'diff' and self.can_update_document():
            self.handle_diff(message, parsed)

    def update_document(self, changes):
        if changes['version'] == self.doc['version']:
            # Document hasn't changed, return.
            return
        elif (
            changes['version'] > self.doc['diff_version'] or
            changes['version'] < self.doc['version']
        ):
            # The version number is too high. Possibly due to server restart.
            # Do not accept it, and send a document instead.
            self.send_document()
            return
        else:
            # The saved version does not contain all accepted diffs, so we keep
            # the remaining ones + 1000
            remaining_diffs = 1000 + \
                self.doc['diff_version'] - changes['version']
            self.doc['last_diffs'] = self.doc['last_diffs'][-remaining_diffs:]
        self.doc['title'] = changes['title']
        self.doc['contents'] = changes['contents']
        self.doc['metadata'] = changes['metadata']
        self.doc['version'] = changes['version']

    def update_title(self, title):
        self.doc['title'] = title

    def update_comments(self, comments_updates):
        for cd in comments_updates:
            id = str(cd["id"])
            if cd["type"] == "create":
                del cd["type"]
                self.doc["comments"][id] = cd
            elif cd["type"] == "delete":
                del self.doc["comments"][id]
            elif cd["type"] == "update":
                self.doc["comments"][id]["comment"] = cd["comment"]
                if "review:isMajor" in cd:
                    self.doc["comments"][id][
                        "review:isMajor"] = cd["review:isMajor"]
            elif cd["type"] == "add_answer":
                comment_id = str(cd["commentId"])
                if "answers" not in self.doc["comments"][comment_id]:
                    self.doc["comments"][comment_id]["answers"] = []
                del cd["type"]
                self.doc["comments"][comment_id]["answers"].append(cd)
            elif cd["type"] == "delete_answer":
                comment_id = str(cd["commentId"])
                for answer in self.doc["comments"][comment_id]["answers"]:
                    if answer["id"] == cd["id"]:
                        self.doc["comments"][comment_id][
                            "answers"].remove(answer)
            elif cd["type"] == "update_answer":
                comment_id = str(cd["commentId"])
                for answer in self.doc["comments"][comment_id]["answers"]:
                    if answer["id"] == cd["id"]:
                        answer["answer"] = cd["answer"]
            self.doc['comment_version'] += 1

    def handle_participant_update(self):
        DocumentWS.send_participant_list(self.user_info.document_id)

    def handle_document_update(self, parsed):
        self.update_document(parsed["document"])
        DocumentWS.save_document(self.user_info.document_id)
        message = {
            "type": 'check_hash',
            "diff_version": parsed["document"]["version"],
            "hash": parsed["document"]["hash"]
        }
        DocumentWS.send_updates(message, self.user_info.document_id, self.id)

    def handle_title_update(self, parsed):
        self.update_title(parsed["title"])
        DocumentWS.save_document(self.user_info.document_id)

    def handle_chat(self, parsed):
        chat = {
            "id": str(uuid.uuid4()),
            "body": parsed['body'],
            "from": self.user_info.user.id,
            "type": 'chat'
        }
        DocumentWS.send_updates(chat, self.user_info.document_id)

    def handle_selection_change(self, message, parsed):
        if self.user_info.document_id in DocumentWS.sessions and parsed[
                "diff_version"] == self.doc['diff_version']:
            DocumentWS.send_updates(
                message, self.user_info.document_id, self.id)

    def handle_settings_change(self, message, parsed):
        DocumentWS.sessions[
            self.user_info.document_id]['settings'][
            parsed['variable']] = parsed['value']
        DocumentWS.send_updates(message, self.user_info.document_id, self.id)

    # Checks if the diff only contains changes to comments.
    def only_comments(self, parsed_diffs):
        allowed_operations = ['addMark', 'removeMark']
        only_comment = True
        for diff in parsed_diffs:
            if not (diff['type'] in allowed_operations and diff[
                    'param']['_'] == 'comment'):
                only_comment = False
        return only_comment

    def handle_diff(self, message, parsed):
        if (
            self.user_info.access_rights in COMMENT_ONLY and
            not self.only_comments(parsed['diff'])
        ):
            print(
                (
                    'received non-comment diff from comment-only '
                    'collaborator. Discarding.'
                )
            )
            return
        if parsed["diff_version"] == self.doc['diff_version'] and parsed[
                "comment_version"] == self.doc['comment_version']:
            self.doc["last_diffs"].extend(parsed["diff"])
            self.doc['diff_version'] += len(parsed["diff"])
            self.update_comments(parsed["comments"])
            self.confirm_diff(parsed["request_id"])
            DocumentWS.send_updates(
                message, self.user_info.document_id, self.id)
        elif parsed["diff_version"] != self.doc['diff_version']:
            if parsed["diff_version"] < (
                    self.doc['diff_version'] - len(self.doc["last_diffs"])):
                print('unfixable')
                # Client has a version that is too old
                self.send_document()
            elif parsed["diff_version"] < self.doc['diff_version']:
                print("can fix it")
                number_requested_diffs = self.doc[
                    'diff_version'] - parsed["diff_version"]
                response = {
                    "type": "diff",
                    "diff_version": parsed["diff_version"],
                    "diff": self.doc["last_diffs"][-number_requested_diffs:],
                    "reject_request_id": parsed["request_id"],
                }
                self.write_message(response)
            else:
                print('unfixable')
                # Client has a version that is too old
                self.send_document()
        else:
            print('comment_version incorrect!')
            print(parsed["comment_version"])
            print(self.doc['comment_version'])

    def check_diff_version(self, parsed):
        pdv = parsed["diff_version"]
        ddv = self.doc['diff_version']
        if pdv == ddv:
            response = {
                "type": "confirm_diff_version",
                "diff_version": pdv,
            }
            self.write_message(response)
            return
        elif pdv + len(self.doc["last_diffs"]) >= ddv:
            number_requested_diffs = ddv - pdv
            response = {
                "type": "diff",
                "diff_version": parsed["diff_version"],
                "diff": self.doc["last_diffs"][-number_requested_diffs:],
            }
            self.write_message(response)
            return
        else:
            print('unfixable')
            # Client has a version that is too old
            self.send_document()
            return

    def can_update_document(self):
        return self.user_info.access_rights in CAN_UPDATE_DOCUMENT

    def on_close(self):
        print('Websocket closing')
        if (
            hasattr(self.user_info, 'document_id') and
            self.user_info.document_id in DocumentWS.sessions and
            hasattr(self, 'id') and
            self.id in DocumentWS.sessions[
                self.user_info.document_id
            ]['participants']
        ):
            del self.doc['participants'][self.id]
            if len(self.doc['participants'].keys()) == 0:
                DocumentWS.save_document(self.user_info.document_id)
                del DocumentWS.sessions[self.user_info.document_id]
                print("noone left")

    @classmethod
    def send_participant_list(cls, document_id):
        if document_id in DocumentWS.sessions:
            participant_list = []
            for waiter in cls.sessions[document_id]['participants'].keys():
                participant_list.append({
                    'session_id': waiter,
                    'id': cls.sessions[document_id]['participants'][
                        waiter
                    ].user_info.user.id,
                    'name': cls.sessions[document_id]['participants'][
                        waiter
                    ].user_info.user.readable_name,
                    'avatar': avatar_url(cls.sessions[document_id][
                        'participants'
                    ][waiter].user_info.user, 80)
                })
            message = {
                "participant_list": participant_list,
                "type": 'connections'
            }
            DocumentWS.send_updates(message, document_id)

    @classmethod
    def send_updates(cls, message, document_id, sender_id=None):
        info("sending message to %d waiters", len(cls.sessions[document_id]))
        for waiter in cls.sessions[document_id]['participants'].keys():
            if cls.sessions[document_id][
                    'participants'][waiter].id != sender_id:
                try:
                    cls.sessions[document_id]['participants'][
                        waiter].write_message(message)
                except WebSocketClosedError:
                    error("Error sending message", exc_info=True)

    @classmethod
    def save_document(cls, document_id):
        doc = cls.sessions[document_id]
        doc_db = doc['db']
        doc_db.title = doc['title']
        doc_db.version = doc['version']
        doc_db.diff_version = doc['diff_version']
        doc_db.comment_version = doc['comment_version']
        doc_db.contents = json_encode(doc['contents'])
        doc_db.metadata = json_encode(doc['metadata'])
        doc_db.settings = json_encode(doc['settings'])
        doc_db.last_diffs = json_encode(doc['last_diffs'])
        doc_db.comments = json_encode(doc['comments'])
        print('saving document #' + str(doc_db.id))
        print('version ' + str(doc_db.version))
        doc_db.save()

    @classmethod
    def save_all_docs(cls):
        for document_id in cls.sessions:
            cls.save_document(document_id)
示例#6
0
class WebSocket(BaseWebSocketHandler):
    sessions = dict()

    def open(self, arg):
        super().open(arg)
        if len(self.args) < 2:
            self.access_denied()
            return
        self.document_id = int(self.args[0])

    def confirm_diff(self, rid):
        response = {'type': 'confirm_diff', 'rid': rid}
        self.send_message(response)

    def subscribe_doc(self, connection_count=0):
        self.user_info = SessionUserInfo(self.user)
        doc_db, can_access = self.user_info.init_access(self.document_id)
        if not can_access or float(doc_db.doc_version) != FW_DOCUMENT_VERSION:
            self.access_denied()
            return
        if (doc_db.id in WebSocket.sessions
                and len(WebSocket.sessions[doc_db.id]['participants']) > 0):
            logger.debug("Serving already opened file")
            self.doc = WebSocket.sessions[doc_db.id]
            self.id = max(self.doc['participants']) + 1
            self.doc['participants'][self.id] = self
            logger.debug("id when opened %s" % self.id)
        else:
            logger.debug("Opening file")
            self.id = 0
            self.doc = {
                'db': doc_db,
                'participants': {
                    0: self
                },
                'last_diffs': json_decode(doc_db.last_diffs),
                'comments': json_decode(doc_db.comments),
                'bibliography': json_decode(doc_db.bibliography),
                'contents': json_decode(doc_db.contents),
                'version': doc_db.version,
                'title': doc_db.title,
                'id': doc_db.id,
                'template': {
                    'id': doc_db.template.id,
                    'definition': json_decode(doc_db.template.definition)
                }
            }
            WebSocket.sessions[doc_db.id] = self.doc
        self.send_message({'type': 'subscribed'})
        if connection_count < 1:
            self.send_styles()
            self.send_document()
        if self.can_communicate():
            self.handle_participant_update()

    def send_styles(self):
        doc_db = self.doc['db']
        response = dict()
        response['type'] = 'styles'
        serializer = PythonWithURLSerializer()
        export_temps = serializer.serialize(
            doc_db.template.exporttemplate_set.all(),
            fields=['file_type', 'template_file', 'title'])
        document_styles = serializer.serialize(
            doc_db.template.documentstyle_set.all(),
            use_natural_foreign_keys=True,
            fields=['title', 'slug', 'contents', 'documentstylefile_set'])

        response['styles'] = {
            'export_templates': [obj['fields'] for obj in export_temps],
            'document_styles': [obj['fields'] for obj in document_styles]
        }
        self.send_message(response)

    def send_document(self):
        response = dict()
        response['type'] = 'doc_data'
        doc_owner = self.doc['db'].owner
        response['doc_info'] = {
            'id': self.doc['id'],
            'is_owner': self.user_info.is_owner,
            'access_rights': self.user_info.access_rights,
            'owner': {
                'id': doc_owner.id,
                'name': doc_owner.readable_name,
                'username': doc_owner.username,
                'avatar': get_user_avatar_url(doc_owner),
                'team_members': []
            }
        }
        response['doc'] = {
            'v': self.doc['version'],
            'contents': self.doc['contents'],
            'bibliography': self.doc['bibliography'],
            'template': self.doc['template'],
            'images': {}
        }
        response['time'] = int(time()) * 1000
        for dimage in DocumentImage.objects.filter(document_id=self.doc['id']):
            image = dimage.image
            field_obj = {
                'id': image.id,
                'title': dimage.title,
                'image': image.image.url,
                'file_type': image.file_type,
                'added': mktime(image.added.timetuple()) * 1000,
                'checksum': image.checksum,
                'cats': []
            }
            if image.thumbnail:
                field_obj['thumbnail'] = image.thumbnail.url
                field_obj['height'] = image.height
                field_obj['width'] = image.width
            response['doc']['images'][image.id] = field_obj
        if self.user_info.access_rights == 'read-without-comments':
            response['doc']['comments'] = []
        elif self.user_info.access_rights == 'review':
            # Reviewer should only get his/her own comments
            filtered_comments = {}
            for key, value in list(self.doc["comments"].items()):
                if value["user"] == self.user_info.user.id:
                    filtered_comments[key] = value
            response['doc']['comments'] = filtered_comments
        else:
            response['doc']['comments'] = self.doc["comments"]
        for team_member in doc_owner.leader.all():
            tm_object = dict()
            tm_object['id'] = team_member.member.id
            tm_object['name'] = team_member.member.readable_name
            tm_object['username'] = team_member.member.get_username()
            tm_object['avatar'] = get_user_avatar_url(team_member.member)
            response['doc_info']['owner']['team_members'].append(tm_object)
        response['doc_info']['session_id'] = self.id
        self.send_message(response)

    def reject_message(self, message):
        if (message["type"] == "diff"):
            self.send_message({'type': 'reject_diff', 'rid': message['rid']})

    def handle_message(self, message):
        if message["type"] == 'subscribe':
            connection_count = 0
            if 'connection' in message:
                connection_count = message['connection']
            self.subscribe_doc(connection_count)
            return
        if self.user_info.document_id not in WebSocket.sessions:
            logger.debug('receiving message for closed document')
            return
        if message["type"] == 'get_document':
            self.send_document()
        elif (message["type"] == 'participant_update'
              and self.can_communicate()):
            self.handle_participant_update()
        elif message["type"] == 'chat' and self.can_communicate():
            self.handle_chat(message)
        elif message["type"] == 'check_version':
            self.check_version(message)
        elif message["type"] == 'selection_change':
            self.handle_selection_change(message)
        elif message["type"] == 'diff' and self.can_update_document():
            self.handle_diff(message)

    def update_bibliography(self, bibliography_updates):
        for bu in bibliography_updates:
            if "id" not in bu:
                continue
            id = bu["id"]
            if bu["type"] == "update":
                self.doc["bibliography"][id] = bu["reference"]
            elif bu["type"] == "delete":
                del self.doc["bibliography"][id]

    def update_images(self, image_updates):
        for iu in image_updates:
            if "id" not in iu:
                continue
            id = iu["id"]
            if iu["type"] == "update":
                # Ensure that access rights exist
                if not UserImage.objects.filter(
                        image__id=id, owner=self.user_info.user).exists():
                    continue
                doc_image = DocumentImage.objects.filter(
                    document_id=self.doc["id"], image_id=id)
                if doc_image.exists():
                    doc_image.title = iu["image"]["title"]
                    doc_image.save()
                else:
                    DocumentImage.objects.create(document_id=self.doc["id"],
                                                 image_id=id,
                                                 title=iu["image"]["title"])
            elif iu["type"] == "delete":
                DocumentImage.objects.filter(document_id=self.doc["id"],
                                             image_id=id).delete()
                for image in Image.objects.filter(id=id):
                    if image.is_deletable():
                        image.delete()

    def update_comments(self, comments_updates):
        comments_updates = deepcopy(comments_updates)
        for cd in comments_updates:
            if "id" not in cd:
                # ignore
                continue
            id = cd["id"]
            if cd["type"] == "create":
                self.doc["comments"][id] = {
                    "user": cd["user"],
                    "username": cd["username"],
                    "assignedUser": cd["assignedUser"],
                    "assignedUsername": cd["assignedUsername"],
                    "date": cd["date"],
                    "comment": cd["comment"],
                    "isMajor": cd["isMajor"],
                    "resolved": cd["resolved"],
                }
            elif cd["type"] == "delete":
                del self.doc["comments"][id]
            elif cd["type"] == "update":
                self.doc["comments"][id]["comment"] = cd["comment"]
                if "isMajor" in cd:
                    self.doc["comments"][id]["isMajor"] = cd["isMajor"]
                if "assignedUser" in cd and "assignedUsername" in cd:
                    self.doc["comments"][id]["assignedUser"] = cd[
                        "assignedUser"]
                    self.doc["comments"][id]["assignedUsername"] = cd[
                        "assignedUsername"]
                if "resolved" in cd:
                    self.doc["comments"][id]["resolved"] = cd["resolved"]
            elif cd["type"] == "add_answer":
                if "answers" not in self.doc["comments"][id]:
                    self.doc["comments"][id]["answers"] = []
                self.doc["comments"][id]["answers"].append({
                    "id":
                    cd["answerId"],
                    "user":
                    cd["user"],
                    "username":
                    cd["username"],
                    "date":
                    cd["date"],
                    "answer":
                    cd["answer"]
                })
            elif cd["type"] == "delete_answer":
                answer_id = cd["answerId"]
                for answer in self.doc["comments"][id]["answers"]:
                    if answer["id"] == answer_id:
                        self.doc["comments"][id]["answers"].remove(answer)
            elif cd["type"] == "update_answer":
                answer_id = cd["answerId"]
                for answer in self.doc["comments"][id]["answers"]:
                    if answer["id"] == answer_id:
                        answer["answer"] = cd["answer"]

    def handle_participant_update(self):
        WebSocket.send_participant_list(self.user_info.document_id)

    def handle_chat(self, message):
        chat = {
            "id": str(uuid.uuid4()),
            "body": message['body'],
            "from": self.user_info.user.id,
            "type": 'chat'
        }
        WebSocket.send_updates(chat, self.user_info.document_id)

    def handle_selection_change(self, message):
        if self.user_info.document_id in WebSocket.sessions and message[
                "v"] == self.doc['version']:
            WebSocket.send_updates(message, self.user_info.document_id,
                                   self.id)

    # Checks if the diff only contains changes to comments.
    def only_comments(self, message):
        allowed_operations = ['addMark', 'removeMark']
        only_comment = True
        if "ds" in message:  # ds = document steps
            for step in message["ds"]:
                if not (step['stepType'] in allowed_operations
                        and step['mark']['type'] == 'comment'):
                    only_comment = False
        return only_comment

    def handle_diff(self, message):
        pv = message["v"]
        dv = self.doc['version']
        logger.debug("PV: %d, DV: %d" % (pv, dv))
        if (self.user_info.access_rights in COMMENT_ONLY
                and not self.only_comments(message)):
            logger.error(('received non-comment diff from comment-only '
                          'collaborator. Discarding.'))
            return
        if pv == dv:
            self.doc["last_diffs"].append(message)
            # Only keep the last 1000 diffs
            self.doc["last_diffs"] = self.doc["last_diffs"][-1000:]
            self.doc['version'] += 1
            if "jd" in message:  # jd = json diff
                try:
                    apply_patch(self.doc['contents'], message["jd"], True)
                except (JsonPatchConflict, JsonPointerException):
                    logger.exception("Cannot apply json diff.")
                    logger.error(json_encode(message))
                    logger.error(json_encode(self.doc['contents']))
                    self.send_document()
                # The json diff is only needed by the python backend which does
                # not understand the steps. It can therefore be removed before
                # broadcast to other clients.
                del message["jd"]
            if "ti" in message:  # ti = title
                self.doc["title"] = message["ti"]
            if "cu" in message:  # cu = comment updates
                self.update_comments(message["cu"])
            if "bu" in message:  # bu = bibliography updates
                self.update_bibliography(message["bu"])
            if "iu" in message:  # iu = image updates
                self.update_images(message["iu"])
            if self.doc['version'] % 10 == 0:
                WebSocket.save_document(self.user_info.document_id)
            self.confirm_diff(message["rid"])
            WebSocket.send_updates(message, self.user_info.document_id,
                                   self.id, self.user_info.user.id)
        elif pv < dv:
            if pv + len(self.doc["last_diffs"]) >= dv:
                # We have enough last_diffs stored to fix it.
                logger.debug("can fix it")
                number_diffs = pv - dv
                messages = self.doc["last_diffs"][number_diffs:]
                for message in messages:
                    new_message = message.copy()
                    new_message["server_fix"] = True
                    self.send_message(new_message)
            else:
                logger.debug('unfixable')
                # Client has a version that is too old to be fixed
                self.send_document()
        else:
            # Client has a higher version than server. Something is fishy!
            logger.debug('unfixable')

    def check_version(self, message):
        pv = message["v"]
        dv = self.doc['version']
        logger.debug("PV: %d, DV: %d" % (pv, dv))
        if pv == dv:
            response = {
                "type": "confirm_version",
                "v": pv,
            }
            self.send_message(response)
            return
        elif pv + len(self.doc["last_diffs"]) >= dv:
            logger.debug("can fix it")
            number_diffs = pv - dv
            messages = self.doc["last_diffs"][number_diffs:]
            for message in messages:
                new_message = message.copy()
                new_message["server_fix"] = True
                self.send_message(new_message)
            return
        else:
            logger.debug('unfixable')
            # Client has a version that is too old
            self.send_document()
            return

    def can_update_document(self):
        return self.user_info.access_rights in CAN_UPDATE_DOCUMENT

    def can_communicate(self):
        return self.user_info.access_rights in CAN_COMMUNICATE

    def on_close(self):
        logger.debug('Websocket closing')
        if (hasattr(self, 'user_info')
                and hasattr(self.user_info, 'document_id')
                and self.user_info.document_id in WebSocket.sessions
                and hasattr(self, 'id') and self.id in WebSocket.sessions[
                    self.user_info.document_id]['participants']):
            del self.doc['participants'][self.id]
            if len(self.doc['participants']) == 0:
                WebSocket.save_document(self.user_info.document_id)
                del WebSocket.sessions[self.user_info.document_id]
                logger.debug("noone left")
            else:
                WebSocket.send_participant_list(self.user_info.document_id)

    @classmethod
    def send_participant_list(cls, document_id):
        if document_id in WebSocket.sessions:
            participant_list = []
            for session_id, waiter in list(
                    cls.sessions[document_id]['participants'].items()):
                access_rights = waiter.user_info.access_rights
                if access_rights not in CAN_COMMUNICATE:
                    continue
                participant_list.append({
                    'session_id':
                    session_id,
                    'id':
                    waiter.user_info.user.id,
                    'name':
                    waiter.user_info.user.readable_name,
                    'avatar':
                    get_user_avatar_url(waiter.user_info.user)
                })
            message = {
                "participant_list": participant_list,
                "type": 'connections'
            }
            WebSocket.send_updates(message, document_id)

    @classmethod
    def send_updates(cls, message, document_id, sender_id=None, user_id=None):
        logger.debug("Sending message to %d waiters",
                     len(cls.sessions[document_id]['participants']))
        for waiter in list(cls.sessions[document_id]['participants'].values()):
            if waiter.id != sender_id:
                access_rights = waiter.user_info.access_rights
                if "comments" in message and len(message["comments"]) > 0:
                    # Filter comments if needed
                    if access_rights == 'read-without-comments':
                        # The reader should not receive the comments update, so
                        # we remove the comments from the copy of the message
                        # sent to the reviewer. We still need to send the rest
                        # of the message as it may contain other diff
                        # information.
                        message = deepcopy(message)
                        message['comments'] = []
                    elif (access_rights == 'review'
                          and user_id != waiter.user_info.user.id):
                        # The reviewer should not receive comments updates from
                        # others than themselves, so we remove the comments
                        # from the copy of the message sent to the reviewer
                        # that are not from them. We still need to sned the
                        # rest of the message as it may contain other diff
                        # information.
                        message = deepcopy(message)
                        message['comments'] = []
                elif (message['type'] in ["chat", "connections"]
                      and access_rights not in CAN_COMMUNICATE):
                    continue
                elif (message['type'] == "selection_change"
                      and access_rights not in CAN_COMMUNICATE
                      and user_id != waiter.user_info.user.id):
                    continue
                waiter.send_message(message)

    @classmethod
    def save_document(cls, document_id):
        doc = cls.sessions[document_id]
        doc_db = doc['db']
        if doc_db.version == doc['version']:
            return
        doc_db.title = doc['title'][-255:]
        doc_db.version = doc['version']
        doc_db.contents = json_encode(doc['contents'])
        doc_db.last_diffs = json_encode(doc['last_diffs'])
        doc_db.comments = json_encode(doc['comments'])
        doc_db.bibliography = json_encode(doc['bibliography'])
        logger.debug('saving document # %d' % doc_db.id)
        logger.debug('version %d' % doc_db.version)
        doc_db.save()

    @classmethod
    def save_all_docs(cls):
        for document_id in cls.sessions:
            cls.save_document(document_id)
示例#7
0
class DocumentWS(BaseWebSocketHandler):
    sessions = dict()

    def open(self, document_id):
        print('Websocket opened')
        current_user = self.get_current_user()
        self.user_info = SessionUserInfo()
        doc_db, can_access = self.user_info.init_access(
            document_id, current_user)

        if can_access:
            if doc_db.id in DocumentWS.sessions:
                self.doc = DocumentWS.sessions[doc_db.id]
                self.id = max(self.doc['participants']) + 1
                print "id when opened %s" % self.id
            else:
                self.id = 0
                self.doc = dict()
                self.doc['db'] = doc_db
                self.doc['participants'] = dict()
                self.doc['last_diffs'] = json_decode(doc_db.last_diffs)
                self.doc['comments'] = json_decode(doc_db.comments)
                self.doc['settings'] = json_decode(doc_db.settings)
                self.doc['contents'] = json_decode(doc_db.contents)
                self.doc['metadata'] = json_decode(doc_db.metadata)
                self.doc['version'] = doc_db.version
                self.doc['diff_version'] = doc_db.diff_version
                self.doc['comment_version'] = doc_db.comment_version
                self.doc['title'] = doc_db.title
                self.doc['id'] = doc_db.id
                DocumentWS.sessions[doc_db.id] = self.doc
            self.doc['participants'][self.id] = self
            response = dict()
            response['type'] = 'welcome'
            self.write_message(response)

    def confirm_diff(self, request_id):
        response = dict()
        response['type'] = 'confirm_diff'
        response['request_id'] = request_id
        self.write_message(response)

    def send_document(self):
        response = dict()
        response['type'] = 'document_data'
        response['document'] = dict()
        response['document']['id'] = self.doc['id']
        response['document']['version'] = self.doc['version']
        if self.doc['diff_version'] < self.doc['version']:
            print('!!!diff version issue!!!')
            self.doc['diff_version'] = self.doc['version']
            self.doc["last_diffs"] = []
        response['document']['title'] = self.doc['title']
        response['document']['contents'] = self.doc['contents']
        response['document']['metadata'] = self.doc['metadata']
        response['document']['settings'] = self.doc['settings']
        document_owner = self.doc['db'].owner
        access_rights = get_accessrights(
            AccessRight.objects.filter(document__owner=document_owner))
        response['document']['access_rights'] = access_rights

        # TODO: switch on filtering when choose workflow and have UI for
        # assigning roles to users
        # filtered_comments = filter_comments_by_role(
        #     DocumentWS.sessions[self.user_info.document_id]["comments"],
        #     access_rights,
        #     'editing',
        #     self.user_info
        # )
        response['document']['comments'] = self.doc["comments"]
        # response['document']['comments'] = filtered_comments
        response['document']['comment_version'] = self.doc["comment_version"]
        response['document']['access_rights'] = get_accessrights(
            AccessRight.objects.filter(document__owner=document_owner))
        response['document']['owner'] = dict()
        response['document']['owner']['id'] = document_owner.id
        response['document']['owner']['name'] = document_owner.readable_name
        response['document']['owner']['avatar'] = avatar_url(
            document_owner, 80)
        response['document']['owner']['team_members'] = []

        for team_member in document_owner.leader.all():
            tm_object = dict()
            tm_object['id'] = team_member.member.id
            tm_object['name'] = team_member.member.readable_name
            tm_object['avatar'] = avatar_url(team_member.member, 80)
            response['document']['owner']['team_members'].append(tm_object)
        response['document_values'] = dict()
        response['document_values']['is_owner'] = self.user_info.is_owner
        response['document_values']['rights'] = self.user_info.access_rights
        if self.doc['version'] > self.doc['diff_version']:
            print('!!!diff version issue!!!')
            self.doc['diff_version'] = self.doc['version']
            self.doc["last_diffs"] = []
        elif self.doc['diff_version'] > self.doc['version']:
            needed_diffs = self.doc['diff_version'] - self.doc['version']
            response['document_values']['last_diffs'] = self.doc["last_diffs"][
                -needed_diffs:]
        else:
            response['document_values']['last_diffs'] = []
        if self.user_info.is_new:
            response['document_values']['is_new'] = True
        if not self.user_info.is_owner:
            response['user'] = dict()
            response['user']['id'] = self.user_info.user.id
            response['user']['name'] = self.user_info.user.readable_name
            response['user']['avatar'] = avatar_url(self.user_info.user, 80)


#        if self.doc['in_control'] == self.id:
#            response['document_values']['control']=True
        response['document_values']['session_id'] = self.id
        self.write_message(response)

    def on_message(self, message):
        if self.user_info.document_id not in DocumentWS.sessions:
            print('receiving message for closed document')
            return
        parsed = json_decode(message)
        print(parsed["type"])
        if parsed["type"] == 'get_document':
            self.send_document()
        elif parsed["type"] == 'participant_update':
            self.handle_participant_update()
        elif parsed["type"] == 'chat':
            self.handle_chat(parsed)
        elif parsed["type"] == 'check_diff_version':
            self.check_diff_version(parsed)
        elif parsed["type"] == 'selection_change':
            self.handle_selection_change(message, parsed)
        elif (parsed["type"] == 'update_document'
              and self.can_update_document()):
            self.handle_document_update(parsed)
        elif parsed["type"] == 'update_title' and self.can_update_document():
            self.handle_title_update(parsed)
        elif parsed["type"] == 'setting_change' and self.can_update_document():
            self.handle_settings_change(message, parsed)
        elif parsed["type"] == 'diff' and self.can_update_document():
            self.handle_diff(message, parsed)

    def update_document(self, changes):
        if changes['version'] == self.doc['version']:
            # Document hasn't changed, return.
            return
        elif (changes['version'] > self.doc['diff_version']
              or changes['version'] < self.doc['version']):
            # The version number is too high. Possibly due to server restart.
            # Do not accept it, and send a document instead.
            self.send_document()
            return
        else:
            # The saved version does not contain all accepted diffs, so we keep
            # the remaining ones + 1000
            remaining_diffs = 1000 + \
                self.doc['diff_version'] - changes['version']
            self.doc['last_diffs'] = self.doc['last_diffs'][-remaining_diffs:]
        self.doc['title'] = changes['title']
        self.doc['contents'] = changes['contents']
        self.doc['metadata'] = changes['metadata']
        self.doc['version'] = changes['version']

    def update_title(self, title):
        self.doc['title'] = title

    def update_comments(self, comments_updates):
        for cd in comments_updates:
            id = str(cd["id"])
            if cd["type"] == "create":
                del cd["type"]
                self.doc["comments"][id] = cd
            elif cd["type"] == "delete":
                del self.doc["comments"][id]
            elif cd["type"] == "update":
                self.doc["comments"][id]["comment"] = cd["comment"]
                if "review:isMajor" in cd:
                    self.doc["comments"][id]["review:isMajor"] = cd[
                        "review:isMajor"]
            elif cd["type"] == "add_answer":
                comment_id = str(cd["commentId"])
                if "answers" not in self.doc["comments"][comment_id]:
                    self.doc["comments"][comment_id]["answers"] = []
                del cd["type"]
                self.doc["comments"][comment_id]["answers"].append(cd)
            elif cd["type"] == "delete_answer":
                comment_id = str(cd["commentId"])
                for answer in self.doc["comments"][comment_id]["answers"]:
                    if answer["id"] == cd["id"]:
                        self.doc["comments"][comment_id]["answers"].remove(
                            answer)
            elif cd["type"] == "update_answer":
                comment_id = str(cd["commentId"])
                for answer in self.doc["comments"][comment_id]["answers"]:
                    if answer["id"] == cd["id"]:
                        answer["answer"] = cd["answer"]
            self.doc['comment_version'] += 1

    def handle_participant_update(self):
        DocumentWS.send_participant_list(self.user_info.document_id)

    def handle_document_update(self, parsed):
        self.update_document(parsed["document"])
        DocumentWS.save_document(self.user_info.document_id)
        message = {
            "type": 'check_hash',
            "diff_version": parsed["document"]["version"],
            "hash": parsed["document"]["hash"]
        }
        DocumentWS.send_updates(message, self.user_info.document_id, self.id)

    def handle_title_update(self, parsed):
        self.update_title(parsed["title"])
        DocumentWS.save_document(self.user_info.document_id)

    def handle_chat(self, parsed):
        chat = {
            "id": str(uuid.uuid4()),
            "body": parsed['body'],
            "from": self.user_info.user.id,
            "type": 'chat'
        }
        DocumentWS.send_updates(chat, self.user_info.document_id)

    def handle_selection_change(self, message, parsed):
        if self.user_info.document_id in DocumentWS.sessions and parsed[
                "diff_version"] == self.doc['diff_version']:
            DocumentWS.send_updates(message, self.user_info.document_id,
                                    self.id)

    def handle_settings_change(self, message, parsed):
        DocumentWS.sessions[self.user_info.document_id]['settings'][
            parsed['variable']] = parsed['value']
        DocumentWS.send_updates(message, self.user_info.document_id, self.id)

    # Checks if the diff only contains changes to comments.
    def only_comments(self, parsed_diffs):
        allowed_operations = ['addMark', 'removeMark']
        only_comment = True
        for diff in parsed_diffs:
            if not (diff['type'] in allowed_operations
                    and diff['param']['_'] == 'comment'):
                only_comment = False
        return only_comment

    def handle_diff(self, message, parsed):
        if (self.user_info.access_rights in COMMENT_ONLY
                and not self.only_comments(parsed['diff'])):
            print(('received non-comment diff from comment-only '
                   'collaborator. Discarding.'))
            return
        if parsed["diff_version"] == self.doc['diff_version'] and parsed[
                "comment_version"] == self.doc['comment_version']:
            self.doc["last_diffs"].extend(parsed["diff"])
            self.doc['diff_version'] += len(parsed["diff"])
            self.update_comments(parsed["comments"])
            self.confirm_diff(parsed["request_id"])
            DocumentWS.send_updates(message, self.user_info.document_id,
                                    self.id)
        elif parsed["diff_version"] != self.doc['diff_version']:
            if parsed["diff_version"] < (self.doc['diff_version'] -
                                         len(self.doc["last_diffs"])):
                print('unfixable')
                # Client has a version that is too old
                self.send_document()
            elif parsed["diff_version"] < self.doc['diff_version']:
                print "can fix it"
                number_requested_diffs = self.doc['diff_version'] - parsed[
                    "diff_version"]
                response = {
                    "type": "diff",
                    "diff_version": parsed["diff_version"],
                    "diff": self.doc["last_diffs"][-number_requested_diffs:],
                    "reject_request_id": parsed["request_id"],
                }
                self.write_message(response)
            else:
                print('unfixable')
                # Client has a version that is too old
                self.send_document()
        else:
            print('comment_version incorrect!')
            print(parsed["comment_version"])
            print(self.doc['comment_version'])

    def check_diff_version(self, parsed):
        pdv = parsed["diff_version"]
        ddv = self.doc['diff_version']
        if pdv == ddv:
            response = {
                "type": "confirm_diff_version",
                "diff_version": pdv,
            }
            self.write_message(response)
            return
        elif pdv + len(self.doc["last_diffs"]) >= ddv:
            number_requested_diffs = ddv - pdv
            response = {
                "type": "diff",
                "diff_version": parsed["diff_version"],
                "diff": self.doc["last_diffs"][-number_requested_diffs:],
            }
            self.write_message(response)
            return
        else:
            print('unfixable')
            # Client has a version that is too old
            self.send_document()
            return

    def can_update_document(self):
        return self.user_info.access_rights in CAN_UPDATE_DOCUMENT

    def on_close(self):
        print('Websocket closing')
        if (hasattr(self.user_info, 'document_id')
                and self.user_info.document_id in DocumentWS.sessions
                and hasattr(self, 'id') and self.id in DocumentWS.sessions[
                    self.user_info.document_id]['participants']):
            del self.doc['participants'][self.id]
            if len(self.doc['participants'].keys()) == 0:
                DocumentWS.save_document(self.user_info.document_id)
                del DocumentWS.sessions[self.user_info.document_id]
                print "noone left"

    @classmethod
    def send_participant_list(cls, document_id):
        if document_id in DocumentWS.sessions:
            participant_list = []
            for waiter in cls.sessions[document_id]['participants'].keys():
                participant_list.append({
                    'session_id':
                    waiter,
                    'id':
                    cls.sessions[document_id]['participants']
                    [waiter].user_info.user.id,
                    'name':
                    cls.sessions[document_id]['participants']
                    [waiter].user_info.user.readable_name,
                    'avatar':
                    avatar_url(
                        cls.sessions[document_id]['participants']
                        [waiter].user_info.user, 80)
                })
            message = {
                "participant_list": participant_list,
                "type": 'connections'
            }
            DocumentWS.send_updates(message, document_id)

    @classmethod
    def send_updates(cls, message, document_id, sender_id=None):
        info("sending message to %d waiters", len(cls.sessions[document_id]))
        for waiter in cls.sessions[document_id]['participants'].keys():
            if cls.sessions[document_id]['participants'][
                    waiter].id != sender_id:
                try:
                    cls.sessions[document_id]['participants'][
                        waiter].write_message(message)
                except WebSocketClosedError:
                    error("Error sending message", exc_info=True)

    @classmethod
    def save_document(cls, document_id):
        doc = cls.sessions[document_id]
        doc_db = doc['db']
        doc_db.title = doc['title']
        doc_db.version = doc['version']
        doc_db.diff_version = doc['diff_version']
        doc_db.comment_version = doc['comment_version']
        doc_db.contents = json_encode(doc['contents'])
        doc_db.metadata = json_encode(doc['metadata'])
        doc_db.settings = json_encode(doc['settings'])
        doc_db.last_diffs = json_encode(doc['last_diffs'])
        doc_db.comments = json_encode(doc['comments'])
        print('saving document #' + str(doc_db.id))
        print('version ' + str(doc_db.version))
        doc_db.save()

    @classmethod
    def save_all_docs(cls):
        for document_id in cls.sessions:
            cls.save_document(document_id)
示例#8
0
 def open(self, arg):
     logger.debug('Websocket opened')
     response = dict()
     current_user = self.get_current_user()
     args = arg.split("/")
     self.messages = {'server': 0, 'client': 0, 'last_ten': []}
     if len(args) < 3 or current_user is None:
         response['type'] = 'access_denied'
         self.id = 0
         self.send_message(response)
         IOLoop.current().add_callback(self.do_close)
         return
     document_id = int(args[0])
     connection_count = int(args[1])
     self.user_info = SessionUserInfo()
     doc_db, can_access = self.user_info.init_access(
         document_id, current_user)
     if not can_access or doc_db.doc_version != FW_DOCUMENT_VERSION:
         response['type'] = 'access_denied'
         self.id = 0
         self.send_message(response)
         return
     response['type'] = 'welcome'
     if (doc_db.id in WebSocket.sessions
             and len(WebSocket.sessions[doc_db.id]['participants']) > 0):
         logger.debug("Serving already opened file")
         self.doc = WebSocket.sessions[doc_db.id]
         self.id = max(self.doc['participants']) + 1
         self.doc['participants'][self.id] = self
         logger.debug("id when opened %s" % self.id)
     else:
         logger.debug("Opening file")
         self.id = 0
         self.doc = {
             'db': doc_db,
             'participants': {
                 0: self
             },
             'last_diffs': json_decode(doc_db.last_diffs),
             'comments': json_decode(doc_db.comments),
             'bibliography': json_decode(doc_db.bibliography),
             'contents': json_decode(doc_db.contents),
             'version': doc_db.version,
             'title': doc_db.title,
             'id': doc_db.id
         }
         WebSocket.sessions[doc_db.id] = self.doc
     serializer = PythonWithURLSerializer()
     export_temps = serializer.serialize(ExportTemplate.objects.all())
     document_styles = serializer.serialize(DocumentStyle.objects.all(),
                                            use_natural_foreign_keys=True)
     cite_styles = serializer.serialize(CitationStyle.objects.all())
     cite_locales = serializer.serialize(CitationLocale.objects.all())
     response['styles'] = {
         'export_templates': [obj['fields'] for obj in export_temps],
         'document_styles': [obj['fields'] for obj in document_styles],
         'citation_styles': [obj['fields'] for obj in cite_styles],
         'citation_locales': [obj['fields'] for obj in cite_locales],
     }
     self.send_message(response)
     if connection_count < 1:
         self.send_document()
     if self.can_communicate():
         self.handle_participant_update()
示例#9
0
class WebSocket(BaseWebSocketHandler):
    sessions = dict()

    def open(self, arg):
        logger.debug('Websocket opened')
        response = dict()
        current_user = self.get_current_user()
        args = arg.split("/")
        self.messages = {'server': 0, 'client': 0, 'last_ten': []}
        if len(args) < 3 or current_user is None:
            response['type'] = 'access_denied'
            self.id = 0
            self.send_message(response)
            IOLoop.current().add_callback(self.do_close)
            return
        document_id = int(args[0])
        connection_count = int(args[1])
        self.user_info = SessionUserInfo()
        doc_db, can_access = self.user_info.init_access(
            document_id, current_user)
        if not can_access or doc_db.doc_version != FW_DOCUMENT_VERSION:
            response['type'] = 'access_denied'
            self.id = 0
            self.send_message(response)
            return
        response['type'] = 'welcome'
        if (doc_db.id in WebSocket.sessions
                and len(WebSocket.sessions[doc_db.id]['participants']) > 0):
            logger.debug("Serving already opened file")
            self.doc = WebSocket.sessions[doc_db.id]
            self.id = max(self.doc['participants']) + 1
            self.doc['participants'][self.id] = self
            logger.debug("id when opened %s" % self.id)
        else:
            logger.debug("Opening file")
            self.id = 0
            self.doc = {
                'db': doc_db,
                'participants': {
                    0: self
                },
                'last_diffs': json_decode(doc_db.last_diffs),
                'comments': json_decode(doc_db.comments),
                'bibliography': json_decode(doc_db.bibliography),
                'contents': json_decode(doc_db.contents),
                'version': doc_db.version,
                'title': doc_db.title,
                'id': doc_db.id
            }
            WebSocket.sessions[doc_db.id] = self.doc
        serializer = PythonWithURLSerializer()
        export_temps = serializer.serialize(ExportTemplate.objects.all())
        document_styles = serializer.serialize(DocumentStyle.objects.all(),
                                               use_natural_foreign_keys=True)
        cite_styles = serializer.serialize(CitationStyle.objects.all())
        cite_locales = serializer.serialize(CitationLocale.objects.all())
        response['styles'] = {
            'export_templates': [obj['fields'] for obj in export_temps],
            'document_styles': [obj['fields'] for obj in document_styles],
            'citation_styles': [obj['fields'] for obj in cite_styles],
            'citation_locales': [obj['fields'] for obj in cite_locales],
        }
        self.send_message(response)
        if connection_count < 1:
            self.send_document()
        if self.can_communicate():
            self.handle_participant_update()

    def do_close(self):
        self.close()

    def confirm_diff(self, rid):
        response = {'type': 'confirm_diff', 'rid': rid}
        self.send_message(response)

    def send_document(self):
        response = dict()
        response['type'] = 'doc_data'
        doc_owner = self.doc['db'].owner
        response['doc_info'] = {
            'id': self.doc['id'],
            'is_owner': self.user_info.is_owner,
            'access_rights': self.user_info.access_rights,
            'owner': {
                'id': doc_owner.id,
                'name': doc_owner.readable_name,
                'avatar': avatar_url(doc_owner, 80),
                'team_members': []
            }
        }
        response['doc'] = {
            'v': self.doc['version'],
            'contents': self.doc['contents'],
            'bibliography': self.doc['bibliography'],
            'images': {}
        }
        for dimage in DocumentImage.objects.filter(document_id=self.doc['id']):
            image = dimage.image
            field_obj = {
                'id': image.id,
                'title': dimage.title,
                'image': image.image.url,
                'file_type': image.file_type,
                'added': mktime(image.added.timetuple()) * 1000,
                'checksum': image.checksum,
                'cats': []
            }
            if image.thumbnail:
                field_obj['thumbnail'] = image.thumbnail.url
                field_obj['height'] = image.height
                field_obj['width'] = image.width
            response['doc']['images'][image.id] = field_obj
        if self.user_info.access_rights == 'read-without-comments':
            response['doc']['comments'] = []
        elif self.user_info.access_rights == 'review':
            # Reviewer should only get his/her own comments
            filtered_comments = {}
            for key, value in self.doc["comments"].items():
                if value["user"] == self.user_info.user.id:
                    filtered_comments[key] = value
            response['doc']['comments'] = filtered_comments
        else:
            response['doc']['comments'] = self.doc["comments"]
        for team_member in doc_owner.leader.all():
            tm_object = dict()
            tm_object['id'] = team_member.member.id
            tm_object['name'] = team_member.member.readable_name
            tm_object['avatar'] = avatar_url(team_member.member, 80)
            response['doc_info']['owner']['team_members'].append(tm_object)
        collaborators = get_accessrights(
            AccessRight.objects.filter(document__owner=doc_owner))
        response['doc_info']['collaborators'] = collaborators
        if self.user_info.is_owner:
            the_user = self.user_info.user
            response['doc_info']['owner']['email'] = the_user.email
            response['doc_info']['owner']['username'] = the_user.username
            response['doc_info']['owner']['first_name'] = the_user.first_name
            response['doc_info']['owner']['last_name'] = the_user.last_name
        else:
            the_user = self.user_info.user
            response['user'] = dict()
            response['user']['id'] = the_user.id
            response['user']['name'] = the_user.readable_name
            response['user']['avatar'] = avatar_url(the_user, 80)
            response['user']['email'] = the_user.email
            response['user']['username'] = the_user.username
            response['user']['first_name'] = the_user.first_name
            response['user']['last_name'] = the_user.last_name
        response['doc_info']['session_id'] = self.id
        self.send_message(response)

    def on_message(self, message):
        if self.user_info.document_id not in WebSocket.sessions:
            logger.debug('receiving message for closed document')
            return
        parsed = json_decode(message)
        if parsed["type"] == 'request_resend':
            self.resend_messages(parsed["from"])
            return
        if 'c' not in parsed and 's' not in parsed:
            self.write_message({'type': 'access_denied'})
            # Message doesn't contain needed client/server info. Ignore.
            return
        logger.debug("Type %s, server %d, client %d, id %d" %
                     (parsed["type"], parsed["s"], parsed["c"], self.id))
        if parsed["c"] < (self.messages["client"] + 1):
            # Receive a message already received at least once. Ignore.
            return
        elif parsed["c"] > (self.messages["client"] + 1):
            # Messages from the client have been lost.
            logger.debug('REQUEST RESEND FROM CLIENT')
            self.write_message({
                'type': 'request_resend',
                'from': self.messages["client"]
            })
            return
        elif parsed["s"] < self.messages["server"]:
            # Message was sent either simultaneously with message from server
            # or a message from the server previously sent never arrived.
            # Resend the messages the client missed.
            logger.debug('SIMULTANEOUS')
            self.messages["client"] += 1
            self.resend_messages(parsed["s"])
            if (parsed["type"] == "diff"):
                self.send_message({
                    'type': 'reject_diff',
                    'rid': parsed['rid']
                })
            return
        # Message order is correct. We continue processing the data.
        self.messages["client"] += 1

        if parsed["type"] == 'get_document':
            self.send_document()
        elif parsed["type"] == 'participant_update' and self.can_communicate():
            self.handle_participant_update()
        elif parsed["type"] == 'chat' and self.can_communicate():
            self.handle_chat(parsed)
        elif parsed["type"] == 'check_version':
            self.check_version(parsed)
        elif parsed["type"] == 'selection_change':
            self.handle_selection_change(parsed)
        elif parsed["type"] == 'diff' and self.can_update_document():
            self.handle_diff(parsed)

    def resend_messages(self, from_no):
        to_send = self.messages["server"] - from_no
        logger.debug('resending messages: %d' % to_send)
        logger.debug('Server: %d, from: %d' %
                     (self.messages["server"], from_no))
        if to_send > len(self.messages['last_ten']):
            # Too many messages requested. We have to abort.
            logger.debug('cannot fix it')
            self.send_document()
            return
        self.messages['server'] -= to_send
        for message in self.messages['last_ten'][0 - to_send:]:
            self.send_message(message)

    def update_bibliography(self, bibliography_updates):
        for bu in bibliography_updates:
            if "id" not in bu:
                continue
            id = bu["id"]
            if bu["type"] == "update":
                self.doc["bibliography"][id] = bu["reference"]
            elif bu["type"] == "delete":
                del self.doc["bibliography"][id]

    def update_images(self, image_updates):
        for iu in image_updates:
            if "id" not in iu:
                continue
            id = iu["id"]
            if iu["type"] == "update":
                # Ensure that access rights exist
                if not UserImage.objects.filter(
                        image__id=id, owner=self.user_info.user).exists():
                    continue
                doc_image = DocumentImage.objects.filter(
                    document_id=self.doc["id"], image_id=id)
                if doc_image.exists():
                    doc_image.title = iu["image"]["title"]
                    doc_image.save()
                else:
                    DocumentImage.objects.create(document_id=self.doc["id"],
                                                 image_id=id,
                                                 title=iu["image"]["title"])
            elif iu["type"] == "delete":
                DocumentImage.objects.filter(document_id=self.doc["id"],
                                             image_id=id).delete()
                for image in Image.objects.filter(id=id):
                    if image.is_deletable():
                        image.delete()

    def update_comments(self, comments_updates):
        comments_updates = deepcopy(comments_updates)
        for cd in comments_updates:
            if "id" not in cd:
                # ignore
                continue
            id = str(cd["id"])
            if cd["type"] == "create":
                del cd["type"]
                self.doc["comments"][id] = cd
            elif cd["type"] == "delete":
                del self.doc["comments"][id]
            elif cd["type"] == "update":
                self.doc["comments"][id]["comment"] = cd["comment"]
                if "review:isMajor" in cd:
                    self.doc["comments"][id]["review:isMajor"] = cd[
                        "review:isMajor"]
            elif cd["type"] == "add_answer":
                if "answers" not in self.doc["comments"][id]:
                    self.doc["comments"][id]["answers"] = []
                del cd["type"]
                self.doc["comments"][id]["answers"].append(cd)
            elif cd["type"] == "delete_answer":
                answer_id = str(cd["answerId"])
                for answer in self.doc["comments"][id]["answers"]:
                    if answer["id"] == answer_id:
                        self.doc["comments"][id]["answers"].remove(answer)
            elif cd["type"] == "update_answer":
                answer_id = str(cd["answerId"])
                for answer in self.doc["comments"][id]["answers"]:
                    if answer["id"] == answer_id:
                        answer["answer"] = cd["answer"]

    def handle_participant_update(self):
        WebSocket.send_participant_list(self.user_info.document_id)

    def handle_chat(self, parsed):
        chat = {
            "id": str(uuid.uuid4()),
            "body": parsed['body'],
            "from": self.user_info.user.id,
            "type": 'chat'
        }
        WebSocket.send_updates(chat, self.user_info.document_id)

    def handle_selection_change(self, parsed):
        if self.user_info.document_id in WebSocket.sessions and parsed[
                "v"] == self.doc['version']:
            WebSocket.send_updates(parsed, self.user_info.document_id, self.id)

    # Checks if the diff only contains changes to comments.
    def only_comments(self, parsed):
        allowed_operations = ['addMark', 'removeMark']
        only_comment = True
        if "ds" in parsed:  # ds = document steps
            for step in parsed["ds"]:
                if not (step['stepType'] in allowed_operations
                        and step['mark']['type'] == 'comment'):
                    only_comment = False
        return only_comment

    def handle_diff(self, parsed):
        pv = parsed["v"]
        dv = self.doc['version']
        logger.debug("PV: %d, DV: %d" % (pv, dv))
        if (self.user_info.access_rights in COMMENT_ONLY
                and not self.only_comments(parsed)):
            logger.error(('received non-comment diff from comment-only '
                          'collaborator. Discarding.'))
            return
        if pv == dv:
            self.doc["last_diffs"].append(parsed)
            # Only keep the last 1000 diffs
            self.doc["last_diffs"] = self.doc["last_diffs"][-1000:]
            self.doc['version'] += 1
            if "jd" in parsed:  # jd = json diff
                try:
                    apply_patch(self.doc['contents'], parsed["jd"], True)
                except JsonPatchConflict:
                    logger.exception("Cannot apply json diff.")
                    logger.error(json_encode(parsed))
                    logger.error(json_encode(self.doc['contents']))
                    self.send_document()
                # The json diff is only needed by the python backend which does
                # not understand the steps. It can therefore be removed before
                # broadcast to other clients.
                del parsed["jd"]
            if "ti" in parsed:  # ti = title
                self.doc["title"] = parsed["ti"]
            if "cu" in parsed:  # cu = comment updates
                self.update_comments(parsed["cu"])
            if "bu" in parsed:  # bu = bibliography updates
                self.update_bibliography(parsed["bu"])
            if "iu" in parsed:  # iu = image updates
                self.update_images(parsed["iu"])
            WebSocket.save_document(self.user_info.document_id)
            self.confirm_diff(parsed["rid"])
            WebSocket.send_updates(parsed, self.user_info.document_id, self.id,
                                   self.user_info.user.id)
        elif pv < dv:
            if pv + len(self.doc["last_diffs"]) >= dv:
                # We have enough last_diffs stored to fix it.
                logger.debug("can fix it")
                number_diffs = pv - dv
                messages = self.doc["last_diffs"][number_diffs:]
                for message in messages:
                    new_message = message.copy()
                    new_message["server_fix"] = True
                    self.send_message(new_message)
            else:
                logger.debug('unfixable')
                # Client has a version that is too old to be fixed
                self.send_document()
        else:
            # Client has a higher version than server. Something is fishy!
            logger.debug('unfixable')

    def check_version(self, parsed):
        pv = parsed["v"]
        dv = self.doc['version']
        logger.debug("PV: %d, DV: %d" % (pv, dv))
        if pv == dv:
            response = {
                "type": "confirm_version",
                "v": pv,
            }
            self.send_message(response)
            return
        elif pv + len(self.doc["last_diffs"]) >= dv:
            logger.debug("can fix it")
            number_diffs = pv - dv
            messages = self.doc["last_diffs"][number_diffs:]
            for message in messages:
                new_message = message.copy()
                new_message["server_fix"] = True
                self.send_message(new_message)
            return
        else:
            logger.debug('unfixable')
            # Client has a version that is too old
            self.send_document()
            return

    def can_update_document(self):
        return self.user_info.access_rights in CAN_UPDATE_DOCUMENT

    def can_communicate(self):
        return self.user_info.access_rights in CAN_COMMUNICATE

    def on_close(self):
        logger.debug('Websocket closing')
        if (hasattr(self, 'user_info')
                and hasattr(self.user_info, 'document_id')
                and self.user_info.document_id in WebSocket.sessions
                and hasattr(self, 'id') and self.id in WebSocket.sessions[
                    self.user_info.document_id]['participants']):
            del self.doc['participants'][self.id]
            if len(self.doc['participants']) == 0:
                WebSocket.save_document(self.user_info.document_id)
                del WebSocket.sessions[self.user_info.document_id]
                logger.debug("noone left")
            else:
                WebSocket.send_participant_list(self.user_info.document_id)

    def send_message(self, message):
        self.messages['server'] += 1
        message['c'] = self.messages['client']
        message['s'] = self.messages['server']
        self.messages['last_ten'].append(message)
        self.messages['last_ten'] = self.messages['last_ten'][-10:]
        logger.debug("Sending: Type %s, Server: %d, Client: %d, id: %d" %
                     (message["type"], message['s'], message['c'], self.id))
        if message["type"] == 'diff':
            logger.debug("Diff version: %d" % message["v"])
        self.write_message(message)

    @classmethod
    def send_participant_list(cls, document_id):
        if document_id in WebSocket.sessions:
            participant_list = []
            for session_id, waiter in cls.sessions[document_id][
                    'participants'].items():
                access_rights = waiter.user_info.access_rights
                if access_rights not in CAN_COMMUNICATE:
                    continue
                participant_list.append({
                    'session_id':
                    session_id,
                    'id':
                    waiter.user_info.user.id,
                    'name':
                    waiter.user_info.user.readable_name,
                    'avatar':
                    avatar_url(waiter.user_info.user, 80)
                })
            message = {
                "participant_list": participant_list,
                "type": 'connections'
            }
            WebSocket.send_updates(message, document_id)

    @classmethod
    def send_updates(cls, message, document_id, sender_id=None, user_id=None):
        logger.debug("Sending message to %d waiters",
                     len(cls.sessions[document_id]['participants']))
        for waiter in cls.sessions[document_id]['participants'].values():
            if waiter.id != sender_id:
                access_rights = waiter.user_info.access_rights
                if "comments" in message and len(message["comments"]) > 0:
                    # Filter comments if needed
                    if access_rights == 'read-without-comments':
                        # The reader should not receive the comments update, so
                        # we remove the comments from the copy of the message
                        # sent to the reviewer. We still need to send the rest
                        # of the message as it may contain other diff
                        # information.
                        message = deepcopy(message)
                        message['comments'] = []
                    elif (access_rights == 'review'
                          and user_id != waiter.user_info.user.id):
                        # The reviewer should not receive comments updates from
                        # others than themselves, so we remove the comments
                        # from the copy of the message sent to the reviewer
                        # that are not from them. We still need to sned the
                        # rest of the message as it may contain other diff
                        # information.
                        message = deepcopy(message)
                        message['comments'] = []
                elif (message['type'] in ["chat", "connections"]
                      and access_rights not in CAN_COMMUNICATE):
                    continue
                elif (message['type'] == "selection_change"
                      and access_rights not in CAN_COMMUNICATE
                      and user_id != waiter.user_info.user.id):
                    continue
                try:
                    waiter.send_message(message)
                except WebSocketClosedError:
                    logger.error("Error sending message", exc_info=True)

    @classmethod
    def save_document(cls, document_id):
        doc = cls.sessions[document_id]
        doc_db = doc['db']
        doc_db.title = doc['title'][-255:]
        doc_db.version = doc['version']
        doc_db.contents = json_encode(doc['contents'])
        doc_db.last_diffs = json_encode(doc['last_diffs'])
        doc_db.comments = json_encode(doc['comments'])
        doc_db.bibliography = json_encode(doc['bibliography'])
        logger.debug('saving document # %d' % doc_db.id)
        logger.debug('version %d' % doc_db.version)
        doc_db.save()

    @classmethod
    def save_all_docs(cls):
        for document_id in cls.sessions:
            cls.save_document(document_id)
示例#10
0
class WebSocket(BaseWebSocketHandler):
    sessions = dict()
    history_length = 1000  # Only keep the last 1000 diffs

    def open(self, arg):
        super().open(arg)
        if len(self.args) < 2:
            self.access_denied()
            return
        self.document_id = int(self.args[0])
        logger.debug(
            f"Action:Document socket opened by user. "
            f"URL:{self.endpoint} User:{self.user.id} ParticipantID:{self.id}")

    def confirm_diff(self, rid):
        response = {
            'type': 'confirm_diff',
            'rid': rid
        }
        self.send_message(response)

    def subscribe_doc(self, connection_count=0):
        self.user_info = SessionUserInfo(self.user)
        doc_db, can_access = self.user_info.init_access(
            self.document_id
        )
        if not can_access or float(doc_db.doc_version) != FW_DOCUMENT_VERSION:
            self.access_denied()
            return
        if (
            doc_db.id in WebSocket.sessions and
            len(WebSocket.sessions[doc_db.id]['participants']) > 0
        ):
            logger.debug(
                f"Action:Serving already opened document. "
                f"URL:{self.endpoint} User:{self.user.id} "
                f" ParticipantID:{self.id}")
            self.doc = WebSocket.sessions[doc_db.id]
            self.id = max(self.doc['participants']) + 1
            self.doc['participants'][self.id] = self
        else:
            logger.debug(
                f"Action:Opening document from DB. "
                f"URL:{self.endpoint} User:{self.user.id} "
                f"ParticipantID:{self.id}")
            self.id = 0
            self.doc = {
                'db': doc_db,
                'participants': {
                    0: self
                },
                'last_diffs': json_decode(doc_db.last_diffs),
                'comments': json_decode(doc_db.comments),
                'bibliography': json_decode(doc_db.bibliography),
                'contents': json_decode(doc_db.contents),
                'version': doc_db.version,
                'title': doc_db.title,
                'id': doc_db.id,
                'template': {
                    'id': doc_db.template.id,
                    'definition': json_decode(doc_db.template.definition)
                }
            }
            WebSocket.sessions[doc_db.id] = self.doc
        logger.debug(
            f"Action:Participant ID Assigned. URL:{self.endpoint} "
            f"User:{self.user.id} ParticipantID:{self.id}")
        self.send_message({
            'type': 'subscribed'
        })
        if connection_count < 1:
            self.send_styles()
            self.send_document()
        if self.can_communicate():
            self.handle_participant_update()

    def send_styles(self):
        doc_db = self.doc['db']
        response = dict()
        response['type'] = 'styles'
        serializer = PythonWithURLSerializer()
        export_temps = serializer.serialize(
            doc_db.template.exporttemplate_set.all(),
            fields=['file_type', 'template_file', 'title']
        )
        document_styles = serializer.serialize(
            doc_db.template.documentstyle_set.all(),
            use_natural_foreign_keys=True,
            fields=['title', 'slug', 'contents', 'documentstylefile_set']
        )
        document_templates = {}
        for obj in DocumentTemplate.objects.filter(
            Q(user=self.user) | Q(user=None)
        ).order_by(F('user').desc(nulls_first=True)):
            document_templates[obj.import_id] = {
                'title': obj.title,
                'id': obj.id
            }

        response['styles'] = {
            'export_templates': [obj['fields'] for obj in export_temps],
            'document_styles': [obj['fields'] for obj in document_styles],
            'document_templates': document_templates
        }
        self.send_message(response)

    def unfixable(self):
        self.send_document()

    def send_document(self):
        response = dict()
        response['type'] = 'doc_data'
        doc_owner = self.doc['db'].owner
        response['doc_info'] = {
            'id': self.doc['id'],
            'is_owner': self.user_info.is_owner,
            'access_rights': self.user_info.access_rights,
            'owner': {
                'id': doc_owner.id,
                'name': doc_owner.readable_name,
                'username': doc_owner.username,
                'avatar': get_user_avatar_url(doc_owner),
                'team_members': []
            }
        }
        response['doc'] = {
            'v': self.doc['version'],
            'contents': self.doc['contents'],
            'bibliography': self.doc['bibliography'],
            'template': self.doc['template'],
            'images': {}
        }
        response['time'] = int(time()) * 1000
        for dimage in DocumentImage.objects.filter(document_id=self.doc['id']):
            image = dimage.image
            field_obj = {
                'id': image.id,
                'title': dimage.title,
                'copyright': json.loads(dimage.copyright),
                'image': image.image.url,
                'file_type': image.file_type,
                'added': mktime(image.added.timetuple()) * 1000,
                'checksum': image.checksum,
                'cats': []
            }
            if image.thumbnail:
                field_obj['thumbnail'] = image.thumbnail.url
                field_obj['height'] = image.height
                field_obj['width'] = image.width
            response['doc']['images'][image.id] = field_obj
        if self.user_info.access_rights == 'read-without-comments':
            response['doc']['comments'] = []
        elif self.user_info.access_rights == 'review':
            # Reviewer should only get his/her own comments
            filtered_comments = {}
            for key, value in list(self.doc["comments"].items()):
                if value["user"] == self.user_info.user.id:
                    filtered_comments[key] = value
            response['doc']['comments'] = filtered_comments
        else:
            response['doc']['comments'] = self.doc["comments"]
        for team_member in doc_owner.leader.all():
            tm_object = dict()
            tm_object['id'] = team_member.member.id
            tm_object['name'] = team_member.member.readable_name
            tm_object['username'] = team_member.member.get_username()
            tm_object['avatar'] = get_user_avatar_url(team_member.member)
            response['doc_info']['owner']['team_members'].append(tm_object)
        response['doc_info']['session_id'] = self.id
        self.send_message(response)

    def reject_message(self, message):
        if (message["type"] == "diff"):
            self.send_message({
                'type': 'reject_diff',
                'rid': message['rid']
            })

    def handle_message(self, message):
        if message["type"] == 'subscribe':
            connection_count = 0
            if 'connection' in message:
                connection_count = message['connection']
            self.subscribe_doc(connection_count)
            return
        if self.user_info.document_id not in WebSocket.sessions:
            logger.debug(
                f"Action:Receiving message for closed document. "
                f"URL:{self.endpoint} User:{self.user.id} "
                f"ParticipantID:{self.id}")
            return
        if message["type"] == 'get_document':
            self.send_document()
        elif (
            message["type"] == 'participant_update' and
            self.can_communicate()
        ):
            self.handle_participant_update()
        elif message["type"] == 'chat' and self.can_communicate():
            self.handle_chat(message)
        elif message["type"] == 'check_version':
            self.check_version(message)
        elif message["type"] == 'selection_change':
            self.handle_selection_change(message)
        elif message["type"] == 'diff' and self.can_update_document():
            self.handle_diff(message)

    def update_bibliography(self, bibliography_updates):
        for bu in bibliography_updates:
            if "id" not in bu:
                continue
            id = bu["id"]
            if bu["type"] == "update":
                self.doc["bibliography"][id] = bu["reference"]
            elif bu["type"] == "delete":
                del self.doc["bibliography"][id]

    def update_images(self, image_updates):
        for iu in image_updates:
            if "id" not in iu:
                continue
            id = iu["id"]
            if iu["type"] == "update":
                # Ensure that access rights exist
                if not UserImage.objects.filter(
                    image__id=id,
                    owner=self.user_info.user
                ).exists():
                    continue
                doc_image = DocumentImage.objects.filter(
                    document_id=self.doc["id"],
                    image_id=id
                ).first()
                if doc_image:
                    doc_image.title = iu["image"]["title"]
                    doc_image.copyright = json.dumps(iu["image"]["copyright"])
                    doc_image.save()
                else:
                    DocumentImage.objects.create(
                        document_id=self.doc["id"],
                        image_id=id,
                        title=iu["image"]["title"],
                        copyright=json.dumps(iu["image"]["copyright"])
                    )
            elif iu["type"] == "delete":
                DocumentImage.objects.filter(
                    document_id=self.doc["id"],
                    image_id=id
                ).delete()
                for image in Image.objects.filter(id=id):
                    if image.is_deletable():
                        image.delete()

    def update_comments(self, comments_updates):
        comments_updates = deepcopy(comments_updates)
        for cd in comments_updates:
            if "id" not in cd:
                # ignore
                continue
            id = cd["id"]
            if cd["type"] == "create":
                self.doc["comments"][id] = {
                    "user": cd["user"],
                    "username": cd["username"],
                    "assignedUser": cd["assignedUser"],
                    "assignedUsername": cd["assignedUsername"],
                    "date": cd["date"],
                    "comment": cd["comment"],
                    "isMajor": cd["isMajor"],
                    "resolved": cd["resolved"],
                }
            elif cd["type"] == "delete":
                del self.doc["comments"][id]
            elif cd["type"] == "update":
                self.doc["comments"][id]["comment"] = cd["comment"]
                if "isMajor" in cd:
                    self.doc["comments"][id][
                        "isMajor"] = cd["isMajor"]
                if "assignedUser" in cd and "assignedUsername" in cd:
                    self.doc["comments"][id][
                        "assignedUser"] = cd["assignedUser"]
                    self.doc["comments"][id][
                        "assignedUsername"] = cd["assignedUsername"]
                if "resolved" in cd:
                    self.doc["comments"][id][
                        "resolved"] = cd["resolved"]
            elif cd["type"] == "add_answer":
                if "answers" not in self.doc["comments"][id]:
                    self.doc["comments"][id]["answers"] = []
                self.doc["comments"][id]["answers"].append({
                    "id": cd["answerId"],
                    "user": cd["user"],
                    "username": cd["username"],
                    "date": cd["date"],
                    "answer": cd["answer"]
                })
            elif cd["type"] == "delete_answer":
                answer_id = cd["answerId"]
                for answer in self.doc["comments"][id]["answers"]:
                    if answer["id"] == answer_id:
                        self.doc["comments"][id]["answers"].remove(answer)
            elif cd["type"] == "update_answer":
                answer_id = cd["answerId"]
                for answer in self.doc["comments"][id]["answers"]:
                    if answer["id"] == answer_id:
                        answer["answer"] = cd["answer"]

    def handle_participant_update(self):
        WebSocket.send_participant_list(self.user_info.document_id)

    def handle_chat(self, message):
        chat = {
            "id": str(uuid.uuid4()),
            "body": message['body'],
            "from": self.user_info.user.id,
            "type": 'chat'
        }
        WebSocket.send_updates(chat, self.user_info.document_id)

    def handle_selection_change(self, message):
        if self.user_info.document_id in WebSocket.sessions and message[
                "v"] == self.doc['version']:
            WebSocket.send_updates(
                message, self.user_info.document_id, self.id)

    # Checks if the diff only contains changes to comments.
    def only_comments(self, message):
        allowed_operations = ['addMark', 'removeMark']
        only_comment = True
        if "ds" in message:  # ds = document steps
            for step in message["ds"]:
                if not (step['stepType'] in allowed_operations and step[
                        'mark']['type'] == 'comment'):
                    only_comment = False
        return only_comment

    def handle_diff(self, message):
        pv = message["v"]
        dv = self.doc['version']
        logger.debug(
            f"Action:Handling Diff. URL:{self.endpoint} User:{self.user.id} "
            f"ParticipantID:{self.id} Client version:{pv} "
            f"Server version:{dv} Message:{message}")
        if (
            self.user_info.access_rights in COMMENT_ONLY and
            not self.only_comments(message)
        ):
            logger.error(
                f"Action:Received non-comment diff from comment-only "
                f"collaborator.Discarding URL:{self.endpoint} "
                f"User:{self.user.id} ParticipantID:{self.id}")
            return
        if pv == dv:
            self.doc["last_diffs"].append(message)
            self.doc["last_diffs"] = self.doc[
                "last_diffs"
            ][-self.history_length:]
            self.doc['version'] += 1
            if "jd" in message:  # jd = json diff
                try:
                    apply_patch(
                       self.doc['contents'],
                       message["jd"],
                       True
                    )
                except (JsonPatchConflict, JsonPointerException):
                    logger.exception(
                        f"Action:Cannot apply json diff. "
                        f"URL:{self.endpoint} User:{self.user.id} "
                        f"ParticipantID:{self.id}")
                    logger.error(
                        f"Action:Patch Exception URL:{self.endpoint} "
                        f"User:{self.user.id} ParticipantID:{self.id} "
                        f"Message:{json_encode(message)}")
                    logger.error(
                        f"Action:Patch Exception URL:{self.endpoint} "
                        f"User:{self.user.id} ParticipantID:{self.id} "
                        f"Document:{json_encode(self.doc['contents'])}")
                    self.unfixable()
                # The json diff is only needed by the python backend which does
                # not understand the steps. It can therefore be removed before
                # broadcast to other clients.
                del message["jd"]
            if "ti" in message:  # ti = title
                self.doc["title"] = message["ti"]
            if "cu" in message:  # cu = comment updates
                self.update_comments(message["cu"])
            if "bu" in message:  # bu = bibliography updates
                self.update_bibliography(message["bu"])
            if "iu" in message:  # iu = image updates
                self.update_images(message["iu"])
            if self.doc['version'] % 10 == 0:
                WebSocket.save_document(self.user_info.document_id)
            self.confirm_diff(message["rid"])
            WebSocket.send_updates(
                message,
                self.user_info.document_id,
                self.id,
                self.user_info.user.id
            )
        elif pv < dv:
            if pv + len(self.doc["last_diffs"]) >= dv:
                # We have enough last_diffs stored to fix it.
                number_diffs = pv - dv
                logger.debug(
                    f"Action:Resending document diffs. URL:{self.endpoint} "
                    f"User:{self.user.id} ParticipantID:{self.id} "
                    f"number of messages to be resent:{number_diffs}")
                messages = self.doc["last_diffs"][number_diffs:]
                for message in messages:
                    new_message = message.copy()
                    new_message["server_fix"] = True
                    self.send_message(new_message)
            else:
                logger.debug(
                    f"Action:User is on a very old version of the document. "
                    f"URL:{self.endpoint} User:{self.user.id} "
                    f"ParticipantID:{self.id}")
                # Client has a version that is too old to be fixed
                self.unfixable()
        else:
            # Client has a higher version than server. Something is fishy!
            logger.debug(
                f"Action:User has higher document version than server.Fishy! "
                f"URL:{self.endpoint} User:{self.user.id} "
                f"ParticipantID:{self.id}")

    def check_version(self, message):
        pv = message["v"]
        dv = self.doc['version']
        logger.debug(
            f"Action:Checking version of document. URL:{self.endpoint} "
            f"User:{self.user.id} ParticipantID:{self.id} "
            f"Client document version:{pv} Server document version:{dv}")
        if pv == dv:
            response = {
                "type": "confirm_version",
                "v": pv,
            }
            self.send_message(response)
            return
        elif pv + len(self.doc["last_diffs"]) >= dv:
            number_diffs = pv - dv
            logger.debug(
                f"Action:Resending document diffs. URL:{self.endpoint} "
                f"User:{self.user.id} ParticipantID:{self.id}"
                f"number of messages to be resent:{number_diffs}")
            messages = self.doc["last_diffs"][number_diffs:]
            for message in messages:
                new_message = message.copy()
                new_message["server_fix"] = True
                self.send_message(new_message)
            return
        else:
            logger.debug(
                f"Action:User is on a very old version of the document. "
                f"URL:{self.endpoint} User:{self.user.id} "
                f"ParticipantID:{self.id}")
            # Client has a version that is too old
            self.unfixable()
            return

    def can_update_document(self):
        return self.user_info.access_rights in CAN_UPDATE_DOCUMENT

    def can_communicate(self):
        return self.user_info.access_rights in CAN_COMMUNICATE

    def on_close(self):
        logger.debug(
            f"Action:Closing websocket. URL:{self.endpoint} "
            f"User:{self.user.id} ParticipantID:{self.id}")
        if (
            hasattr(self, 'user_info') and
            hasattr(self.user_info, 'document_id') and
            self.user_info.document_id in WebSocket.sessions and
            hasattr(self, 'id') and
            self.id in WebSocket.sessions[
                self.user_info.document_id
            ]['participants']
        ):
            del self.doc['participants'][self.id]
            if len(self.doc['participants']) == 0:
                WebSocket.save_document(self.user_info.document_id)
                del WebSocket.sessions[self.user_info.document_id]
                logger.debug(
                    f"Action:No participants for the document. "
                    f"URL:{self.endpoint} User:{self.user.id}")
            else:
                WebSocket.send_participant_list(self.user_info.document_id)

    @classmethod
    def send_participant_list(cls, document_id):
        if document_id in WebSocket.sessions:
            participant_list = []
            for session_id, waiter in list(cls.sessions[
                document_id
            ]['participants'].items()):
                access_rights = waiter.user_info.access_rights
                if access_rights not in CAN_COMMUNICATE:
                    continue
                participant_list.append({
                    'session_id': session_id,
                    'id': waiter.user_info.user.id,
                    'name': waiter.user_info.user.readable_name,
                    'avatar': get_user_avatar_url(waiter.user_info.user)
                })
            message = {
                "participant_list": participant_list,
                "type": 'connections'
            }
            WebSocket.send_updates(message, document_id)

    @classmethod
    def send_updates(cls, message, document_id, sender_id=None, user_id=None):
        logger.debug(
            f"Action:Sending message to waiters. DocumentID:{document_id} "
            f"waiters:{len(cls.sessions[document_id]['participants'])}")
        for waiter in list(cls.sessions[document_id]['participants'].values()):
            if waiter.id != sender_id:
                access_rights = waiter.user_info.access_rights
                if "comments" in message and len(message["comments"]) > 0:
                    # Filter comments if needed
                    if access_rights == 'read-without-comments':
                        # The reader should not receive the comments update, so
                        # we remove the comments from the copy of the message
                        # sent to the reviewer. We still need to send the rest
                        # of the message as it may contain other diff
                        # information.
                        message = deepcopy(message)
                        message['comments'] = []
                    elif (
                        access_rights == 'review' and
                        user_id != waiter.user_info.user.id
                    ):
                        # The reviewer should not receive comments updates from
                        # others than themselves, so we remove the comments
                        # from the copy of the message sent to the reviewer
                        # that are not from them. We still need to send the
                        # rest of the message as it may contain other diff
                        # information.
                        message = deepcopy(message)
                        message['comments'] = []
                elif (
                    message['type'] in ["chat", "connections"] and
                    access_rights not in CAN_COMMUNICATE
                ):
                    continue
                elif (
                    message['type'] == "selection_change" and
                    access_rights not in CAN_COMMUNICATE and
                    user_id != waiter.user_info.user.id
                ):
                    continue
                waiter.send_message(message)

    @classmethod
    def save_document(cls, document_id):
        doc = cls.sessions[document_id]
        doc_db = doc['db']
        if doc_db.version == doc['version']:
            return
        doc_db.title = doc['title'][-255:]
        doc_db.version = doc['version']
        doc_db.contents = json_encode(doc['contents'])
        doc_db.last_diffs = json_encode(doc['last_diffs'])
        doc_db.comments = json_encode(doc['comments'])
        doc_db.bibliography = json_encode(doc['bibliography'])
        logger.debug(
            f"Action:Saving document to DB. DocumentID:{doc_db.id} "
            f"Doc version:{doc_db.version}")
        try:
            # this try block is to avoid a db exception
            # in case the doc has been deleted from the db
            # in fiduswriter the owner of a doc could delete a doc
            # while an invited writer is editing the same doc
            doc_db.save(update_fields=[
                        'title',
                        'version',
                        'contents',
                        'last_diffs',
                        'comments',
                        'bibliography',
                        'updated'])
        except DatabaseError as e:
            expected_msg = 'Save with update_fields did not affect any rows.'
            if str(e) == expected_msg:
                cls.__insert_document(doc=doc_db)
            else:
                raise e

    @classmethod
    def __insert_document(cls, doc: Document) -> None:
        """
        Purpose:
        during plugin tests we experienced Integrity errors
         at the end of tests.
        This exception occurs while handling another exception,
         so in order to have a clean tests output
          we raise the exception in a way we don't output
           misleading error messages related to different exceptions

        :param doc: socket document model instance
        :return: None
        """
        from django.db.utils import IntegrityError
        try:
            doc.save()
        except IntegrityError as e:
            raise IntegrityError(
                'plugin test error when we try to save a doc already deleted '
                'along with the rest of db data so it '
                'raises an Integrity error: {}'.format(e)) from None

    @classmethod
    def save_all_docs(cls):
        for document_id in cls.sessions:
            cls.save_document(document_id)
示例#11
0
class DocumentWS(BaseWebSocketHandler):
    sessions = dict()

    def open(self, document_id):
        print('Websocket opened')
        response = dict()
        current_user = self.get_current_user()
        if current_user is None:
            response['type'] = 'access_denied'
            self.write_message(response)
            return
        self.user_info = SessionUserInfo()
        doc_db, can_access = self.user_info.init_access(
            document_id, current_user)
        if can_access:
            if doc_db.id in DocumentWS.sessions:
                self.doc = DocumentWS.sessions[doc_db.id]
                self.id = max(self.doc['participants']) + 1
                print("id when opened %s" % self.id)
            else:
                self.id = 0
                self.doc = dict()
                self.doc['db'] = doc_db
                self.doc['participants'] = dict()
                self.doc['last_diffs'] = json_decode(doc_db.last_diffs)
                self.doc['comments'] = json_decode(doc_db.comments)
                self.doc['settings'] = json_decode(doc_db.settings)
                self.doc['contents'] = json_decode(doc_db.contents)
                self.doc['metadata'] = json_decode(doc_db.metadata)
                self.doc['version'] = doc_db.version
                self.doc['diff_version'] = doc_db.diff_version
                self.doc['comment_version'] = doc_db.comment_version
                self.doc['title'] = doc_db.title
                self.doc['id'] = doc_db.id
                DocumentWS.sessions[doc_db.id] = self.doc
            self.doc['participants'][self.id] = self
            response['type'] = 'welcome'
            self.write_message(response)

    def confirm_diff(self, request_id):
        response = dict()
        response['type'] = 'confirm_diff'
        response['request_id'] = request_id
        self.write_message(response)

    def send_document(self):
        response = dict()
        response['type'] = 'doc_data'
        response['doc'] = dict()
        response['doc']['id'] = self.doc['id']
        response['doc']['version'] = self.doc['version']
        if self.doc['diff_version'] < self.doc['version']:
            print('!!!diff version issue!!!')
            self.doc['diff_version'] = self.doc['version']
            self.doc["last_diffs"] = []
        response['doc']['title'] = self.doc['title']
        response['doc']['contents'] = self.doc['contents']
        response['doc']['metadata'] = self.doc['metadata']
        response['doc']['settings'] = self.doc['settings']
        document_owner = self.doc['db'].owner
        access_rights = get_accessrights(
            AccessRight.objects.filter(
                document__owner=document_owner))
        response['doc']['access_rights'] = access_rights

        if (self.user_info.access_rights == 'read-without-comments'):
            response['doc']['comments'] = []
        elif (self.user_info.access_rights == 'review'):
            # Reviewer should only get his/her own comments
            filtered_comments = {}
            for key, value in self.doc["comments"].items():
                if value["user"] == self.user_info.user.id:
                    filtered_comments[key] = value
            response['doc']['comments'] = filtered_comments
        else:
            response['doc']['comments'] = self.doc["comments"]
        response['doc']['comment_version'] = self.doc["comment_version"]
        response['doc']['access_rights'] = get_accessrights(
            AccessRight.objects.filter(document__owner=document_owner))
        response['doc']['owner'] = dict()
        response['doc']['owner']['id'] = document_owner.id
        response['doc']['owner']['name'] = document_owner.readable_name
        response['doc']['owner'][
            'avatar'] = avatar_url(document_owner, 80)
        response['doc']['owner']['team_members'] = []

        for team_member in document_owner.leader.all():
            tm_object = dict()
            tm_object['id'] = team_member.member.id
            tm_object['name'] = team_member.member.readable_name
            tm_object['avatar'] = avatar_url(team_member.member, 80)
            response['doc']['owner']['team_members'].append(tm_object)
        response['doc_info'] = dict()
        response['doc_info']['is_owner'] = self.user_info.is_owner
        response['doc_info']['rights'] = self.user_info.access_rights
        if self.doc['version'] > self.doc['diff_version']:
            print('!!!diff version issue!!!')
            self.doc['diff_version'] = self.doc['version']
            self.doc["last_diffs"] = []
        elif self.doc['diff_version'] > self.doc['version']:
            needed_diffs = self.doc['diff_version'] - self.doc['version']
            # We only send those diffs needed by the receiver.
            response['doc_info']['unapplied_diffs'] = self.doc[
                "last_diffs"][-needed_diffs:]
        else:
            response['doc_info']['unapplied_diffs'] = []
        if not self.user_info.is_owner:
            response['user'] = dict()
            response['user']['id'] = self.user_info.user.id
            response['user']['name'] = self.user_info.user.readable_name
            response['user']['avatar'] = avatar_url(self.user_info.user, 80)
        response['doc_info']['session_id'] = self.id
        self.write_message(response)

    def on_message(self, message):
        if self.user_info.document_id not in DocumentWS.sessions:
            print('receiving message for closed document')
            return
        parsed = json_decode(message)
        print(parsed["type"])
        if parsed["type"] == 'get_document':
            self.send_document()
        elif parsed["type"] == 'participant_update' and self.can_communicate():
            self.handle_participant_update()
        elif parsed["type"] == 'chat' and self.can_communicate():
            self.handle_chat(parsed)
        elif parsed["type"] == 'check_diff_version':
            self.check_diff_version(parsed)
        elif parsed["type"] == 'selection_change':
            self.handle_selection_change(parsed)
        elif (
            parsed["type"] == 'update_doc' and
            self.can_update_document()
        ):
            self.handle_document_update(parsed)
        elif parsed["type"] == 'update_title' and self.can_update_document():
            self.handle_title_update(parsed)
        elif parsed["type"] == 'diff' and self.can_update_document():
            self.handle_diff(parsed)

    def update_document(self, changes):
        if changes['version'] == self.doc['version']:
            # Document hasn't changed, return.
            return
        elif (
            changes['version'] > self.doc['diff_version'] or
            changes['version'] < self.doc['version']
        ):
            # The version number is too high. Possibly due to server restart.
            # Do not accept it, and send a document instead.
            self.send_document()
            return
        else:
            # The saved version does not contain all accepted diffs, so we keep
            # the remaining ones + 1000 in case a client needs to reconnect and
            # is missing some.
            remaining_diffs = 1000 + \
                self.doc['diff_version'] - changes['version']
            self.doc['last_diffs'] = self.doc['last_diffs'][-remaining_diffs:]
        self.doc['title'] = changes['title']
        self.doc['contents'] = changes['contents']
        self.doc['metadata'] = changes['metadata']
        self.doc['settings'] = changes['settings']
        self.doc['version'] = changes['version']

    def update_title(self, title):
        self.doc['title'] = title

    def update_comments(self, comments_updates):
        comments_updates = deepcopy(comments_updates)
        for cd in comments_updates:
            id = str(cd["id"])
            if cd["type"] == "create":
                del cd["type"]
                self.doc["comments"][id] = cd
            elif cd["type"] == "delete":
                del self.doc["comments"][id]
            elif cd["type"] == "update":
                self.doc["comments"][id]["comment"] = cd["comment"]
                if "review:isMajor" in cd:
                    self.doc["comments"][id][
                        "review:isMajor"] = cd["review:isMajor"]
            elif cd["type"] == "add_answer":
                comment_id = str(cd["commentId"])
                if "answers" not in self.doc["comments"][comment_id]:
                    self.doc["comments"][comment_id]["answers"] = []
                del cd["type"]
                self.doc["comments"][comment_id]["answers"].append(cd)
            elif cd["type"] == "delete_answer":
                comment_id = str(cd["commentId"])
                for answer in self.doc["comments"][comment_id]["answers"]:
                    if answer["id"] == cd["id"]:
                        self.doc["comments"][comment_id][
                            "answers"].remove(answer)
            elif cd["type"] == "update_answer":
                comment_id = str(cd["commentId"])
                for answer in self.doc["comments"][comment_id]["answers"]:
                    if answer["id"] == cd["id"]:
                        answer["answer"] = cd["answer"]
            self.doc['comment_version'] += 1

    def handle_participant_update(self):
        DocumentWS.send_participant_list(self.user_info.document_id)

    def handle_document_update(self, parsed):
        self.update_document(parsed["doc"])
        DocumentWS.save_document(self.user_info.document_id, False)
        message = {
            "type": 'check_hash',
            "diff_version": parsed["doc"]["version"],
            "hash": parsed["hash"]
        }
        DocumentWS.send_updates(message, self.user_info.document_id, self.id)

    def handle_title_update(self, parsed):
        self.update_title(parsed["title"])
        DocumentWS.save_document(self.user_info.document_id, False)

    def handle_chat(self, parsed):
        chat = {
            "id": str(uuid.uuid4()),
            "body": parsed['body'],
            "from": self.user_info.user.id,
            "type": 'chat'
        }
        DocumentWS.send_updates(chat, self.user_info.document_id)

    def handle_selection_change(self, parsed):
        if self.user_info.document_id in DocumentWS.sessions and parsed[
                "diff_version"] == self.doc['diff_version']:
            DocumentWS.send_updates(
                parsed, self.user_info.document_id, self.id)

    # Checks if the diff only contains changes to comments.
    def only_comments(self, parsed_diffs):
        allowed_operations = ['addMark', 'removeMark']
        only_comment = True
        for diff in parsed_diffs:
            if not (diff['stepType'] in allowed_operations and diff[
                    'mark']['type'] == 'comment'):
                only_comment = False
        return only_comment

    def handle_diff(self, parsed):
        if (
            self.user_info.access_rights in COMMENT_ONLY and
            not self.only_comments(parsed['diff'])
        ):
            print(
                (
                    'received non-comment diff from comment-only '
                    'collaborator. Discarding.'
                )
            )
            return
        if parsed["diff_version"] == self.doc['diff_version'] and parsed[
                "comment_version"] == self.doc['comment_version']:
            self.doc["last_diffs"].extend(parsed["diff"])
            self.doc['diff_version'] += len(parsed["diff"])
            self.update_comments(parsed["comments"])
            self.confirm_diff(parsed["request_id"])
            DocumentWS.send_updates(
                parsed,
                self.user_info.document_id,
                self.id,
                self.user_info.user.id
            )
        elif parsed["diff_version"] != self.doc['diff_version']:
            if parsed["diff_version"] < (
                    self.doc['diff_version'] - len(self.doc["last_diffs"])):
                print('unfixable')
                # Client has a version that is too old
                self.send_document()
            elif parsed["diff_version"] < self.doc['diff_version']:
                print("can fix it")
                number_requested_diffs = self.doc[
                    'diff_version'] - parsed["diff_version"]
                response = {
                    "type": "diff",
                    "diff_version": parsed["diff_version"],
                    "diff": self.doc["last_diffs"][-number_requested_diffs:],
                    "reject_request_id": parsed["request_id"],
                }
                self.write_message(response)
            else:
                print('unfixable')
                # Client has a version that is too old
                self.send_document()
        else:
            print('comment_version incorrect!')

    def check_diff_version(self, parsed):
        pdv = parsed["diff_version"]
        ddv = self.doc['diff_version']
        if pdv == ddv:
            response = {
                "type": "confirm_diff_version",
                "diff_version": pdv,
            }
            self.write_message(response)
            return
        elif pdv + len(self.doc["last_diffs"]) >= ddv:
            number_requested_diffs = ddv - pdv
            response = {
                "type": "diff",
                "diff_version": parsed["diff_version"],
                "diff": self.doc["last_diffs"][-number_requested_diffs:],
            }
            self.write_message(response)
            return
        else:
            print('unfixable')
            # Client has a version that is too old
            self.send_document()
            return

    def can_update_document(self):
        return self.user_info.access_rights in CAN_UPDATE_DOCUMENT

    def can_communicate(self):
        return self.user_info.access_rights in CAN_COMMUNICATE

    def on_close(self):
        print('Websocket closing')
        if (
            hasattr(self, 'user_info') and
            hasattr(self.user_info, 'document_id') and
            self.user_info.document_id in DocumentWS.sessions and
            hasattr(self, 'id') and
            self.id in DocumentWS.sessions[
                self.user_info.document_id
            ]['participants']
        ):
            del self.doc['participants'][self.id]
            if len(self.doc['participants'].keys()) == 0:
                DocumentWS.save_document(self.user_info.document_id, True)
                del DocumentWS.sessions[self.user_info.document_id]
                print("noone left")

    @classmethod
    def send_participant_list(cls, document_id):
        if document_id in DocumentWS.sessions:
            participant_list = []
            for session_id, waiter in cls.sessions[
                document_id
            ]['participants'].items():
                access_rights = waiter.user_info.access_rights
                if access_rights not in CAN_COMMUNICATE:
                    continue
                participant_list.append({
                    'session_id': session_id,
                    'id': waiter.user_info.user.id,
                    'name': waiter.user_info.user.readable_name,
                    'avatar': avatar_url(waiter.user_info.user, 80)
                })
            message = {
                "participant_list": participant_list,
                "type": 'connections'
            }
            DocumentWS.send_updates(message, document_id)

    @classmethod
    def send_updates(cls, message, document_id, sender_id=None, user_id=None):
        info("sending message to %d waiters", len(cls.sessions[document_id]))
        for waiter in cls.sessions[document_id]['participants'].values():
            if waiter.id != sender_id:
                access_rights = waiter.user_info.access_rights
                if "comments" in message and len(message["comments"]) > 0:
                    # Filter comments if needed
                    if access_rights == 'read-without-comments':
                        # The reader should not receive the comments update, so
                        # we remove the comments from the copy of the message
                        # sent to the reviewer. We still need to sned the rest
                        # of the message as it may contain other diff
                        # information.
                        message = deepcopy(message)
                        message['comments'] = []
                    elif (
                        access_rights == 'review' and
                        user_id != waiter.user_info.user.id
                    ):
                        # The reviewer should not receive comments updates from
                        # others than themselves, so we remove the comments
                        # from the copy of the message sent to the reviewer
                        # that are not from them. We still need to sned the
                        # rest of the message as it may contain other diff
                        # information.
                        message = deepcopy(message)
                        message['comments'] = []
                elif (
                    message['type'] in ["chat", "connections"] and
                    access_rights not in CAN_COMMUNICATE
                ):
                    continue
                elif (
                    message['type'] == "selection_change" and
                    access_rights not in CAN_COMMUNICATE and
                    user_id != waiter.user_info.user.id
                ):
                    continue
                try:
                    waiter.write_message(message)
                except WebSocketClosedError:
                    error("Error sending message", exc_info=True)

    @classmethod
    def save_document(cls, document_id, all_have_left):
        doc = cls.sessions[document_id]
        doc_db = doc['db']
        doc_db.title = doc['title']
        doc_db.version = doc['version']
        doc_db.diff_version = doc['diff_version']
        doc_db.comment_version = doc['comment_version']
        doc_db.contents = json_encode(doc['contents'])
        doc_db.metadata = json_encode(doc['metadata'])
        doc_db.settings = json_encode(doc['settings'])
        if all_have_left:
            remaining_diffs = doc['diff_version'] - doc['version']
            if remaining_diffs > 0:
                doc['last_diffs'] = doc['last_diffs'][-remaining_diffs:]
            else:
                doc['last_diffs'] = []
        doc_db.last_diffs = json_encode(doc['last_diffs'])
        doc_db.comments = json_encode(doc['comments'])
        print('saving document #' + str(doc_db.id))
        print('version ' + str(doc_db.version))
        doc_db.save()

    @classmethod
    def save_all_docs(cls):
        for document_id in cls.sessions:
            cls.save_document(document_id, True)
示例#12
0
class WebSocket(BaseWebSocketHandler):
    sessions = dict()

    def open(self, document_id):
        print('Websocket opened')
        response = dict()
        current_user = self.get_current_user()
        if current_user is None:
            response['type'] = 'access_denied'
            self.write_message(response)
            return
        self.user_info = SessionUserInfo()
        doc_db, can_access = self.user_info.init_access(
            document_id, current_user)
        if can_access:
            if doc_db.id in WebSocket.sessions:
                self.doc = WebSocket.sessions[doc_db.id]
                self.id = max(self.doc['participants']) + 1
                print("id when opened %s" % self.id)
            else:
                self.id = 0
                self.doc = dict()
                self.doc['db'] = doc_db
                self.doc['participants'] = dict()
                self.doc['last_diffs'] = json_decode(doc_db.last_diffs)
                self.doc['comments'] = json_decode(doc_db.comments)
                self.doc['settings'] = json_decode(doc_db.settings)
                self.doc['contents'] = json_decode(doc_db.contents)
                self.doc['metadata'] = json_decode(doc_db.metadata)
                self.doc['version'] = doc_db.version
                self.doc['diff_version'] = doc_db.diff_version
                self.doc['comment_version'] = doc_db.comment_version
                self.doc['title'] = doc_db.title
                self.doc['id'] = doc_db.id
                WebSocket.sessions[doc_db.id] = self.doc
            self.doc['participants'][self.id] = self
            response['type'] = 'welcome'
            self.write_message(response)

    def confirm_diff(self, request_id):
        response = dict()
        response['type'] = 'confirm_diff'
        response['request_id'] = request_id
        self.write_message(response)

    def send_document(self):
        response = dict()
        response['type'] = 'doc_data'
        response['doc'] = dict()
        response['doc']['id'] = self.doc['id']
        response['doc']['version'] = self.doc['version']
        if self.doc['diff_version'] < self.doc['version']:
            print('!!!diff version issue!!!')
            self.doc['diff_version'] = self.doc['version']
            self.doc["last_diffs"] = []
        response['doc']['title'] = self.doc['title']
        response['doc']['contents'] = self.doc['contents']
        response['doc']['metadata'] = self.doc['metadata']
        response['doc']['settings'] = self.doc['settings']
        doc_owner = self.doc['db'].owner
        access_rights = get_accessrights(
            AccessRight.objects.filter(
                document__owner=doc_owner))
        response['doc']['access_rights'] = access_rights

        if self.user_info.access_rights == 'read-without-comments':
            response['doc']['comments'] = []
        elif self.user_info.access_rights == 'review':
            # Reviewer should only get his/her own comments
            filtered_comments = {}
            for key, value in self.doc["comments"].items():
                if value["user"] == self.user_info.user.id:
                    filtered_comments[key] = value
            response['doc']['comments'] = filtered_comments
        else:
            response['doc']['comments'] = self.doc["comments"]
        response['doc']['comment_version'] = self.doc["comment_version"]
        response['doc']['access_rights'] = get_accessrights(
            AccessRight.objects.filter(document__owner=doc_owner))
        response['doc']['owner'] = dict()
        response['doc']['owner']['id'] = doc_owner.id
        response['doc']['owner']['name'] = doc_owner.readable_name
        response['doc']['owner'][
            'avatar'] = avatar_url(doc_owner, 80)
        response['doc']['owner']['team_members'] = []

        for team_member in doc_owner.leader.all():
            tm_object = dict()
            tm_object['id'] = team_member.member.id
            tm_object['name'] = team_member.member.readable_name
            tm_object['avatar'] = avatar_url(team_member.member, 80)
            response['doc']['owner']['team_members'].append(tm_object)
        response['doc_info'] = dict()
        response['doc_info']['is_owner'] = self.user_info.is_owner
        response['doc_info']['rights'] = self.user_info.access_rights
        if self.doc['version'] > self.doc['diff_version']:
            print('!!!diff version issue!!!')
            self.doc['diff_version'] = self.doc['version']
            self.doc["last_diffs"] = []
        elif self.doc['diff_version'] > self.doc['version']:
            needed_diffs = self.doc['diff_version'] - self.doc['version']
            # We only send those diffs needed by the receiver.
            response['doc_info']['unapplied_diffs'] = self.doc[
                "last_diffs"][-needed_diffs:]
        else:
            response['doc_info']['unapplied_diffs'] = []
        if self.user_info.is_owner:
            the_user = self.user_info.user
            response['doc']['owner']['email'] = the_user.email
            response['doc']['owner']['username'] = the_user.username
            response['doc']['owner']['first_name'] = the_user.first_name
            response['doc']['owner']['last_name'] = the_user.last_name
            response['doc']['owner']['email'] = the_user.email
        else:
            the_user = self.user_info.user
            response['user'] = dict()
            response['user']['id'] = the_user.id
            response['user']['name'] = the_user.readable_name
            response['user']['avatar'] = avatar_url(the_user, 80)
            response['user']['email'] = the_user.email
            response['user']['username'] = the_user.username
            response['user']['first_name'] = the_user.first_name
            response['user']['last_name'] = the_user.last_name
            response['user']['email'] = the_user.email
        response['doc_info']['session_id'] = self.id
        self.write_message(response)

    def on_message(self, message):
        if self.user_info.document_id not in WebSocket.sessions:
            print('receiving message for closed document')
            return
        parsed = json_decode(message)
        print(parsed["type"])
        if parsed["type"] == 'get_document':
            self.send_document()
        elif parsed["type"] == 'participant_update' and self.can_communicate():
            self.handle_participant_update()
        elif parsed["type"] == 'chat' and self.can_communicate():
            self.handle_chat(parsed)
        elif parsed["type"] == 'check_diff_version':
            self.check_diff_version(parsed)
        elif parsed["type"] == 'selection_change':
            self.handle_selection_change(parsed)
        elif (
            parsed["type"] == 'update_doc' and
            self.can_update_document()
        ):
            self.handle_document_update(parsed)
        elif parsed["type"] == 'update_title' and self.can_update_document():
            self.handle_title_update(parsed)
        elif parsed["type"] == 'diff' and self.can_update_document():
            self.handle_diff(parsed)

    def update_document(self, changes):
        if changes['version'] == self.doc['version']:
            # Document hasn't changed, return.
            return
        elif (
            changes['version'] > self.doc['diff_version'] or
            changes['version'] < self.doc['version']
        ):
            # The version number is too high. Possibly due to server restart.
            # Do not accept it, and send a document instead.
            self.send_document()
            return
        else:
            # The saved version does not contain all accepted diffs, so we keep
            # the remaining ones + 1000 in case a client needs to reconnect and
            # is missing some.
            remaining_diffs = 1000 + \
                self.doc['diff_version'] - changes['version']
            self.doc['last_diffs'] = self.doc['last_diffs'][-remaining_diffs:]
        self.doc['title'] = changes['title']
        self.doc['contents'] = changes['contents']
        self.doc['metadata'] = changes['metadata']
        self.doc['settings'] = changes['settings']
        self.doc['version'] = changes['version']

    def update_title(self, title):
        self.doc['title'] = title

    def update_comments(self, comments_updates):
        comments_updates = deepcopy(comments_updates)
        for cd in comments_updates:
            id = str(cd["id"])
            if cd["type"] == "create":
                del cd["type"]
                self.doc["comments"][id] = cd
            elif cd["type"] == "delete":
                del self.doc["comments"][id]
            elif cd["type"] == "update":
                self.doc["comments"][id]["comment"] = cd["comment"]
                if "review:isMajor" in cd:
                    self.doc["comments"][id][
                        "review:isMajor"] = cd["review:isMajor"]
            elif cd["type"] == "add_answer":
                comment_id = str(cd["commentId"])
                if "answers" not in self.doc["comments"][comment_id]:
                    self.doc["comments"][comment_id]["answers"] = []
                del cd["type"]
                self.doc["comments"][comment_id]["answers"].append(cd)
            elif cd["type"] == "delete_answer":
                comment_id = str(cd["commentId"])
                for answer in self.doc["comments"][comment_id]["answers"]:
                    if answer["id"] == cd["id"]:
                        self.doc["comments"][comment_id][
                            "answers"].remove(answer)
            elif cd["type"] == "update_answer":
                comment_id = str(cd["commentId"])
                for answer in self.doc["comments"][comment_id]["answers"]:
                    if answer["id"] == cd["id"]:
                        answer["answer"] = cd["answer"]
            self.doc['comment_version'] += 1

    def handle_participant_update(self):
        WebSocket.send_participant_list(self.user_info.document_id)

    def handle_document_update(self, parsed):
        self.update_document(parsed["doc"])
        WebSocket.save_document(self.user_info.document_id, False)
        message = {
            "type": 'check_hash',
            "diff_version": parsed["doc"]["version"],
            "hash": parsed["hash"]
        }
        WebSocket.send_updates(message, self.user_info.document_id, self.id)

    def handle_title_update(self, parsed):
        self.update_title(parsed["title"])
        WebSocket.save_document(self.user_info.document_id, False)

    def handle_chat(self, parsed):
        chat = {
            "id": str(uuid.uuid4()),
            "body": parsed['body'],
            "from": self.user_info.user.id,
            "type": 'chat'
        }
        WebSocket.send_updates(chat, self.user_info.document_id)

    def handle_selection_change(self, parsed):
        if self.user_info.document_id in WebSocket.sessions and parsed[
                "diff_version"] == self.doc['diff_version']:
            WebSocket.send_updates(
                parsed, self.user_info.document_id, self.id)

    # Checks if the diff only contains changes to comments.
    def only_comments(self, parsed_diffs):
        allowed_operations = ['addMark', 'removeMark']
        only_comment = True
        for diff in parsed_diffs:
            if not (diff['stepType'] in allowed_operations and diff[
                    'mark']['type'] == 'comment'):
                only_comment = False
        return only_comment

    def handle_diff(self, parsed):
        pdv = parsed["diff_version"]
        ddv = self.doc['diff_version']
        pcv = parsed["comment_version"]
        dcv = self.doc['comment_version']
        if (
            self.user_info.access_rights in COMMENT_ONLY and
            not self.only_comments(parsed['diff'])
        ):
            print(
                (
                    'received non-comment diff from comment-only '
                    'collaborator. Discarding.'
                )
            )
            return
        if pdv == ddv and pcv == dcv:
            self.doc["last_diffs"].extend(parsed["diff"])
            self.doc['diff_version'] += len(parsed["diff"])
            self.update_comments(parsed["comments"])
            self.confirm_diff(parsed["request_id"])
            WebSocket.send_updates(
                parsed,
                self.user_info.document_id,
                self.id,
                self.user_info.user.id
            )
        elif pdv > ddv:
            # Client has a higher version than server. Something is fishy!
            print('unfixable')
        elif pdv < ddv:
            if pdv + len(self.doc["last_diffs"]) >= ddv:
                # We have enough last_diffs stored to fix it.
                print("can fix it")
                number_diffs = \
                    parsed["diff_version"] - self.doc['diff_version']
                response = {
                    "type": "diff",
                    "server_fix": True,
                    "diff_version": parsed["diff_version"],
                    "diff": self.doc["last_diffs"][number_diffs:],
                    "reject_request_id": parsed["request_id"],
                }
                self.write_message(response)
            else:
                print('unfixable')
                # Client has a version that is too old to be fixed
                self.send_document()
        else:
            print('comment_version incorrect!')

    def check_diff_version(self, parsed):
        pdv = parsed["diff_version"]
        ddv = self.doc['diff_version']
        if pdv == ddv:
            response = {
                "type": "confirm_diff_version",
                "diff_version": pdv,
            }
            self.write_message(response)
            return
        elif pdv + len(self.doc["last_diffs"]) >= ddv:
            print("can fix it")
            number_diffs = pdv - ddv
            response = {
                "type": "diff",
                "server_fix": True,
                "diff_version": pdv,
                "diff": self.doc["last_diffs"][number_diffs:],
            }
            self.write_message(response)
            return
        else:
            print('unfixable')
            # Client has a version that is too old
            self.send_document()
            return

    def can_update_document(self):
        return self.user_info.access_rights in CAN_UPDATE_DOCUMENT

    def can_communicate(self):
        return self.user_info.access_rights in CAN_COMMUNICATE

    def on_close(self):
        print('Websocket closing')
        if (
            hasattr(self, 'user_info') and
            hasattr(self.user_info, 'document_id') and
            self.user_info.document_id in WebSocket.sessions and
            hasattr(self, 'id') and
            self.id in WebSocket.sessions[
                self.user_info.document_id
            ]['participants']
        ):
            del self.doc['participants'][self.id]
            if len(self.doc['participants'].keys()) == 0:
                WebSocket.save_document(self.user_info.document_id, True)
                del WebSocket.sessions[self.user_info.document_id]
                print("noone left")

    @classmethod
    def send_participant_list(cls, document_id):
        if document_id in WebSocket.sessions:
            participant_list = []
            for session_id, waiter in cls.sessions[
                document_id
            ]['participants'].items():
                access_rights = waiter.user_info.access_rights
                if access_rights not in CAN_COMMUNICATE:
                    continue
                participant_list.append({
                    'session_id': session_id,
                    'id': waiter.user_info.user.id,
                    'name': waiter.user_info.user.readable_name,
                    'avatar': avatar_url(waiter.user_info.user, 80)
                })
            message = {
                "participant_list": participant_list,
                "type": 'connections'
            }
            WebSocket.send_updates(message, document_id)

    @classmethod
    def send_updates(cls, message, document_id, sender_id=None, user_id=None):
        info("sending message to %d waiters", len(cls.sessions[document_id]))
        for waiter in cls.sessions[document_id]['participants'].values():
            if waiter.id != sender_id:
                access_rights = waiter.user_info.access_rights
                if "comments" in message and len(message["comments"]) > 0:
                    # Filter comments if needed
                    if access_rights == 'read-without-comments':
                        # The reader should not receive the comments update, so
                        # we remove the comments from the copy of the message
                        # sent to the reviewer. We still need to send the rest
                        # of the message as it may contain other diff
                        # information.
                        message = deepcopy(message)
                        message['comments'] = []
                    elif (
                        access_rights == 'review' and
                        user_id != waiter.user_info.user.id
                    ):
                        # The reviewer should not receive comments updates from
                        # others than themselves, so we remove the comments
                        # from the copy of the message sent to the reviewer
                        # that are not from them. We still need to sned the
                        # rest of the message as it may contain other diff
                        # information.
                        message = deepcopy(message)
                        message['comments'] = []
                elif (
                    message['type'] in ["chat", "connections"] and
                    access_rights not in CAN_COMMUNICATE
                ):
                    continue
                elif (
                    message['type'] == "selection_change" and
                    access_rights not in CAN_COMMUNICATE and
                    user_id != waiter.user_info.user.id
                ):
                    continue
                try:
                    waiter.write_message(message)
                except WebSocketClosedError:
                    error("Error sending message", exc_info=True)

    @classmethod
    def save_document(cls, document_id, all_have_left):
        doc = cls.sessions[document_id]
        doc_db = doc['db']
        doc_db.title = doc['title'][-255:]
        doc_db.version = doc['version']
        doc_db.diff_version = doc['diff_version']
        doc_db.comment_version = doc['comment_version']
        doc_db.contents = json_encode(doc['contents'])
        doc_db.metadata = json_encode(doc['metadata'])
        doc_db.settings = json_encode(doc['settings'])
        if all_have_left:
            remaining_diffs = doc['diff_version'] - doc['version']
            if remaining_diffs > 0:
                doc['last_diffs'] = doc['last_diffs'][-remaining_diffs:]
            else:
                doc['last_diffs'] = []
        doc_db.last_diffs = json_encode(doc['last_diffs'])
        doc_db.comments = json_encode(doc['comments'])
        print('saving document #' + str(doc_db.id))
        print('version ' + str(doc_db.version))
        doc_db.save()

    @classmethod
    def save_all_docs(cls):
        for document_id in cls.sessions:
            cls.save_document(document_id, True)
示例#13
0
class WebSocket(BaseWebSocketHandler):
    sessions = dict()

    def open(self, arg):
        super().open(arg)
        if len(self.args) < 2:
            self.access_denied()
            return
        self.document_id = int(self.args[0])

    def confirm_diff(self, rid):
        response = {
            'type': 'confirm_diff',
            'rid': rid
        }
        self.send_message(response)

    def subscribe_doc(self, connection_count=0):
        self.user_info = SessionUserInfo(self.user)
        doc_db, can_access = self.user_info.init_access(
            self.document_id
        )
        if not can_access or float(doc_db.doc_version) != FW_DOCUMENT_VERSION:
            self.access_denied()
            return
        if (
            doc_db.id in WebSocket.sessions and
            len(WebSocket.sessions[doc_db.id]['participants']) > 0
        ):
            logger.debug("Serving already opened file")
            self.doc = WebSocket.sessions[doc_db.id]
            self.id = max(self.doc['participants']) + 1
            self.doc['participants'][self.id] = self
            logger.debug("id when opened %s" % self.id)
        else:
            logger.debug("Opening file")
            self.id = 0
            self.doc = {
                'db': doc_db,
                'participants': {
                    0: self
                },
                'last_diffs': json_decode(doc_db.last_diffs),
                'comments': json_decode(doc_db.comments),
                'bibliography': json_decode(doc_db.bibliography),
                'contents': json_decode(doc_db.contents),
                'version': doc_db.version,
                'title': doc_db.title,
                'id': doc_db.id,
                'template': {
                    'title': doc_db.template.title,
                    'definition': json_decode(doc_db.template.definition)
                }
            }
            WebSocket.sessions[doc_db.id] = self.doc
        self.send_message({
            'type': 'subscribed'
        })
        if connection_count < 1:
            self.send_styles()
            self.send_document()
        if self.can_communicate():
            self.handle_participant_update()

    def send_styles(self):
        doc_db = self.doc['db']
        response = dict()
        response['type'] = 'styles'
        serializer = PythonWithURLSerializer()
        export_temps = serializer.serialize(
            doc_db.template.export_templates.all()
        )
        document_styles = serializer.serialize(
            doc_db.template.document_styles.all(),
            use_natural_foreign_keys=True
        )
        cite_styles = serializer.serialize(
            doc_db.template.citation_styles.all()
        )
        cite_locales = serializer.serialize(
            CitationLocale.objects.all()
        )
        response['styles'] = {
            'export_templates': [obj['fields'] for obj in export_temps],
            'document_styles': [obj['fields'] for obj in document_styles],
            'citation_styles': [obj['fields'] for obj in cite_styles],
            'citation_locales': [obj['fields'] for obj in cite_locales],
        }
        self.send_message(response)

    def send_document(self):
        response = dict()
        response['type'] = 'doc_data'
        doc_owner = self.doc['db'].owner
        response['doc_info'] = {
            'id': self.doc['id'],
            'is_owner': self.user_info.is_owner,
            'access_rights': self.user_info.access_rights,
            'owner': {
                'id': doc_owner.id,
                'name': doc_owner.readable_name,
                'username': doc_owner.username,
                'avatar': get_user_avatar_url(doc_owner)['url'],
                'team_members': []
            }
        }
        response['doc'] = {
            'v': self.doc['version'],
            'contents': self.doc['contents'],
            'bibliography': self.doc['bibliography'],
            'template': self.doc['template'],
            'images': {}
        }
        response['time'] = int(time()) * 1000
        for dimage in DocumentImage.objects.filter(document_id=self.doc['id']):
            image = dimage.image
            field_obj = {
                'id': image.id,
                'title': dimage.title,
                'image': image.image.url,
                'file_type': image.file_type,
                'added': mktime(image.added.timetuple()) * 1000,
                'checksum': image.checksum,
                'cats': []
            }
            if image.thumbnail:
                field_obj['thumbnail'] = image.thumbnail.url
                field_obj['height'] = image.height
                field_obj['width'] = image.width
            response['doc']['images'][image.id] = field_obj
        if self.user_info.access_rights == 'read-without-comments':
            response['doc']['comments'] = []
        elif self.user_info.access_rights == 'review':
            # Reviewer should only get his/her own comments
            filtered_comments = {}
            for key, value in list(self.doc["comments"].items()):
                if value["user"] == self.user_info.user.id:
                    filtered_comments[key] = value
            response['doc']['comments'] = filtered_comments
        else:
            response['doc']['comments'] = self.doc["comments"]
        for team_member in doc_owner.leader.all():
            tm_object = dict()
            tm_object['id'] = team_member.member.id
            tm_object['name'] = team_member.member.readable_name
            tm_object['username'] = team_member.member.get_username()
            tm_object['avatar'] = get_user_avatar_url(
                team_member.member
            )['url']
            response['doc_info']['owner']['team_members'].append(tm_object)
        collaborators = get_accessrights(
            AccessRight.objects.filter(document__owner=doc_owner)
        )
        response['doc_info']['collaborators'] = collaborators
        response['doc_info']['session_id'] = self.id
        self.send_message(response)

    def reject_message(self, message):
        if (message["type"] == "diff"):
            self.send_message({
                'type': 'reject_diff',
                'rid': message['rid']
            })

    def handle_message(self, message):
        if message["type"] == 'subscribe':
            connection_count = 0
            if 'connection' in message:
                connection_count = message['connection']
            self.subscribe_doc(connection_count)
            return
        if self.user_info.document_id not in WebSocket.sessions:
            logger.debug('receiving message for closed document')
            return
        if message["type"] == 'get_document':
            self.send_document()
        elif (
            message["type"] == 'participant_update' and
            self.can_communicate()
        ):
            self.handle_participant_update()
        elif message["type"] == 'chat' and self.can_communicate():
            self.handle_chat(message)
        elif message["type"] == 'check_version':
            self.check_version(message)
        elif message["type"] == 'selection_change':
            self.handle_selection_change(message)
        elif message["type"] == 'diff' and self.can_update_document():
            self.handle_diff(message)

    def update_bibliography(self, bibliography_updates):
        for bu in bibliography_updates:
            if "id" not in bu:
                continue
            id = bu["id"]
            if bu["type"] == "update":
                self.doc["bibliography"][id] = bu["reference"]
            elif bu["type"] == "delete":
                del self.doc["bibliography"][id]

    def update_images(self, image_updates):
        for iu in image_updates:
            if "id" not in iu:
                continue
            id = iu["id"]
            if iu["type"] == "update":
                # Ensure that access rights exist
                if not UserImage.objects.filter(
                    image__id=id,
                    owner=self.user_info.user
                ).exists():
                    continue
                doc_image = DocumentImage.objects.filter(
                    document_id=self.doc["id"],
                    image_id=id
                )
                if doc_image.exists():
                    doc_image.title = iu["image"]["title"]
                    doc_image.save()
                else:
                    DocumentImage.objects.create(
                        document_id=self.doc["id"],
                        image_id=id,
                        title=iu["image"]["title"]
                    )
            elif iu["type"] == "delete":
                DocumentImage.objects.filter(
                    document_id=self.doc["id"],
                    image_id=id
                ).delete()
                for image in Image.objects.filter(id=id):
                    if image.is_deletable():
                        image.delete()

    def update_comments(self, comments_updates):
        comments_updates = deepcopy(comments_updates)
        for cd in comments_updates:
            if "id" not in cd:
                # ignore
                continue
            id = cd["id"]
            if cd["type"] == "create":
                self.doc["comments"][id] = {
                    "user": cd["user"],
                    "username": cd["username"],
                    "assignedUser": cd["assignedUser"],
                    "assignedUsername": cd["assignedUsername"],
                    "date": cd["date"],
                    "comment": cd["comment"],
                    "isMajor": cd["isMajor"],
                    "resolved": cd["resolved"],
                }
            elif cd["type"] == "delete":
                del self.doc["comments"][id]
            elif cd["type"] == "update":
                self.doc["comments"][id]["comment"] = cd["comment"]
                if "isMajor" in cd:
                    self.doc["comments"][id][
                        "isMajor"] = cd["isMajor"]
                if "assignedUser" in cd and "assignedUsername" in cd:
                    self.doc["comments"][id][
                        "assignedUser"] = cd["assignedUser"]
                    self.doc["comments"][id][
                        "assignedUsername"] = cd["assignedUsername"]
                if "resolved" in cd:
                    self.doc["comments"][id][
                        "resolved"] = cd["resolved"]
            elif cd["type"] == "add_answer":
                if "answers" not in self.doc["comments"][id]:
                    self.doc["comments"][id]["answers"] = []
                self.doc["comments"][id]["answers"].append({
                    "id": cd["answerId"],
                    "user": cd["user"],
                    "username": cd["username"],
                    "date": cd["date"],
                    "answer": cd["answer"]
                })
            elif cd["type"] == "delete_answer":
                answer_id = cd["answerId"]
                for answer in self.doc["comments"][id]["answers"]:
                    if answer["id"] == answer_id:
                        self.doc["comments"][id]["answers"].remove(answer)
            elif cd["type"] == "update_answer":
                answer_id = cd["answerId"]
                for answer in self.doc["comments"][id]["answers"]:
                    if answer["id"] == answer_id:
                        answer["answer"] = cd["answer"]

    def handle_participant_update(self):
        WebSocket.send_participant_list(self.user_info.document_id)

    def handle_chat(self, message):
        chat = {
            "id": str(uuid.uuid4()),
            "body": message['body'],
            "from": self.user_info.user.id,
            "type": 'chat'
        }
        WebSocket.send_updates(chat, self.user_info.document_id)

    def handle_selection_change(self, message):
        if self.user_info.document_id in WebSocket.sessions and message[
                "v"] == self.doc['version']:
            WebSocket.send_updates(
                message, self.user_info.document_id, self.id)

    # Checks if the diff only contains changes to comments.
    def only_comments(self, message):
        allowed_operations = ['addMark', 'removeMark']
        only_comment = True
        if "ds" in message:  # ds = document steps
            for step in message["ds"]:
                if not (step['stepType'] in allowed_operations and step[
                        'mark']['type'] == 'comment'):
                    only_comment = False
        return only_comment

    def handle_diff(self, message):
        pv = message["v"]
        dv = self.doc['version']
        logger.debug("PV: %d, DV: %d" % (pv, dv))
        if (
            self.user_info.access_rights in COMMENT_ONLY and
            not self.only_comments(message)
        ):
            logger.error(
                (
                    'received non-comment diff from comment-only '
                    'collaborator. Discarding.'
                )
            )
            return
        if pv == dv:
            self.doc["last_diffs"].append(message)
            # Only keep the last 1000 diffs
            self.doc["last_diffs"] = self.doc["last_diffs"][-1000:]
            self.doc['version'] += 1
            if "jd" in message:  # jd = json diff
                try:
                    apply_patch(
                       self.doc['contents'],
                       message["jd"],
                       True
                    )
                except (JsonPatchConflict, JsonPointerException):
                    logger.exception("Cannot apply json diff.")
                    logger.error(json_encode(message))
                    logger.error(json_encode(self.doc['contents']))
                    self.send_document()
                # The json diff is only needed by the python backend which does
                # not understand the steps. It can therefore be removed before
                # broadcast to other clients.
                del message["jd"]
            if "ti" in message:  # ti = title
                self.doc["title"] = message["ti"]
            if "cu" in message:  # cu = comment updates
                self.update_comments(message["cu"])
            if "bu" in message:  # bu = bibliography updates
                self.update_bibliography(message["bu"])
            if "iu" in message:  # iu = image updates
                self.update_images(message["iu"])
            if self.doc['version'] % 10 == 0:
                WebSocket.save_document(self.user_info.document_id)
            self.confirm_diff(message["rid"])
            WebSocket.send_updates(
                message,
                self.user_info.document_id,
                self.id,
                self.user_info.user.id
            )
        elif pv < dv:
            if pv + len(self.doc["last_diffs"]) >= dv:
                # We have enough last_diffs stored to fix it.
                logger.debug("can fix it")
                number_diffs = pv - dv
                messages = self.doc["last_diffs"][number_diffs:]
                for message in messages:
                    new_message = message.copy()
                    new_message["server_fix"] = True
                    self.send_message(new_message)
            else:
                logger.debug('unfixable')
                # Client has a version that is too old to be fixed
                self.send_document()
        else:
            # Client has a higher version than server. Something is fishy!
            logger.debug('unfixable')

    def check_version(self, message):
        pv = message["v"]
        dv = self.doc['version']
        logger.debug("PV: %d, DV: %d" % (pv, dv))
        if pv == dv:
            response = {
                "type": "confirm_version",
                "v": pv,
            }
            self.send_message(response)
            return
        elif pv + len(self.doc["last_diffs"]) >= dv:
            logger.debug("can fix it")
            number_diffs = pv - dv
            messages = self.doc["last_diffs"][number_diffs:]
            for message in messages:
                new_message = message.copy()
                new_message["server_fix"] = True
                self.send_message(new_message)
            return
        else:
            logger.debug('unfixable')
            # Client has a version that is too old
            self.send_document()
            return

    def can_update_document(self):
        return self.user_info.access_rights in CAN_UPDATE_DOCUMENT

    def can_communicate(self):
        return self.user_info.access_rights in CAN_COMMUNICATE

    def on_close(self):
        logger.debug('Websocket closing')
        if (
            hasattr(self, 'user_info') and
            hasattr(self.user_info, 'document_id') and
            self.user_info.document_id in WebSocket.sessions and
            hasattr(self, 'id') and
            self.id in WebSocket.sessions[
                self.user_info.document_id
            ]['participants']
        ):
            del self.doc['participants'][self.id]
            if len(self.doc['participants']) == 0:
                WebSocket.save_document(self.user_info.document_id)
                del WebSocket.sessions[self.user_info.document_id]
                logger.debug("noone left")
            else:
                WebSocket.send_participant_list(self.user_info.document_id)

    @classmethod
    def send_participant_list(cls, document_id):
        if document_id in WebSocket.sessions:
            participant_list = []
            for session_id, waiter in list(cls.sessions[
                document_id
            ]['participants'].items()):
                access_rights = waiter.user_info.access_rights
                if access_rights not in CAN_COMMUNICATE:
                    continue
                participant_list.append({
                    'session_id': session_id,
                    'id': waiter.user_info.user.id,
                    'name': waiter.user_info.user.readable_name,
                    'avatar': get_user_avatar_url(waiter.user_info.user)['url']
                })
            message = {
                "participant_list": participant_list,
                "type": 'connections'
            }
            WebSocket.send_updates(message, document_id)

    @classmethod
    def send_updates(cls, message, document_id, sender_id=None, user_id=None):
        logger.debug(
            "Sending message to %d waiters",
            len(cls.sessions[document_id]['participants'])
        )
        for waiter in list(cls.sessions[document_id]['participants'].values()):
            if waiter.id != sender_id:
                access_rights = waiter.user_info.access_rights
                if "comments" in message and len(message["comments"]) > 0:
                    # Filter comments if needed
                    if access_rights == 'read-without-comments':
                        # The reader should not receive the comments update, so
                        # we remove the comments from the copy of the message
                        # sent to the reviewer. We still need to send the rest
                        # of the message as it may contain other diff
                        # information.
                        message = deepcopy(message)
                        message['comments'] = []
                    elif (
                        access_rights == 'review' and
                        user_id != waiter.user_info.user.id
                    ):
                        # The reviewer should not receive comments updates from
                        # others than themselves, so we remove the comments
                        # from the copy of the message sent to the reviewer
                        # that are not from them. We still need to sned the
                        # rest of the message as it may contain other diff
                        # information.
                        message = deepcopy(message)
                        message['comments'] = []
                elif (
                    message['type'] in ["chat", "connections"] and
                    access_rights not in CAN_COMMUNICATE
                ):
                    continue
                elif (
                    message['type'] == "selection_change" and
                    access_rights not in CAN_COMMUNICATE and
                    user_id != waiter.user_info.user.id
                ):
                    continue
                waiter.send_message(message)

    @classmethod
    def save_document(cls, document_id):
        doc = cls.sessions[document_id]
        doc_db = doc['db']
        if doc_db.version == doc['version']:
            return
        doc_db.title = doc['title'][-255:]
        doc_db.version = doc['version']
        doc_db.contents = json_encode(doc['contents'])
        doc_db.last_diffs = json_encode(doc['last_diffs'])
        doc_db.comments = json_encode(doc['comments'])
        doc_db.bibliography = json_encode(doc['bibliography'])
        logger.debug('saving document # %d' % doc_db.id)
        logger.debug('version %d' % doc_db.version)
        doc_db.save()

    @classmethod
    def save_all_docs(cls):
        for document_id in cls.sessions:
            cls.save_document(document_id)