Mercurial > libervia-backend
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) |