comparison src/plugins/plugin_comp_file_sharing.py @ 2527:a201194fc461

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).
author Goffi <goffi@goffi.org>
date Fri, 16 Mar 2018 17:06:35 +0100
parents 95c31756944c
children 65e278997715
comparison
equal deleted inserted replaced
2526:35d591086974 2527:a201194fc461
23 from sat.core.log import getLogger 23 from sat.core.log import getLogger
24 log = getLogger(__name__) 24 log = getLogger(__name__)
25 from sat.tools.common import regex 25 from sat.tools.common import regex
26 from sat.tools import stream 26 from sat.tools import stream
27 from twisted.internet import defer 27 from twisted.internet import defer
28 from twisted.words.protocols.jabber import error
29 from wokkel import pubsub
30 from wokkel import generic
31 from functools import partial
28 import os 32 import os
29 import os.path 33 import os.path
30 import mimetypes 34 import mimetypes
31 35
32 36
37 C.PI_TYPE: C.PLUG_TYPE_ENTRY_POINT, 41 C.PI_TYPE: C.PLUG_TYPE_ENTRY_POINT,
38 C.PI_PROTOCOLS: [], 42 C.PI_PROTOCOLS: [],
39 C.PI_DEPENDENCIES: ["FILE", "XEP-0231", "XEP-0234", "XEP-0260", "XEP-0261", "XEP-0264", "XEP-0329"], 43 C.PI_DEPENDENCIES: ["FILE", "XEP-0231", "XEP-0234", "XEP-0260", "XEP-0261", "XEP-0264", "XEP-0329"],
40 C.PI_RECOMMENDATIONS: [], 44 C.PI_RECOMMENDATIONS: [],
41 C.PI_MAIN: "FileSharing", 45 C.PI_MAIN: "FileSharing",
42 C.PI_HANDLER: "no", 46 C.PI_HANDLER: C.BOOL_TRUE,
43 C.PI_DESCRIPTION: _(u"""Component hosting and sharing files""") 47 C.PI_DESCRIPTION: _(u"""Component hosting and sharing files""")
44 } 48 }
45 49
46 HASH_ALGO = u'sha-256' 50 HASH_ALGO = u'sha-256'
51 COMMENT_NODE_PREFIX = 'org.salut-a-toi.file_comments/'
47 52
48 53
49 class FileSharing(object): 54 class FileSharing(object):
50 55
51 def __init__(self, host): 56 def __init__(self, host):
56 self._h = host.plugins['XEP-0300'] 61 self._h = host.plugins['XEP-0300']
57 self._t = host.plugins['XEP-0264'] 62 self._t = host.plugins['XEP-0264']
58 host.trigger.add("FILE_getDestDir", self._getDestDirTrigger) 63 host.trigger.add("FILE_getDestDir", self._getDestDirTrigger)
59 host.trigger.add("XEP-0234_fileSendingRequest", self._fileSendingRequestTrigger, priority=1000) 64 host.trigger.add("XEP-0234_fileSendingRequest", self._fileSendingRequestTrigger, priority=1000)
60 self.files_path = host.getLocalPath(None, C.FILES_DIR, profile=False) 65 self.files_path = host.getLocalPath(None, C.FILES_DIR, profile=False)
66
67 def getHandler(self, client):
68 return Comments_handler(self)
61 69
62 def profileConnected(self, client): 70 def profileConnected(self, client):
63 path = client.file_tmp_dir = os.path.join( 71 path = client.file_tmp_dir = os.path.join(
64 self.host.memory.getConfig('', 'local_dir'), 72 self.host.memory.getConfig('', 'local_dir'),
65 C.FILES_TMP_DIR, 73 C.FILES_TMP_DIR,
181 def _fileSendingRequestTrigger(self, client, session, content_data, content_name, file_data, file_elt): 189 def _fileSendingRequestTrigger(self, client, session, content_data, content_name, file_data, file_elt):
182 if not client.is_component: 190 if not client.is_component:
183 return True, None 191 return True, None
184 else: 192 else:
185 return False, self._retrieveFiles(client, session, content_data, content_name, file_data, file_elt) 193 return False, self._retrieveFiles(client, session, content_data, content_name, file_data, file_elt)
194
195
196 class Comments_handler(pubsub.PubSubService):
197
198 def __init__(self, plugin_parent):
199 super(Comments_handler, self).__init__() # PubsubVirtualResource())
200 self.host = plugin_parent.host
201 self.plugin_parent = plugin_parent
202
203 def _getFileId(self, nodeIdentifier):
204 if not nodeIdentifier.startswith(COMMENT_NODE_PREFIX):
205 raise error.StanzaError('item-not-found')
206 file_id = nodeIdentifier[len(COMMENT_NODE_PREFIX):]
207 if not file_id:
208 raise error.StanzaError('item-not-found')
209 return file_id
210
211 @defer.inlineCallbacks
212 def getFileData(self, requestor, nodeIdentifier):
213 file_id = self._getFileId(nodeIdentifier)
214 try:
215 files = yield self.host.memory.getFiles(self.parent, requestor, file_id)
216 except (exceptions.NotFound, exceptions.PermissionError):
217 # we don't differenciate between NotFound and PermissionError
218 # to avoid leaking information on existing files
219 raise error.StanzaError('item-not-found')
220 if not files:
221 raise error.StanzaError('item-not-found')
222 if len(files) > 1:
223 raise error.InternalError('there should be only one file')
224 defer.returnValue(files[0])
225
226 def commentsUpdate(self, extra, new_comments, peer_jid):
227 """update comments (replace or insert new_comments)
228
229 @param extra(dict): extra data to update
230 @param new_comments(list[tuple(unicode, unicode, unicode)]): comments to update or insert
231 @param peer_jid(unicode, None): bare jid of the requestor, or None if request is done by owner
232 """
233 current_comments = extra.setdefault('comments', [])
234 new_comments_by_id = {c[0]:c for c in new_comments}
235 updated = []
236 # we now check every current comment, to see if one id in new ones
237 # exist, in which case we must update
238 for idx, comment in enumerate(current_comments):
239 comment_id = comment[0]
240 if comment_id in new_comments_by_id:
241 # a new comment has an existing id, update is requested
242 if peer_jid and comment[1] != peer_jid:
243 # requestor has not the right to modify the comment
244 raise exceptions.PermissionError
245 # we replace old_comment with updated one
246 new_comment = new_comments_by_id[comment_id]
247 current_comments[idx] = new_comment
248 updated.append(new_comment)
249
250 # we now remove every updated comments, to only keep
251 # the ones to insert
252 for comment in updated:
253 new_comments.remove(comment)
254
255 current_comments.extend(new_comments)
256
257 def commentsDelete(self, extra, comments):
258 try:
259 comments_dict = extra['comments']
260 except KeyError:
261 return
262 for comment in comments:
263 try:
264 comments_dict.remove(comment)
265 except ValueError:
266 continue
267
268 def _getFrom(self, item_elt):
269 """retrieve published of an item
270
271 @param item_elt(domish.element): <item> element
272 @return (unicode): full jid as string
273 """
274 iq_elt = item_elt
275 while iq_elt.parent != None:
276 iq_elt = iq_elt.parent
277 return iq_elt['from']
278
279 @defer.inlineCallbacks
280 def publish(self, requestor, service, nodeIdentifier, items):
281 # we retrieve file a first time to check authorisations
282 file_data = yield self.getFileData(requestor, nodeIdentifier)
283 file_id = file_data['id']
284 comments = [(item['id'], self._getFrom(item), item.toXml()) for item in items]
285 if requestor.userhostJID() == file_data['owner']:
286 peer_jid = None
287 else:
288 peer_jid = requestor.userhost()
289 update_cb = partial(self.commentsUpdate, new_comments=comments, peer_jid=peer_jid)
290 try:
291 yield self.host.memory.fileUpdate(file_id, 'extra', update_cb)
292 except exceptions.PermissionError:
293 raise error.StanzaError('not-authorized')
294
295 @defer.inlineCallbacks
296 def items(self, requestor, service, nodeIdentifier, maxItems,
297 itemIdentifiers):
298 file_data = yield self.getFileData(requestor, nodeIdentifier)
299 comments = file_data['extra'].get('comments', [])
300 if itemIdentifiers:
301 defer.returnValue([generic.parseXml(c[2]) for c in comments if c[0] in itemIdentifiers])
302 else:
303 defer.returnValue([generic.parseXml(c[2]) for c in comments])
304
305 @defer.inlineCallbacks
306 def retract(self, requestor, service, nodeIdentifier, itemIdentifiers):
307 file_data = yield self.getFileData(requestor, nodeIdentifier)
308 file_id = file_data['id']
309 try:
310 comments = file_data['extra']['comments']
311 except KeyError:
312 raise error.StanzaError('item-not-found')
313
314 to_remove = []
315 for comment in comments:
316 comment_id = comment[0]
317 if comment_id in itemIdentifiers:
318 to_remove.append(comment)
319 itemIdentifiers.remove(comment_id)
320 if not itemIdentifiers:
321 break
322
323 if itemIdentifiers:
324 # not all items have been to_remove, we can't continue
325 raise error.StanzaError('item-not-found')
326
327 if requestor.userhostJID() != file_data['owner']:
328 if not all([c[1] == requestor.userhost() for c in to_remove]):
329 raise error.StanzaError('not-authorized')
330
331 remove_cb = partial(self.commentsDelete, comments=to_remove)
332 yield self.host.memory.fileUpdate(file_id, 'extra', remove_cb)