# HG changeset patch # User Goffi # Date 1521216395 -3600 # Node ID a201194fc46198802e9d977babaa1bb4cdec5e8b # Parent 35d591086974ce51406ec2c1e250d27989c3e618 component file sharing: comments handling first draft: comments use a minimal pubsub service which create virtual nodes for each files. A pubsub request to org.salut-a-toi.file_comments/[FILE_ID] allow to handle comments in classic way. Permissions are the same as for files (i.e. if an entity can see a file, she can comment it). diff -r 35d591086974 -r a201194fc461 src/core/xmpp.py --- a/src/core/xmpp.py Fri Mar 16 17:03:46 2018 +0100 +++ b/src/core/xmpp.py Fri Mar 16 17:06:35 2018 +0100 @@ -629,7 +629,7 @@ if not required: return else: - log.error(_(u"Plugin {current_name} if needed for {entry_name}, but it doesn't handle component mode").format( + log.error(_(u"Plugin {current_name} is needed for {entry_name}, but it doesn't handle component mode").format( current_name = current._info[u'import_name'], entry_name = self.entry_plugin._info[u'import_name'] )) diff -r 35d591086974 -r a201194fc461 src/plugins/plugin_comp_file_sharing.py --- a/src/plugins/plugin_comp_file_sharing.py Fri Mar 16 17:03:46 2018 +0100 +++ b/src/plugins/plugin_comp_file_sharing.py Fri Mar 16 17:06:35 2018 +0100 @@ -25,6 +25,10 @@ from sat.tools.common import regex from sat.tools import stream from twisted.internet import defer +from twisted.words.protocols.jabber import error +from wokkel import pubsub +from wokkel import generic +from functools import partial import os import os.path import mimetypes @@ -39,11 +43,12 @@ C.PI_DEPENDENCIES: ["FILE", "XEP-0231", "XEP-0234", "XEP-0260", "XEP-0261", "XEP-0264", "XEP-0329"], C.PI_RECOMMENDATIONS: [], C.PI_MAIN: "FileSharing", - C.PI_HANDLER: "no", + C.PI_HANDLER: C.BOOL_TRUE, C.PI_DESCRIPTION: _(u"""Component hosting and sharing files""") } HASH_ALGO = u'sha-256' +COMMENT_NODE_PREFIX = 'org.salut-a-toi.file_comments/' class FileSharing(object): @@ -59,6 +64,9 @@ host.trigger.add("XEP-0234_fileSendingRequest", self._fileSendingRequestTrigger, priority=1000) self.files_path = host.getLocalPath(None, C.FILES_DIR, profile=False) + def getHandler(self, client): + return Comments_handler(self) + def profileConnected(self, client): path = client.file_tmp_dir = os.path.join( self.host.memory.getConfig('', 'local_dir'), @@ -183,3 +191,142 @@ return True, None else: return False, self._retrieveFiles(client, session, content_data, content_name, file_data, file_elt) + + +class Comments_handler(pubsub.PubSubService): + + def __init__(self, plugin_parent): + super(Comments_handler, self).__init__() # PubsubVirtualResource()) + self.host = plugin_parent.host + self.plugin_parent = plugin_parent + + def _getFileId(self, nodeIdentifier): + if not nodeIdentifier.startswith(COMMENT_NODE_PREFIX): + raise error.StanzaError('item-not-found') + file_id = nodeIdentifier[len(COMMENT_NODE_PREFIX):] + if not file_id: + raise error.StanzaError('item-not-found') + return file_id + + @defer.inlineCallbacks + def getFileData(self, requestor, nodeIdentifier): + file_id = self._getFileId(nodeIdentifier) + try: + files = yield self.host.memory.getFiles(self.parent, requestor, file_id) + except (exceptions.NotFound, exceptions.PermissionError): + # we don't differenciate between NotFound and PermissionError + # to avoid leaking information on existing files + raise error.StanzaError('item-not-found') + if not files: + raise error.StanzaError('item-not-found') + if len(files) > 1: + raise error.InternalError('there should be only one file') + defer.returnValue(files[0]) + + def commentsUpdate(self, extra, new_comments, peer_jid): + """update comments (replace or insert new_comments) + + @param extra(dict): extra data to update + @param new_comments(list[tuple(unicode, unicode, unicode)]): comments to update or insert + @param peer_jid(unicode, None): bare jid of the requestor, or None if request is done by owner + """ + current_comments = extra.setdefault('comments', []) + new_comments_by_id = {c[0]:c for c in new_comments} + updated = [] + # we now check every current comment, to see if one id in new ones + # exist, in which case we must update + for idx, comment in enumerate(current_comments): + comment_id = comment[0] + if comment_id in new_comments_by_id: + # a new comment has an existing id, update is requested + if peer_jid and comment[1] != peer_jid: + # requestor has not the right to modify the comment + raise exceptions.PermissionError + # we replace old_comment with updated one + new_comment = new_comments_by_id[comment_id] + current_comments[idx] = new_comment + updated.append(new_comment) + + # we now remove every updated comments, to only keep + # the ones to insert + for comment in updated: + new_comments.remove(comment) + + current_comments.extend(new_comments) + + def commentsDelete(self, extra, comments): + try: + comments_dict = extra['comments'] + except KeyError: + return + for comment in comments: + try: + comments_dict.remove(comment) + except ValueError: + continue + + def _getFrom(self, item_elt): + """retrieve published of an item + + @param item_elt(domish.element): element + @return (unicode): full jid as string + """ + iq_elt = item_elt + while iq_elt.parent != None: + iq_elt = iq_elt.parent + return iq_elt['from'] + + @defer.inlineCallbacks + def publish(self, requestor, service, nodeIdentifier, items): + # we retrieve file a first time to check authorisations + file_data = yield self.getFileData(requestor, nodeIdentifier) + file_id = file_data['id'] + comments = [(item['id'], self._getFrom(item), item.toXml()) for item in items] + if requestor.userhostJID() == file_data['owner']: + peer_jid = None + else: + peer_jid = requestor.userhost() + update_cb = partial(self.commentsUpdate, new_comments=comments, peer_jid=peer_jid) + try: + yield self.host.memory.fileUpdate(file_id, 'extra', update_cb) + except exceptions.PermissionError: + raise error.StanzaError('not-authorized') + + @defer.inlineCallbacks + def items(self, requestor, service, nodeIdentifier, maxItems, + itemIdentifiers): + file_data = yield self.getFileData(requestor, nodeIdentifier) + comments = file_data['extra'].get('comments', []) + if itemIdentifiers: + defer.returnValue([generic.parseXml(c[2]) for c in comments if c[0] in itemIdentifiers]) + else: + defer.returnValue([generic.parseXml(c[2]) for c in comments]) + + @defer.inlineCallbacks + def retract(self, requestor, service, nodeIdentifier, itemIdentifiers): + file_data = yield self.getFileData(requestor, nodeIdentifier) + file_id = file_data['id'] + try: + comments = file_data['extra']['comments'] + except KeyError: + raise error.StanzaError('item-not-found') + + to_remove = [] + for comment in comments: + comment_id = comment[0] + if comment_id in itemIdentifiers: + to_remove.append(comment) + itemIdentifiers.remove(comment_id) + if not itemIdentifiers: + break + + if itemIdentifiers: + # not all items have been to_remove, we can't continue + raise error.StanzaError('item-not-found') + + if requestor.userhostJID() != file_data['owner']: + if not all([c[1] == requestor.userhost() for c in to_remove]): + raise error.StanzaError('not-authorized') + + remove_cb = partial(self.commentsDelete, comments=to_remove) + yield self.host.memory.fileUpdate(file_id, 'extra', remove_cb) diff -r 35d591086974 -r a201194fc461 src/plugins/plugin_xep_0231.py --- a/src/plugins/plugin_xep_0231.py Fri Mar 16 17:03:46 2018 +0100 +++ b/src/plugins/plugin_xep_0231.py Fri Mar 16 17:06:35 2018 +0100 @@ -1,7 +1,7 @@ #!/usr/bin/env python2 # -*- coding: utf-8 -*- -# SAT plugin for Jingle File Transfer (XEP-0231) +# SAT plugin for Bit of Binary handling (XEP-0231) # Copyright (C) 2009-2018 Jérôme Poisson (goffi@goffi.org) # This program is free software: you can redistribute it and/or modify diff -r 35d591086974 -r a201194fc461 src/plugins/plugin_xep_0329.py --- a/src/plugins/plugin_xep_0329.py Fri Mar 16 17:03:46 2018 +0100 +++ b/src/plugins/plugin_xep_0329.py Fri Mar 16 17:06:35 2018 +0100 @@ -75,7 +75,6 @@ self.type = type_ self.access = {} if access is None else access assert isinstance(self.access, dict) - self.persistent = False self.parent = None if parent is not None: assert name