Mercurial > libervia-pubsub
comparison sat_pubsub/backend.py @ 414:ccb2a22ea0fc
Python 3 port:
/!\ Python 3.6+ is now needed to use SàT Pubsub
/!\ instability may occur and features may not be working anymore, this will improve with time
The same procedure as in backend has been applied (check backend commit ab2696e34d29 logs
for details).
Python minimal version has been updated in setup.py
author | Goffi <goffi@goffi.org> |
---|---|
date | Fri, 16 Aug 2019 12:53:33 +0200 |
parents | c56a728412f1 |
children | 4179ed660a85 |
comparison
equal
deleted
inserted
replaced
413:a5edf5e1dd74 | 414:ccb2a22ea0fc |
---|---|
1 #!/usr/bin/python | 1 #!/usr/bin/env python3 |
2 #-*- coding: utf-8 -*- | 2 #-*- coding: utf-8 -*- |
3 # | 3 # |
4 # Copyright (c) 2012-2019 Jérôme Poisson | 4 # Copyright (c) 2012-2019 Jérôme Poisson |
5 # Copyright (c) 2013-2016 Adrien Cossa | 5 # Copyright (c) 2013-2016 Adrien Cossa |
6 # Copyright (c) 2003-2011 Ralph Meijer | 6 # Copyright (c) 2003-2011 Ralph Meijer |
62 """ | 62 """ |
63 | 63 |
64 import copy | 64 import copy |
65 import uuid | 65 import uuid |
66 | 66 |
67 from zope.interface import implements | 67 from zope.interface import implementer |
68 | 68 |
69 from twisted.application import service | 69 from twisted.application import service |
70 from twisted.python import components, log | 70 from twisted.python import components, log |
71 from twisted.internet import defer, reactor | 71 from twisted.internet import defer, reactor |
72 from twisted.words.protocols.jabber.error import StanzaError | 72 from twisted.words.protocols.jabber.error import StanzaError |
121 """ | 121 """ |
122 return container.ItemData(*[elementCopy(item_data.item)] | 122 return container.ItemData(*[elementCopy(item_data.item)] |
123 + [copy.deepcopy(d) for d in item_data[1:]]) | 123 + [copy.deepcopy(d) for d in item_data[1:]]) |
124 | 124 |
125 | 125 |
126 @implementer(iidavoll.IBackendService) | |
126 class BackendService(service.Service, utility.EventDispatcher): | 127 class BackendService(service.Service, utility.EventDispatcher): |
127 """ | 128 """ |
128 Generic publish-subscribe backend service. | 129 Generic publish-subscribe backend service. |
129 | 130 |
130 @cvar nodeOptions: Node configuration form as a mapping from the field | 131 @cvar nodeOptions: Node configuration form as a mapping from the field |
132 and possible options to choose from. | 133 and possible options to choose from. |
133 @type nodeOptions: C{dict}. | 134 @type nodeOptions: C{dict}. |
134 @cvar defaultConfig: The default node configuration. | 135 @cvar defaultConfig: The default node configuration. |
135 """ | 136 """ |
136 | 137 |
137 implements(iidavoll.IBackendService) | |
138 | 138 |
139 nodeOptions = { | 139 nodeOptions = { |
140 const.OPT_PERSIST_ITEMS: | 140 const.OPT_PERSIST_ITEMS: |
141 {"type": "boolean", | 141 {"type": "boolean", |
142 "label": "Persist items to storage"}, | 142 "label": "Persist items to storage"}, |
199 def __init__(self, storage, config): | 199 def __init__(self, storage, config): |
200 utility.EventDispatcher.__init__(self) | 200 utility.EventDispatcher.__init__(self) |
201 self.storage = storage | 201 self.storage = storage |
202 self._callbackList = [] | 202 self._callbackList = [] |
203 self.config = config | 203 self.config = config |
204 self.admins = config[u'admins_jids_list'] | 204 self.admins = config['admins_jids_list'] |
205 | 205 |
206 def isAdmin(self, entity_jid): | 206 def isAdmin(self, entity_jid): |
207 """Return True if an entity is an administrator""" | 207 """Return True if an entity is an administrator""" |
208 return entity_jid.userhostJID() in self.admins | 208 return entity_jid.userhostJID() in self.admins |
209 | 209 |
253 d.addCallback(self._makeMetaData) | 253 d.addCallback(self._makeMetaData) |
254 return d | 254 return d |
255 | 255 |
256 def _makeMetaData(self, metaData): | 256 def _makeMetaData(self, metaData): |
257 options = [] | 257 options = [] |
258 for key, value in metaData.iteritems(): | 258 for key, value in metaData.items(): |
259 if key in self.nodeOptions: | 259 if key in self.nodeOptions: |
260 option = {"var": key} | 260 option = {"var": key} |
261 option.update(self.nodeOptions[key]) | 261 option.update(self.nodeOptions[key]) |
262 option["value"] = value | 262 option["value"] = value |
263 options.append(option) | 263 options.append(option) |
322 @param item_elt (domish.Element): item to parse | 322 @param item_elt (domish.Element): item to parse |
323 @return (list): list of found categories | 323 @return (list): list of found categories |
324 """ | 324 """ |
325 categories = [] | 325 categories = [] |
326 try: | 326 try: |
327 entry_elt = item_elt.elements(const.NS_ATOM, "entry").next() | 327 entry_elt = next(item_elt.elements(const.NS_ATOM, "entry")) |
328 except StopIteration: | 328 except StopIteration: |
329 return categories | 329 return categories |
330 | 330 |
331 for category_elt in entry_elt.elements(const.NS_ATOM, 'category'): | 331 for category_elt in entry_elt.elements(const.NS_ATOM, 'category'): |
332 category = category_elt.getAttribute('term') | 332 category = category_elt.getAttribute('term') |
371 else: | 371 else: |
372 raise StanzaError('feature-not-implemented', text='unknown write restriction {}'.format(write_restriction)) | 372 raise StanzaError('feature-not-implemented', text='unknown write restriction {}'.format(write_restriction)) |
373 | 373 |
374 # we now remove every field which is not in data schema | 374 # we now remove every field which is not in data schema |
375 to_remove = set() | 375 to_remove = set() |
376 for item_var, item_field in item_form.fields.iteritems(): | 376 for item_var, item_field in item_form.fields.items(): |
377 if item_var not in schema_form.fields: | 377 if item_var not in schema_form.fields: |
378 to_remove.add(item_field) | 378 to_remove.add(item_field) |
379 | 379 |
380 for field in to_remove: | 380 for field in to_remove: |
381 item_form.removeField(field) | 381 item_form.removeField(field) |
385 """Check that publisher can overwrite items | 385 """Check that publisher can overwrite items |
386 | 386 |
387 current publisher must correspond to each item publisher | 387 current publisher must correspond to each item publisher |
388 """ | 388 """ |
389 def doCheck(item_pub_map): | 389 def doCheck(item_pub_map): |
390 for item_publisher in item_pub_map.itervalues(): | 390 for item_publisher in item_pub_map.values(): |
391 if item_publisher.userhost() != publisher.userhost(): | 391 if item_publisher.userhost() != publisher.userhost(): |
392 raise error.ItemForbidden() | 392 raise error.ItemForbidden() |
393 | 393 |
394 d = node.getItemsPublishers(itemIdentifiers) | 394 d = node.getItemsPublishers(itemIdentifiers) |
395 d.addCallback(doCheck) | 395 d.addCallback(doCheck) |
429 item.defaultUri = None | 429 item.defaultUri = None |
430 if not item.getAttribute("id"): | 430 if not item.getAttribute("id"): |
431 item["id"] = yield node.getNextId() | 431 item["id"] = yield node.getNextId() |
432 new_item = True | 432 new_item = True |
433 if ret_payload is None: | 433 if ret_payload is None: |
434 ret_pubsub_elt = domish.Element((pubsub.NS_PUBSUB, u'pubsub')) | 434 ret_pubsub_elt = domish.Element((pubsub.NS_PUBSUB, 'pubsub')) |
435 ret_publish_elt = ret_pubsub_elt.addElement(u'publish') | 435 ret_publish_elt = ret_pubsub_elt.addElement('publish') |
436 ret_publish_elt[u'node'] = node.nodeIdentifier | 436 ret_publish_elt['node'] = node.nodeIdentifier |
437 ret_payload = ret_pubsub_elt | 437 ret_payload = ret_pubsub_elt |
438 ret_publish_elt = ret_payload.publish | 438 ret_publish_elt = ret_payload.publish |
439 ret_item_elt = ret_publish_elt.addElement(u'item') | 439 ret_item_elt = ret_publish_elt.addElement('item') |
440 ret_item_elt["id"] = item[u"id"] | 440 ret_item_elt["id"] = item["id"] |
441 else: | 441 else: |
442 check_overwrite = True | 442 check_overwrite = True |
443 new_item = False | 443 new_item = False |
444 access_model, item_config = self.parseItemConfig(item) | 444 access_model, item_config = self.parseItemConfig(item) |
445 categories = self.parseCategories(item) | 445 categories = self.parseCategories(item) |
460 publishers = set(pub_map.values()) | 460 publishers = set(pub_map.values()) |
461 if len(publishers) != 1: | 461 if len(publishers) != 1: |
462 # TODO: handle multiple items publishing (from several | 462 # TODO: handle multiple items publishing (from several |
463 # publishers) | 463 # publishers) |
464 raise error.NoPublishing( | 464 raise error.NoPublishing( |
465 u"consistent_publisher is currently only possible when " | 465 "consistent_publisher is currently only possible when " |
466 u"publishing items from a single publisher. Try to " | 466 "publishing items from a single publisher. Try to " |
467 u"publish one item at a time") | 467 "publish one item at a time") |
468 # we replace requestor and new payload's publisher by original | 468 # we replace requestor and new payload's publisher by original |
469 # item publisher to keep publisher consistent | 469 # item publisher to keep publisher consistent |
470 requestor = publishers.pop() | 470 requestor = publishers.pop() |
471 for item in items: | 471 for item in items: |
472 item['publisher'] = requestor.full() | 472 item['publisher'] = requestor.full() |
504 set()) | 504 set()) |
505 subs.add(subscription) | 505 subs.add(subscription) |
506 | 506 |
507 notifications = [(subscriber, subscriptions_, items_data) | 507 notifications = [(subscriber, subscriptions_, items_data) |
508 for subscriber, subscriptions_ | 508 for subscriber, subscriptions_ |
509 in subsBySubscriber.iteritems()] | 509 in subsBySubscriber.items()] |
510 | 510 |
511 return notifications | 511 return notifications |
512 | 512 |
513 def rootNotFound(failure): | 513 def rootNotFound(failure): |
514 failure.trap(error.NodeNotFound) | 514 failure.trap(error.NodeNotFound) |
760 if requestor_bare in affiliations and affiliations[requestor_bare] != 'owner': | 760 if requestor_bare in affiliations and affiliations[requestor_bare] != 'owner': |
761 # FIXME: it may be interesting to allow the owner to ask for ownership removal | 761 # FIXME: it may be interesting to allow the owner to ask for ownership removal |
762 # if at least one other entity is owner for this node | 762 # if at least one other entity is owner for this node |
763 raise error.Forbidden("You can't change your own affiliation") | 763 raise error.Forbidden("You can't change your own affiliation") |
764 | 764 |
765 to_delete = [jid_ for jid_, affiliation in affiliations.iteritems() if affiliation == 'none'] | 765 to_delete = [jid_ for jid_, affiliation in affiliations.items() if affiliation == 'none'] |
766 for jid_ in to_delete: | 766 for jid_ in to_delete: |
767 del affiliations[jid_] | 767 del affiliations[jid_] |
768 | 768 |
769 if to_delete: | 769 if to_delete: |
770 d = node.deleteAffiliations(to_delete) | 770 d = node.deleteAffiliations(to_delete) |
958 # FIXME: for node, access should be renamed owner-roster, not publisher | 958 # FIXME: for node, access should be renamed owner-roster, not publisher |
959 yield self.checkRosterGroups(node, requestor) | 959 yield self.checkRosterGroups(node, requestor) |
960 elif access_model == const.VAL_AMODEL_WHITELIST: | 960 elif access_model == const.VAL_AMODEL_WHITELIST: |
961 yield self.checkNodeAffiliations(node, requestor) | 961 yield self.checkNodeAffiliations(node, requestor) |
962 else: | 962 else: |
963 raise Exception(u"Unknown access_model") | 963 raise Exception("Unknown access_model") |
964 | 964 |
965 defer.returnValue((affiliation, owner, roster, access_model)) | 965 defer.returnValue((affiliation, owner, roster, access_model)) |
966 | 966 |
967 @defer.inlineCallbacks | 967 @defer.inlineCallbacks |
968 def getItemsIds(self, nodeIdentifier, requestor, authorized_groups, unrestricted, maxItems=None, ext_data=None, pep=False, recipient=None): | 968 def getItemsIds(self, nodeIdentifier, requestor, authorized_groups, unrestricted, maxItems=None, ext_data=None, pep=False, recipient=None): |
1152 We check that requestor is publisher of all the items he wants to retract | 1152 We check that requestor is publisher of all the items he wants to retract |
1153 and raise error.Forbidden if it is not the case | 1153 and raise error.Forbidden if it is not the case |
1154 """ | 1154 """ |
1155 # TODO: the behaviour should be configurable (per node ?) | 1155 # TODO: the behaviour should be configurable (per node ?) |
1156 if (any((requestor.userhostJID() != publisher.userhostJID() | 1156 if (any((requestor.userhostJID() != publisher.userhostJID() |
1157 for publisher in publishers_map.itervalues())) | 1157 for publisher in publishers_map.values())) |
1158 and not self.isAdmin(requestor) | 1158 and not self.isAdmin(requestor) |
1159 ): | 1159 ): |
1160 raise error.Forbidden() | 1160 raise error.Forbidden() |
1161 | 1161 |
1162 if affiliation in ['owner', 'publisher']: | 1162 if affiliation in ['owner', 'publisher']: |
1276 "subscribe", | 1276 "subscribe", |
1277 ] | 1277 ] |
1278 | 1278 |
1279 discoIdentity = disco.DiscoIdentity('pubsub', | 1279 discoIdentity = disco.DiscoIdentity('pubsub', |
1280 'service', | 1280 'service', |
1281 u'Salut à Toi pubsub service') | 1281 'Salut à Toi pubsub service') |
1282 | 1282 |
1283 pubsubService = None | 1283 pubsubService = None |
1284 | 1284 |
1285 _errorMap = { | 1285 _errorMap = { |
1286 error.NodeNotFound: ('item-not-found', None, None), | 1286 error.NodeNotFound: ('item-not-found', None, None), |
1521 subscribers, | 1521 subscribers, |
1522 redirectURI)) | 1522 redirectURI)) |
1523 return d | 1523 return d |
1524 | 1524 |
1525 def _mapErrors(self, failure): | 1525 def _mapErrors(self, failure): |
1526 e = failure.trap(*self._errorMap.keys()) | 1526 e = failure.trap(*list(self._errorMap.keys())) |
1527 | 1527 |
1528 condition, pubsubCondition, feature = self._errorMap[e] | 1528 condition, pubsubCondition, feature = self._errorMap[e] |
1529 msg = failure.value.msg | 1529 msg = failure.value.msg |
1530 | 1530 |
1531 if pubsubCondition: | 1531 if pubsubCondition: |
1599 def getConfigurationOptions(self): | 1599 def getConfigurationOptions(self): |
1600 return self.backend.nodeOptions | 1600 return self.backend.nodeOptions |
1601 | 1601 |
1602 def _publish_errb(self, failure, request): | 1602 def _publish_errb(self, failure, request): |
1603 if failure.type == error.NodeNotFound and self.backend.supportsAutoCreate(): | 1603 if failure.type == error.NodeNotFound and self.backend.supportsAutoCreate(): |
1604 print "Auto-creating node %s" % (request.nodeIdentifier,) | 1604 print("Auto-creating node %s" % (request.nodeIdentifier,)) |
1605 d = self.backend.createNode(request.nodeIdentifier, | 1605 d = self.backend.createNode(request.nodeIdentifier, |
1606 request.sender, | 1606 request.sender, |
1607 pep=self._isPep(request), | 1607 pep=self._isPep(request), |
1608 recipient=request.recipient) | 1608 recipient=request.recipient) |
1609 d.addCallback(lambda ignore, | 1609 d.addCallback(lambda ignore, |
1778 iidavoll.IBackendService, | 1778 iidavoll.IBackendService, |
1779 iwokkel.IPubSubResource) | 1779 iwokkel.IPubSubResource) |
1780 | 1780 |
1781 | 1781 |
1782 | 1782 |
1783 @implementer(iwokkel.IDisco) | |
1783 class ExtraDiscoHandler(XMPPHandler): | 1784 class ExtraDiscoHandler(XMPPHandler): |
1784 implements(iwokkel.IDisco) | |
1785 # see comment in twisted/plugins/pubsub.py | 1785 # see comment in twisted/plugins/pubsub.py |
1786 # FIXME: upstream must be fixed so we can use custom (non pubsub#) disco features | 1786 # FIXME: upstream must be fixed so we can use custom (non pubsub#) disco features |
1787 | 1787 |
1788 def getDiscoInfo(self, requestor, service, nodeIdentifier=''): | 1788 def getDiscoInfo(self, requestor, service, nodeIdentifier=''): |
1789 return [disco.DiscoFeature(pubsub.NS_ORDER_BY)] | 1789 return [disco.DiscoFeature(pubsub.NS_ORDER_BY)] |