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).
This commit is contained in:
Goffi 2018-03-16 17:06:35 +01:00
commit 07fa89be84
4 changed files with 150 additions and 4 deletions

View file

@ -25,6 +25,10 @@ log = getLogger(__name__)
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 @@ PLUGIN_INFO = {
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 @@ class FileSharing(object):
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 @@ class FileSharing(object):
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): <item> 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)