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)]