# HG changeset patch # User Ralph Meijer # Date 1191415303 0 # Node ID ef22e4150caa0fe2d36a900717bfc0fff797d7f6 # Parent 5abb017bd6876068e8a4d412feb5591ba0d10249 Move protocol implementations (pubsub, disco, forms) to and depend on wokkel. Author: ralphm Fixes: #4 diff -r 5abb017bd687 -r ef22e4150caa INSTALL --- a/INSTALL Thu Jan 18 14:08:32 2007 +0000 +++ b/INSTALL Wed Oct 03 12:41:43 2007 +0000 @@ -5,6 +5,7 @@ - Twisted Words >= 0.4.0 - uuid.py (http://ofxsuite.berlios.de/uuid.py) - A jabber server that supports the component protocol (JEP-0114) +- Wokkel (http://wokkel.ik.nu/) For the PostgreSQL backend, the following is also required: diff -r 5abb017bd687 -r ef22e4150caa LICENSE --- a/LICENSE Thu Jan 18 14:08:32 2007 +0000 +++ b/LICENSE Wed Oct 03 12:41:43 2007 +0000 @@ -1,4 +1,4 @@ -Copyright (c) 2003-2006 Ralph Meijer +Copyright (c) 2003-2007 Ralph Meijer Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the diff -r 5abb017bd687 -r ef22e4150caa README --- a/README Thu Jan 18 14:08:32 2007 +0000 +++ b/README Wed Oct 03 12:41:43 2007 +0000 @@ -14,7 +14,7 @@ There are two different backends: one using PostgreSQL for storage, and one just keeping everything in memory. -In Idavoll the mimimal requirements of JEP-0060 version 1.8 are implemented +In Idavoll the mimimal requirements of JEP-0060 version 1.9 are implemented plus most optional features, as returned by Service Discovery. Installing @@ -26,7 +26,7 @@ Copyright ========= -All of the code in this distribution is Copyright (c) 2003-2006 Ralph Meijer. +All of the code in this distribution is Copyright (c) 2003-2007 Ralph Meijer. Idavoll is made available under the MIT license. The included LICENSE file describes this in detail. diff -r 5abb017bd687 -r ef22e4150caa idavoll/__init__.py --- a/idavoll/__init__.py Thu Jan 18 14:08:32 2007 +0000 +++ b/idavoll/__init__.py Wed Oct 03 12:41:43 2007 +0000 @@ -1,3 +1,6 @@ -# Copyright (c) 2003-2006 Ralph Meijer +# Copyright (c) 2003-2007 Ralph Meijer # See LICENSE for details. +""" +Idavoll, a generic XMPP publish-subscribe service. +""" diff -r 5abb017bd687 -r ef22e4150caa idavoll/backend.py --- a/idavoll/backend.py Thu Jan 18 14:08:32 2007 +0000 +++ b/idavoll/backend.py Wed Oct 03 12:41:43 2007 +0000 @@ -1,214 +1,524 @@ -# Copyright (c) 2003-2006 Ralph Meijer +# -*- test-case-name: idavoll.test.test_backend -*- +# +# Copyright (c) 2003-2007 Ralph Meijer # See LICENSE for details. -from zope.interface import Interface -import storage +import uuid + +from zope.interface import implements -class Error(Exception): - msg = '' +from twisted.application import service +from twisted.python import components +from twisted.internet import defer +from twisted.words.protocols.jabber.error import StanzaError +from twisted.words.xish import utility - def __str__(self): - return self.msg - -class Forbidden(Error): - pass +from wokkel.iwokkel import IDisco, IPubSubService +from wokkel.pubsub import PubSubService, PubSubError -class ItemForbidden(Error): - pass +from idavoll import error, iidavoll +from idavoll.iidavoll import IBackendService -class ItemRequired(Error): - pass - -class NoInstantNodes(Error): - pass +def _get_affiliation(node, entity): + d = node.get_affiliation(entity) + d.addCallback(lambda affiliation: (node, affiliation)) + return d -class NotSubscribed(Error): - pass +class BackendService(service.Service, utility.EventDispatcher): -class InvalidConfigurationOption(Error): - msg = 'Invalid configuration option' - -class InvalidConfigurationValue(Error): - msg = 'Bad configuration value' + implements(iidavoll.IBackendService) -class NodeNotPersistent(Error): - pass - -class NoRootNode(Error): - pass + options = {"pubsub#persist_items": + {"type": "boolean", + "label": "Persist items to storage"}, + "pubsub#deliver_payloads": + {"type": "boolean", + "label": "Deliver payloads with event notifications"}, + } -class IBackendService(Interface): - """ Interface to a backend service of a pubsub service. """ + default_config = {"pubsub#persist_items": True, + "pubsub#deliver_payloads": True, + } - def __init__(storage): - """ - @param storage: L{storage} object. - """ + def __init__(self, storage): + utility.EventDispatcher.__init__(self) + self.storage = storage + self._callback_list = [] def supports_publisher_affiliation(self): - """ Reports if the backend supports the publisher affiliation. - - @rtype: C{bool} - """ + return True def supports_outcast_affiliation(self): - """ Reports if the backend supports the publisher affiliation. - - @rtype: C{bool} - """ + return True def supports_persistent_items(self): - """ Reports if the backend supports persistent items. - - @rtype: C{bool} - """ + return True - def get_node_type(node_id): - """ Return type of a node. - - @return: a deferred that returns either 'leaf' or 'collection' - """ + def get_node_type(self, node_id): + d = self.storage.get_node(node_id) + d.addCallback(lambda node: node.get_type()) + return d def get_nodes(self): - """ Returns list of all nodes. + return self.storage.get_node_ids() + + def get_node_meta_data(self, node_id): + d = self.storage.get_node(node_id) + d.addCallback(lambda node: node.get_meta_data()) + d.addCallback(self._make_meta_data) + return d - @return: a deferred that returns a C{list} of node ids. - """ + def _make_meta_data(self, meta_data): + options = [] + for key, value in meta_data.iteritems(): + if self.options.has_key(key): + option = {"var": key} + option.update(self.options[key]) + option["value"] = value + options.append(option) + + return options + + def _check_auth(self, node, requestor): + def check(affiliation, node): + if affiliation not in ['owner', 'publisher']: + raise error.Forbidden() + return node - def get_node_meta_data(node_id): - """ Return meta data for a node. + d = node.get_affiliation(requestor) + d.addCallback(check, node) + return d + + def publish(self, node_id, items, requestor): + d = self.storage.get_node(node_id) + d.addCallback(self._check_auth, requestor) + d.addCallback(self._do_publish, items, requestor) + return d + + def _do_publish(self, node, items, requestor): + configuration = node.get_configuration() + persist_items = configuration["pubsub#persist_items"] + deliver_payloads = configuration["pubsub#deliver_payloads"] - @return: a deferred that returns a C{list} of C{dict}s with the - metadata. - """ + if items and not persist_items and not deliver_payloads: + raise error.ItemForbidden() + elif not items and (persist_items or deliver_payloads): + raise error.ItemRequired() + + if persist_items or deliver_payloads: + for item in items: + if not item.getAttribute("id"): + item["id"] = uuid.generate() + + if persist_items: + d = node.store_items(items, requestor) + else: + d = defer.succeed(None) -class INodeCreationService(Interface): - """ A service for creating nodes """ + d.addCallback(self._do_notify, node.id, items, deliver_payloads) + return d + + def _do_notify(self, result, node_id, items, deliver_payloads): + if items and not deliver_payloads: + for item in items: + item.children = [] + + self.dispatch({'items': items, 'node_id': node_id}, + '//event/pubsub/notify') + + def get_notification_list(self, node_id, items): + d = self.storage.get_node(node_id) + d.addCallback(lambda node: node.get_subscribers()) + d.addCallback(self._magic_filter, node_id, items) + return d + + def _magic_filter(self, subscribers, node_id, items): + list = [] + for subscriber in subscribers: + list.append((subscriber, items)) + return list + + def register_notifier(self, observerfn, *args, **kwargs): + self.addObserver('//event/pubsub/notify', observerfn, *args, **kwargs) - def create_node(node_id, requestor): - """ Create a node. - - @return: a deferred that fires when the node has been created. - """ + def subscribe(self, node_id, subscriber, requestor): + subscriber_entity = subscriber.userhostJID() + if subscriber_entity != requestor: + return defer.fail(error.Forbidden()) + + d = self.storage.get_node(node_id) + d.addCallback(_get_affiliation, subscriber_entity) + d.addCallback(self._do_subscribe, subscriber) + return d + + def _do_subscribe(self, result, subscriber): + node, affiliation = result -class INodeDeletionService(Interface): - """ A service for deleting nodes. """ + if affiliation == 'outcast': + raise error.Forbidden() + + d = node.add_subscription(subscriber, 'subscribed') + d.addCallback(lambda _: 'subscribed') + d.addErrback(self._get_subscription, node, subscriber) + d.addCallback(self._return_subscription, node.id) + return d + + def _get_subscription(self, failure, node, subscriber): + failure.trap(error.SubscriptionExists) + return node.get_subscription(subscriber) + + def _return_subscription(self, result, node_id): + return node_id, result - def register_pre_delete(pre_delete_fn): - """ Register a callback that is called just before a node deletion. - - The function C{pre_deleted_fn} is added to a list of functions - to be called just before deletion of a node. The callback - C{pre_delete_fn} is called with the C{node_id} that is about to be - deleted and should return a deferred that returns a list of deferreds - that are to be fired after deletion. The backend collects the lists - from all these callbacks before actually deleting the node in question. - After deletion all collected deferreds are fired to do post-processing. + def unsubscribe(self, node_id, subscriber, requestor): + if subscriber.userhostJID() != requestor: + return defer.fail(error.Forbidden()) + + d = self.storage.get_node(node_id) + d.addCallback(lambda node: node.remove_subscription(subscriber)) + return d + + def get_subscriptions(self, entity): + return self.storage.get_subscriptions(entity) + + def supports_instant_nodes(self): + return True + + def create_node(self, node_id, requestor): + if not node_id: + node_id = 'generic/%s' % uuid.generate() + d = self.storage.create_node(node_id, requestor) + d.addCallback(lambda _: node_id) + return d - The idea is that you want to be able to collect data from the - node before deleting it, for example to get a list of subscribers - that have to be notified after the node has been deleted. To do this, - C{pre_delete_fn} fetches the subscriber list and passes this - list to a callback attached to a deferred that it sets up. This - deferred is returned in the list of deferreds. - """ + def get_default_configuration(self): + d = defer.succeed(self.default_config) + d.addCallback(self._make_config) + return d + + def get_node_configuration(self, node_id): + if not node_id: + raise error.NoRootNode() + + d = self.storage.get_node(node_id) + d.addCallback(lambda node: node.get_configuration()) + + d.addCallback(self._make_config) + return d + + def _make_config(self, config): + options = [] + for key, value in self.options.iteritems(): + option = {"var": key} + option.update(value) + if config.has_key(key): + option["value"] = config[key] + options.append(option) + + return options + + def set_node_configuration(self, node_id, options, requestor): + if not node_id: + raise error.NoRootNode() - def get_subscribers(node_id): - """ Get node subscriber list. - - @return: a deferred that fires with the list of subscribers. - """ + for key, value in options.iteritems(): + if not self.options.has_key(key): + raise error.InvalidConfigurationOption() + if self.options[key]["type"] == 'boolean': + try: + options[key] = bool(int(value)) + except ValueError: + raise error.InvalidConfigurationValue() + + d = self.storage.get_node(node_id) + d.addCallback(_get_affiliation, requestor) + d.addCallback(self._do_set_node_configuration, options) + return d - def delete_node(node_id, requestor): - """ Delete a node. - - @return: a deferred that fires when the node has been deleted. - """ + def _do_set_node_configuration(self, result, options): + node, affiliation = result + + if affiliation != 'owner': + raise error.Forbidden() + + return node.set_configuration(options) + + def get_affiliations(self, entity): + return self.storage.get_affiliations(entity) + + def get_items(self, node_id, requestor, max_items=None, item_ids=[]): + d = self.storage.get_node(node_id) + d.addCallback(_get_affiliation, requestor) + d.addCallback(self._do_get_items, max_items, item_ids) + return d -class IPublishService(Interface): - """ A service for publishing items to a node. """ + def _do_get_items(self, result, max_items, item_ids): + node, affiliation = result + + if affiliation == 'outcast': + raise error.Forbidden() + + if item_ids: + return node.get_items_by_id(item_ids) + else: + return node.get_items(max_items) + + def retract_item(self, node_id, item_ids, requestor): + d = self.storage.get_node(node_id) + d.addCallback(_get_affiliation, requestor) + d.addCallback(self._do_retract, item_ids) + return d - def publish(node_id, items, requestor): - """ Publish items to a pubsub node. - - @return: a deferred that fires when the items have been published. - """ -class INotificationService(Interface): - """ A service for notification of published items. """ + def _do_retract(self, result, item_ids): + node, affiliation = result + persist_items = node.get_configuration()["pubsub#persist_items"] + + if affiliation not in ['owner', 'publisher']: + raise error.Forbidden() - def register_notifier(observerfn, *args, **kwargs): - """ Register callback which is called for notification. """ + if not persist_items: + raise error.NodeNotPersistent() + + d = node.remove_items(item_ids) + d.addCallback(self._do_notify_retraction, node.id) + return d + + def _do_notify_retraction(self, item_ids, node_id): + self.dispatch({ 'item_ids': item_ids, 'node_id': node_id }, + '//event/pubsub/retract') - def get_notification_list(node_id, items): - pass + def purge_node(self, node_id, requestor): + d = self.storage.get_node(node_id) + d.addCallback(_get_affiliation, requestor) + d.addCallback(self._do_purge) + return d + + def _do_purge(self, result): + node, affiliation = result + persist_items = node.get_configuration()["pubsub#persist_items"] -class ISubscriptionService(Interface): - """ A service for managing subscriptions. """ + if affiliation != 'owner': + raise error.Forbidden() + + if not persist_items: + raise error.NodeNotPersistent() + + d = node.purge() + d.addCallback(self._do_notify_purge, node.id) + return d + + def _do_notify_purge(self, result, node_id): + self.dispatch(node_id, '//event/pubsub/purge') - def subscribe(node_id, subscriber, requestor): - """ Request the subscription of an entity to a pubsub node. + def register_pre_delete(self, pre_delete_fn): + self._callback_list.append(pre_delete_fn) + + def get_subscribers(self, node_id): + d = self.storage.get_node(node_id) + d.addCallback(lambda node: node.get_subscribers()) + return d - Depending on the node's configuration and possible business rules, the - C{subscriber} is added to the list of subscriptions of the node with id - C{node_id}. The C{subscriber} might be different from the C{requestor}, - and if the C{requestor} is not allowed to subscribe this entity an - exception should be raised. + def delete_node(self, node_id, requestor): + d = self.storage.get_node(node_id) + d.addCallback(_get_affiliation, requestor) + d.addCallback(self._do_pre_delete) + return d + + def _do_pre_delete(self, result): + node, affiliation = result + + if affiliation != 'owner': + raise error.Forbidden() + + d = defer.DeferredList([cb(node.id) for cb in self._callback_list], + consumeErrors=1) + d.addCallback(self._do_delete, node.id) - @return: a deferred that returns the subscription state - """ + def _do_delete(self, result, node_id): + dl = [] + for succeeded, r in result: + if succeeded and r: + dl.extend(r) + + d = self.storage.delete_node(node_id) + d.addCallback(self._do_notify_delete, dl) + + return d - def unsubscribe(node_id, subscriber, requestor): - """ Cancel the subscription of an entity to a pubsub node. + def _do_notify_delete(self, result, dl): + for d in dl: + d.callback(None) + + +class PubSubServiceFromBackend(PubSubService): + """ + Adapts a backend to an xmpp publish-subscribe service. + """ + + implements(IDisco) - The subscription of C{subscriber} is removed from the list of - subscriptions of the node with id C{node_id}. If the C{requestor} - is not allowed to unsubscribe C{subscriber}, an an exception should - be raised. + _errorMap = { + error.NodeNotFound: ('item-not-found', None, None), + error.NodeExists: ('conflict', None, None), + error.SubscriptionNotFound: ('not-authorized', + 'not-subscribed', + None), + error.Forbidden: ('forbidden', None, None), + error.ItemForbidden: ('bad-request', 'item-forbidden', None), + error.ItemRequired: ('bad-request', 'item-required', None), + error.NoInstantNodes: ('not-acceptable', + 'unsupported', + 'instant-nodes'), + error.NotSubscribed: ('not-authorized', 'not-subscribed', None), + error.InvalidConfigurationOption: ('not-acceptable', None, None), + error.InvalidConfigurationValue: ('not-acceptable', None, None), + error.NodeNotPersistent: ('feature-not-implemented', + 'unsupported', + 'persistent-node'), + error.NoRootNode: ('bad-request', None, None), + } - @return: a deferred that fires when unsubscription is complete. - """ + def __init__(self, backend): + PubSubService.__init__(self) - def get_subscriptions(entity): - """ Report the list of current subscriptions with this pubsub service. + self.backend = backend + self.hideNodes = False - Report the list of the current subscriptions with all nodes within this - pubsub service, for the C{entity}. + self.pubSubFeatures = self._getPubSubFeatures() + + self.backend.register_notifier(self._notify) - @return: a deferred that returns the list of all current subscriptions - as tuples C{(node_id, subscriber, subscription)}. - """ + def _getPubSubFeatures(self): + features = [ + "config-node", + "create-nodes", + "delete-any", + "delete-nodes", + "item-ids", + "meta-data", + "publish", + "purge-nodes", + "retract-items", + "retrieve-affiliations", + "retrieve-default", + "retrieve-items", + "retrieve-subscriptions", + "subscribe", + ] -class IAffiliationsService(Interface): - """ A service for retrieving the affiliations with this pubsub service. """ + if self.backend.supports_instant_nodes(): + features.append("instant-nodes") + + if self.backend.supports_outcast_affiliation(): + features.append("outcast-affiliation") + + if self.backend.supports_persistent_items(): + features.append("persistent-items") + + if self.backend.supports_publisher_affiliation(): + features.append("publisher-affiliation") + + return features - def get_affiliations(entity): - """ Report the list of current affiliations with this pubsub service. + def _notify(self, data): + items = data['items'] + nodeIdentifier = data['node_id'] + d = self.backend.get_notification_list(nodeIdentifier, items) + d.addCallback(lambda notifications: self.notifyPublish(self.serviceJID, + nodeIdentifier, + notifications)) + + def _mapErrors(self, failure): + e = failure.trap(*self._errorMap.keys()) + + condition, pubsubCondition, feature = self._errorMap[e] + msg = failure.value.msg - Report the list of the current affiliations with all nodes within this - pubsub service, for the C{entity}. + if pubsubCondition: + exc = PubSubError(condition, pubsubCondition, feature, msg) + else: + exc = StanzaError(condition, text=msg) + + raise exc - @return: a deferred that returns the list of all current affiliations - as tuples C{(node_id, affiliation)}. - """ + def getNodeInfo(self, requestor, service, nodeIdentifier): + info = {} + + def saveType(result): + info['type'] = result + return nodeIdentifier + + def saveMetaData(result): + info['meta-data'] = result + return info -class IRetractionService(Interface): - """ A service for retracting published items """ + d = defer.succeed(nodeIdentifier) + d.addCallback(self.backend.get_node_type) + d.addCallback(saveType) + d.addCallback(self.backend.get_node_meta_data) + d.addCallback(saveMetaData) + d.errback(self._mapErrors) + return d + + def getNodes(self, requestor, service): + d = self.backend.get_nodes() + return d.addErrback(self._mapErrors) + + def publish(self, requestor, service, nodeIdentifier, items): + d = self.backend.publish(nodeIdentifier, items, requestor) + return d.addErrback(self._mapErrors) - def retract_item(node_id, item_id, requestor): - """ Removes item in node from persistent storage """ + def subscribe(self, requestor, service, nodeIdentifier, subscriber): + d = self.backend.subscribe(nodeIdentifier, subscriber, requestor) + return d.addErrback(self._mapErrors) + + def unsubscribe(self, requestor, service, nodeIdentifier, subscriber): + d = self.backend.unsubscribe(nodeIdentifier, subscriber, requestor) + return d.addErrback(self._mapErrors) - def purge_node(node_id, requestor): - """ Removes all items in node from persistent storage """ + def subscriptions(self, requestor, service): + d = self.backend.get_subscriptions(requestor) + return d.addErrback(self._mapErrors) + + def affiliations(self, requestor, service): + d = self.backend.get_affiliations(requestor) + return d.addErrback(self._mapErrors) -class IItemRetrievalService(Interface): - """ A service for retrieving previously published items. """ + def create(self, requestor, service, nodeIdentifier): + d = self.backend.create_node(nodeIdentifier, requestor) + return d.addErrback(self._mapErrors) + + def getDefaultConfiguration(self, requestor, service): + d = self.backend.get_default_configuration() + return d.addErrback(self._mapErrors) - def get_items(node_id, requestor, max_items=None, item_ids=[]): - """ Retrieve items from persistent storage + def getConfiguration(self, requestor, service, nodeIdentifier): + d = self.backend.get_node_configuration(nodeIdentifier) + return d.addErrback(self._mapErrors) + + def setConfiguration(self, requestor, service, nodeIdentifier, options): + d = self.backend.set_node_configuration(nodeIdentifier, options, + requestor) + return d.addErrback(self._mapErrors) - If C{max_items} is given, return the C{max_items} last published - items, else if C{item_ids} is not empty, return the items requested. - If neither is given, return all items. + def items(self, requestor, service, nodeIdentifier, maxItems, itemIdentifiers): + d = self.backend.get_items(nodeIdentifier, requestor, maxItems, + itemIdentifiers) + return d.addErrback(self._mapErrors) + + def retract(self, requestor, service, nodeIdentifier, itemIdentifiers): + d = self.backend.retract_item(nodeIdentifier, itemIdentifiers, + requestor) + return d.addErrback(self._mapErrors) - @return: a deferred that returns the requested items - """ + def purge(self, requestor, service, nodeIdentifier): + d = self.backend.purge_node(nodeIdentifier, requestor) + return d.addErrback(self._mapErrors) + + def delete(self, requestor, service, nodeIdentifier): + d = self.backend.delete_node(nodeIdentifier, requestor) + return d.addErrback(self._mapErrors) + +components.registerAdapter(PubSubServiceFromBackend, + IBackendService, + IPubSubService) diff -r 5abb017bd687 -r ef22e4150caa idavoll/data_form.py --- a/idavoll/data_form.py Thu Jan 18 14:08:32 2007 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,55 +0,0 @@ -# Copyright (c) 2003-2006 Ralph Meijer -# See LICENSE for details. - -from twisted.words.xish import domish - -NS_X_DATA = 'jabber:x:data' - -class Field(domish.Element): - def __init__(self, type='text-single', var=None, label=None, - value=None, values=[], options={}): - domish.Element.__init__(self, (NS_X_DATA, 'field')) - self['type'] = type - if var is not None: - self['var'] = var - if label is not None: - self['label'] = label - if value is not None: - self.set_value(value) - else: - self.set_values(values) - if type in ['list-single', 'list-multi']: - for value, label in options.iteritems(): - self.addChild(Option(value, label)) - - def set_value(self, value): - if self['type'] == 'boolean': - value = str(int(bool(value))) - else: - value = str(value) - - value_element = self.value or self.addElement('value') - value_element.children = [] - value_element.addContent(value) - - def set_values(self, values): - for value in values: - value = str(value) - self.addElement('value', content=value) - -class Option(domish.Element): - def __init__(self, value, label=None): - domish.Element.__init__(self, (NS_X_DATA, 'option')) - if label is not None: - self['label'] = label - self.addElement('value', content=value) - -class Form(domish.Element): - def __init__(self, type, form_type): - domish.Element.__init__(self, (NS_X_DATA, 'x'), - attribs={'type': type}) - self.add_field(type='hidden', var='FORM_TYPE', values=[form_type]) - - def add_field(self, type='text-single', var=None, label=None, - value=None, values=[], options={}): - self.addChild(Field(type, var, label, value, values, options)) diff -r 5abb017bd687 -r ef22e4150caa idavoll/disco.py --- a/idavoll/disco.py Thu Jan 18 14:08:32 2007 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,31 +0,0 @@ -# Copyright (c) 2003-2006 Ralph Meijer -# See LICENSE for details. - -from twisted.words.xish import domish - -NS = 'http://jabber.org/protocol/disco' -NS_INFO = NS + '#info' -NS_ITEMS = NS + '#items' - -class Feature(domish.Element): - def __init__(self, feature): - domish.Element.__init__(self, (NS_INFO, 'feature'), - attribs={'var': feature}) -class Identity(domish.Element): - def __init__(self, category, type, name = None): - domish.Element.__init__(self, (NS_INFO, 'identity'), - attribs={'category': category, - 'type': type}) - if name: - self['name'] = name - -class Item(domish.Element): - def __init__(self, jid, node = None, name = None): - domish.Element.__init__(self, (NS_ITEMS, 'item'), - attribs={'jid': jid}) - if node: - self['node'] = node - - if name: - self['name'] = name - diff -r 5abb017bd687 -r ef22e4150caa idavoll/error.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/idavoll/error.py Wed Oct 03 12:41:43 2007 +0000 @@ -0,0 +1,47 @@ +# Copyright (c) 2003-2007 Ralph Meijer +# See LICENSE for details. + +class Error(Exception): + msg = '' + + def __str__(self): + return self.msg + +class NodeNotFound(Error): + pass + +class NodeExists(Error): + pass + +class SubscriptionNotFound(Error): + pass + +class SubscriptionExists(Error): + pass + +class Forbidden(Error): + pass + +class ItemForbidden(Error): + pass + +class ItemRequired(Error): + pass + +class NoInstantNodes(Error): + pass + +class NotSubscribed(Error): + pass + +class InvalidConfigurationOption(Error): + msg = 'Invalid configuration option' + +class InvalidConfigurationValue(Error): + msg = 'Bad configuration value' + +class NodeNotPersistent(Error): + pass + +class NoRootNode(Error): + pass diff -r 5abb017bd687 -r ef22e4150caa idavoll/generic_backend.py --- a/idavoll/generic_backend.py Thu Jan 18 14:08:32 2007 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,378 +0,0 @@ -# Copyright (c) 2003-2006 Ralph Meijer -# See LICENSE for details. - -import sha -import time -import uuid -from twisted.words.protocols.jabber import jid -from twisted.words.xish import utility -from twisted.application import service -from twisted.internet import defer -from zope.interface import implements -import backend, storage - -def _get_affiliation(node, entity): - d = node.get_affiliation(entity) - d.addCallback(lambda affiliation: (node, affiliation)) - return d - -class BackendService(service.MultiService, utility.EventDispatcher): - - implements(backend.IBackendService) - - options = {"pubsub#persist_items": - {"type": "boolean", - "label": "Persist items to storage"}, - "pubsub#deliver_payloads": - {"type": "boolean", - "label": "Deliver payloads with event notifications"}, - } - - default_config = {"pubsub#persist_items": True, - "pubsub#deliver_payloads": True, - } - - def __init__(self, storage): - service.MultiService.__init__(self) - utility.EventDispatcher.__init__(self) - self.storage = storage - - def supports_publisher_affiliation(self): - return True - - def supports_outcast_affiliation(self): - return True - - def supports_persistent_items(self): - return True - - def get_node_type(self, node_id): - d = self.storage.get_node(node_id) - d.addCallback(lambda node: node.get_type()) - return d - - def get_nodes(self): - return self.storage.get_node_ids() - - def get_node_meta_data(self, node_id): - d = self.storage.get_node(node_id) - d.addCallback(lambda node: node.get_meta_data()) - d.addCallback(self._make_meta_data) - return d - - def _make_meta_data(self, meta_data): - options = [] - for key, value in meta_data.iteritems(): - if self.options.has_key(key): - option = {"var": key} - option.update(self.options[key]) - option["value"] = value - options.append(option) - - return options - -class PublishService(service.Service): - - implements(backend.IPublishService) - - def _check_auth(self, node, requestor): - def check(affiliation, node): - if affiliation not in ['owner', 'publisher']: - raise backend.Forbidden - return node - - d = node.get_affiliation(requestor) - d.addCallback(check, node) - return d - - def publish(self, node_id, items, requestor): - d = self.parent.storage.get_node(node_id) - d.addCallback(self._check_auth, requestor) - d.addCallback(self._do_publish, items, requestor) - return d - - def _do_publish(self, node, items, requestor): - configuration = node.get_configuration() - persist_items = configuration["pubsub#persist_items"] - deliver_payloads = configuration["pubsub#deliver_payloads"] - - if items and not persist_items and not deliver_payloads: - raise backend.ItemForbidden - elif not items and (persist_items or deliver_payloads): - raise backend.ItemRequired - - if persist_items or deliver_payloads: - for item in items: - if not item.getAttribute("id"): - item["id"] = uuid.generate() - - if persist_items: - d = node.store_items(items, requestor) - else: - d = defer.succeed(None) - - d.addCallback(self._do_notify, node.id, items, deliver_payloads) - return d - - def _do_notify(self, result, node_id, items, deliver_payloads): - if items and not deliver_payloads: - for item in items: - item.children = [] - - self.parent.dispatch({ 'items': items, 'node_id': node_id }, - '//event/pubsub/notify') - -class NotificationService(service.Service): - - implements(backend.INotificationService) - - def get_notification_list(self, node_id, items): - d = self.parent.storage.get_node(node_id) - d.addCallback(lambda node: node.get_subscribers()) - d.addCallback(self._magic_filter, node_id, items) - return d - - def _magic_filter(self, subscribers, node_id, items): - list = [] - for subscriber in subscribers: - list.append((subscriber, items)) - return list - - def register_notifier(self, observerfn, *args, **kwargs): - self.parent.addObserver('//event/pubsub/notify', observerfn, - *args, **kwargs) - -class SubscriptionService(service.Service): - - implements(backend.ISubscriptionService) - - def subscribe(self, node_id, subscriber, requestor): - subscriber_entity = subscriber.userhostJID() - if subscriber_entity != requestor: - return defer.fail(backend.Forbidden()) - - d = self.parent.storage.get_node(node_id) - d.addCallback(_get_affiliation, subscriber_entity) - d.addCallback(self._do_subscribe, subscriber) - return d - - def _do_subscribe(self, result, subscriber): - node, affiliation = result - - if affiliation == 'outcast': - raise backend.Forbidden - - d = node.add_subscription(subscriber, 'subscribed') - d.addCallback(lambda _: 'subscribed') - d.addErrback(self._get_subscription, node, subscriber) - d.addCallback(self._return_subscription, node.id) - return d - - def _get_subscription(self, failure, node, subscriber): - failure.trap(storage.SubscriptionExists) - return node.get_subscription(subscriber) - - def _return_subscription(self, result, node_id): - return node_id, result - - def unsubscribe(self, node_id, subscriber, requestor): - if subscriber.userhostJID() != requestor: - return defer.fail(backend.Forbidden()) - - d = self.parent.storage.get_node(node_id) - d.addCallback(lambda node: node.remove_subscription(subscriber)) - return d - - def get_subscriptions(self, entity): - return self.parent.storage.get_subscriptions(entity) - -class NodeCreationService(service.Service): - - implements(backend.INodeCreationService) - - def supports_instant_nodes(self): - return True - - def create_node(self, node_id, requestor): - if not node_id: - node_id = 'generic/%s' % uuid.generate() - d = self.parent.storage.create_node(node_id, requestor) - d.addCallback(lambda _: node_id) - return d - - def get_default_configuration(self): - d = defer.succeed(self.parent.default_config) - d.addCallback(self._make_config) - return d - - def get_node_configuration(self, node_id): - if not node_id: - raise backend.NoRootNode - - d = self.parent.storage.get_node(node_id) - d.addCallback(lambda node: node.get_configuration()) - - d.addCallback(self._make_config) - return d - - def _make_config(self, config): - options = [] - for key, value in self.parent.options.iteritems(): - option = {"var": key} - option.update(value) - if config.has_key(key): - option["value"] = config[key] - options.append(option) - - return options - - def set_node_configuration(self, node_id, options, requestor): - if not node_id: - raise backend.NoRootNode - - for key, value in options.iteritems(): - if not self.parent.options.has_key(key): - raise backend.InvalidConfigurationOption - if self.parent.options[key]["type"] == 'boolean': - try: - options[key] = bool(int(value)) - except ValueError: - raise backend.InvalidConfigurationValue - - d = self.parent.storage.get_node(node_id) - d.addCallback(_get_affiliation, requestor) - d.addCallback(self._do_set_node_configuration, options) - return d - - def _do_set_node_configuration(self, result, options): - node, affiliation = result - - if affiliation != 'owner': - raise backend.Forbidden - - return node.set_configuration(options) - -class AffiliationsService(service.Service): - - implements(backend.IAffiliationsService) - - def get_affiliations(self, entity): - return self.parent.storage.get_affiliations(entity) - -class ItemRetrievalService(service.Service): - - implements(backend.IItemRetrievalService) - - def get_items(self, node_id, requestor, max_items=None, item_ids=[]): - d = self.parent.storage.get_node(node_id) - d.addCallback(_get_affiliation, requestor) - d.addCallback(self._do_get_items, max_items, item_ids) - return d - - def _do_get_items(self, result, max_items, item_ids): - node, affiliation = result - - if affiliation == 'outcast': - raise backend.Forbidden - - if item_ids: - return node.get_items_by_id(item_ids) - else: - return node.get_items(max_items) - -class RetractionService(service.Service): - - implements(backend.IRetractionService) - - def retract_item(self, node_id, item_ids, requestor): - d = self.parent.storage.get_node(node_id) - d.addCallback(_get_affiliation, requestor) - d.addCallback(self._do_retract, item_ids) - return d - - def _do_retract(self, result, item_ids): - node, affiliation = result - persist_items = node.get_configuration()["pubsub#persist_items"] - - if affiliation not in ['owner', 'publisher']: - raise backend.Forbidden - - if not persist_items: - raise backend.NodeNotPersistent - - d = node.remove_items(item_ids) - d.addCallback(self._do_notify_retraction, node.id) - return d - - def _do_notify_retraction(self, item_ids, node_id): - self.parent.dispatch({ 'item_ids': item_ids, 'node_id': node_id }, - '//event/pubsub/retract') - - def purge_node(self, node_id, requestor): - d = self.parent.storage.get_node(node_id) - d.addCallback(_get_affiliation, requestor) - d.addCallback(self._do_purge) - return d - - def _do_purge(self, result): - node, affiliation = result - persist_items = node.get_configuration()["pubsub#persist_items"] - - if affiliation != 'owner': - raise backend.Forbidden - - if not persist_items: - raise backend.NodeNotPersistent - - d = node.purge() - d.addCallback(self._do_notify_purge, node.id) - return d - - def _do_notify_purge(self, result, node_id): - self.parent.dispatch(node_id, '//event/pubsub/purge') - -class NodeDeletionService(service.Service): - - implements(backend.INodeDeletionService) - - def __init__(self): - self._callback_list = [] - - def register_pre_delete(self, pre_delete_fn): - self._callback_list.append(pre_delete_fn) - - def get_subscribers(self, node_id): - d = self.parent.storage.get_node(node_id) - d.addCallback(lambda node: node.get_subscribers()) - return d - - def delete_node(self, node_id, requestor): - d = self.parent.storage.get_node(node_id) - d.addCallback(_get_affiliation, requestor) - d.addCallback(self._do_pre_delete) - return d - - def _do_pre_delete(self, result): - node, affiliation = result - - if affiliation != 'owner': - raise backend.Forbidden - - d = defer.DeferredList([cb(node.id) for cb in self._callback_list], - consumeErrors=1) - d.addCallback(self._do_delete, node.id) - - def _do_delete(self, result, node_id): - dl = [] - for succeeded, r in result: - if succeeded and r: - dl.extend(r) - - d = self.parent.storage.delete_node(node_id) - d.addCallback(self._do_notify_delete, dl) - - return d - - def _do_notify_delete(self, result, dl): - for d in dl: - d.callback(None) diff -r 5abb017bd687 -r ef22e4150caa idavoll/idavoll.py --- a/idavoll/idavoll.py Thu Jan 18 14:08:32 2007 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,202 +0,0 @@ -# Copyright (c) 2003-2006 Ralph Meijer -# See LICENSE for details. - -from twisted.words.protocols.jabber import component, error -from twisted.application import service -from twisted.internet import defer - -import disco -import pubsub - -try: - from twisted.words.protocols.jabber.ijabber import IService -except ImportError: - from twisted.words.protocols.jabber.component import IService - -__version__ = '0.6.0' - -NS_VERSION = 'jabber:iq:version' - -IQ_GET = '/iq[@type="get"]' -IQ_SET = '/iq[@type="set"]' -VERSION = IQ_GET + '/query[@xmlns="' + NS_VERSION + '"]' -DISCO_INFO = IQ_GET + '/query[@xmlns="' + disco.NS_INFO + '"]' -DISCO_ITEMS = IQ_GET + '/query[@xmlns="' + disco.NS_ITEMS + '"]' - -class IdavollService(component.Service): - - def componentConnected(self, xmlstream): - self.xmlstream = xmlstream - xmlstream.addObserver(VERSION, self.onVersion, 1) - xmlstream.addObserver(DISCO_INFO, self.onDiscoInfo, 1) - xmlstream.addObserver(DISCO_ITEMS, self.onDiscoItems, 1) - xmlstream.addObserver(IQ_GET, self.iqFallback, -1) - xmlstream.addObserver(IQ_SET, self.iqFallback, -1) - - def get_disco_info(self, node): - info = [] - - if not node: - info.append(disco.Feature(disco.NS_ITEMS)) - info.append(disco.Feature(NS_VERSION)) - - return defer.succeed(info) - - def onVersion(self, iq): - iq.swapAttributeValues("to", "from") - iq["type"] = "result" - name = iq.addElement("name", None, 'Idavoll') - version = iq.addElement("version", None, __version__) - self.send(iq) - iq.handled = True - - def onDiscoInfo(self, iq): - dl = [] - node = iq.query.getAttribute("node") - - for c in self.parent: - if IService.providedBy(c): - if hasattr(c, "get_disco_info"): - dl.append(c.get_disco_info(node)) - d = defer.DeferredList(dl, fireOnOneErrback=1, consumeErrors=1) - d.addCallback(self._disco_info_results, iq, node) - d.addErrback(self._error, iq) - d.addCallback(self.send) - - iq.handled = True - - def _disco_info_results(self, results, iq, node): - info = [] - for i in results: - info.extend(i[1]) - - if node and not info: - return error.StanzaError('item-not-found').toResponse(iq) - else: - iq.swapAttributeValues("to", "from") - iq["type"] = "result" - for item in info: - #domish.Element.addChild should probably do this for all - # subclasses of Element - item.parent = iq.query - - iq.query.addChild(item) - - return iq - - def _error(self, result, iq): - print "Got error on index %d:" % result.value[1] - result.value[0].printBriefTraceback() - return error.StanzaError('internal-server-error').toResponse(iq) - - def onDiscoItems(self, iq): - dl = [] - node = iq.query.getAttribute("node") - - for c in self.parent: - if IService.providedBy(c): - if hasattr(c, "get_disco_items"): - dl.append(c.get_disco_items(node)) - d = defer.DeferredList(dl, fireOnOneErrback=1, consumeErrors=1) - d.addCallback(self._disco_items_result, iq, node) - d.addErrback(self._error, iq) - d.addCallback(self.send) - - iq.handled = True - - def _disco_items_result(self, results, iq, node): - items = [] - - for i in results: - items.extend(i[1]) - - iq.swapAttributeValues("to", "from") - iq["type"] = "result" - iq.query.children = items - - return iq - - def iqFallback(self, iq): - if iq.handled == True: - return - - self.send(error.StanzaError('service-unavailable').toResponse(iq)) - -class LogService(component.Service): - - def transportConnected(self, xmlstream): - xmlstream.rawDataInFn = self.rawDataIn - xmlstream.rawDataOutFn = self.rawDataOut - - def rawDataIn(self, buf): - print "RECV: %s" % unicode(buf, 'utf-8').encode('ascii', 'replace') - - def rawDataOut(self, buf): - print "SEND: %s" % unicode(buf, 'utf-8').encode('ascii', 'replace') - -def makeService(config): - serviceCollection = service.MultiService() - - # set up Jabber Component - sm = component.buildServiceManager(config["jid"], config["secret"], - ("tcp:%s:%s" % (config["rhost"], config["rport"]))) - - if config["verbose"]: - LogService().setServiceParent(sm) - - if config['backend'] == 'pgsql': - import pgsql_storage - st = pgsql_storage.Storage(user=config['dbuser'], - database=config['dbname'], - password=config['dbpass']) - elif config['backend'] == 'memory': - import memory_storage - st = memory_storage.Storage() - - import generic_backend as b - bs = b.BackendService(st) - - c = IService(bs) - c.setServiceParent(sm) - c.hide_nodes = config["hide-nodes"] - - bsc = b.PublishService() - bsc.setServiceParent(bs) - IService(bsc).setServiceParent(sm) - - bsc = b.NotificationService() - bsc.setServiceParent(bs) - IService(bsc).setServiceParent(sm) - - bsc = b.SubscriptionService() - bsc.setServiceParent(bs) - IService(bsc).setServiceParent(sm) - - bsc = b.NodeCreationService() - bsc.setServiceParent(bs) - IService(bsc).setServiceParent(sm) - - bsc = b.AffiliationsService() - bsc.setServiceParent(bs) - IService(bsc).setServiceParent(sm) - - bsc = b.ItemRetrievalService() - bsc.setServiceParent(bs) - IService(bsc).setServiceParent(sm) - - bsc = b.RetractionService() - bsc.setServiceParent(bs) - IService(bsc).setServiceParent(sm) - - bsc = b.NodeDeletionService() - bsc.setServiceParent(bs) - IService(bsc).setServiceParent(sm) - - s = IdavollService() - s.setServiceParent(sm) - - sm.setServiceParent(serviceCollection) - - # other stuff - - return sm diff -r 5abb017bd687 -r ef22e4150caa idavoll/iidavoll.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/idavoll/iidavoll.py Wed Oct 03 12:41:43 2007 +0000 @@ -0,0 +1,417 @@ +# Copyright (c) 2003-2007 Ralph Meijer +# See LICENSE for details. + +""" +Interfaces for idavoll. +""" + +from zope.interface import Interface + +class IBackendService(Interface): + """ Interface to a backend service of a pubsub service. """ + + def __init__(storage): + """ + @param storage: L{storage} object. + """ + + def supports_publisher_affiliation(): + """ Reports if the backend supports the publisher affiliation. + + @rtype: C{bool} + """ + + def supports_outcast_affiliation(): + """ Reports if the backend supports the publisher affiliation. + + @rtype: C{bool} + """ + + def supports_persistent_items(): + """ Reports if the backend supports persistent items. + + @rtype: C{bool} + """ + + def get_node_type(node_id): + """ Return type of a node. + + @return: a deferred that returns either 'leaf' or 'collection' + """ + + def get_nodes(): + """ Returns list of all nodes. + + @return: a deferred that returns a C{list} of node ids. + """ + + def get_node_meta_data(node_id): + """ Return meta data for a node. + + @return: a deferred that returns a C{list} of C{dict}s with the + metadata. + """ + + def create_node(node_id, requestor): + """ Create a node. + + @return: a deferred that fires when the node has been created. + """ + + def register_pre_delete(pre_delete_fn): + """ Register a callback that is called just before a node deletion. + + The function C{pre_deleted_fn} is added to a list of functions + to be called just before deletion of a node. The callback + C{pre_delete_fn} is called with the C{node_id} that is about to be + deleted and should return a deferred that returns a list of deferreds + that are to be fired after deletion. The backend collects the lists + from all these callbacks before actually deleting the node in question. + After deletion all collected deferreds are fired to do post-processing. + + The idea is that you want to be able to collect data from the + node before deleting it, for example to get a list of subscribers + that have to be notified after the node has been deleted. To do this, + C{pre_delete_fn} fetches the subscriber list and passes this + list to a callback attached to a deferred that it sets up. This + deferred is returned in the list of deferreds. + """ + + def delete_node(node_id, requestor): + """ Delete a node. + + @return: a deferred that fires when the node has been deleted. + """ + + def purge_node(node_id, requestor): + """ Removes all items in node from persistent storage """ + + def subscribe(node_id, subscriber, requestor): + """ Request the subscription of an entity to a pubsub node. + + Depending on the node's configuration and possible business rules, the + C{subscriber} is added to the list of subscriptions of the node with id + C{node_id}. The C{subscriber} might be different from the C{requestor}, + and if the C{requestor} is not allowed to subscribe this entity an + exception should be raised. + + @return: a deferred that returns the subscription state + """ + + def unsubscribe(node_id, subscriber, requestor): + """ Cancel the subscription of an entity to a pubsub node. + + The subscription of C{subscriber} is removed from the list of + subscriptions of the node with id C{node_id}. If the C{requestor} + is not allowed to unsubscribe C{subscriber}, an an exception should + be raised. + + @return: a deferred that fires when unsubscription is complete. + """ + + def get_subscribers(node_id): + """ Get node subscriber list. + + @return: a deferred that fires with the list of subscribers. + """ + + def get_subscriptions(entity): + """ Report the list of current subscriptions with this pubsub service. + + Report the list of the current subscriptions with all nodes within this + pubsub service, for the C{entity}. + + @return: a deferred that returns the list of all current subscriptions + as tuples C{(node_id, subscriber, subscription)}. + """ + + def get_affiliations(entity): + """ Report the list of current affiliations with this pubsub service. + + Report the list of the current affiliations with all nodes within this + pubsub service, for the C{entity}. + + @return: a deferred that returns the list of all current affiliations + as tuples C{(node_id, affiliation)}. + """ + + def publish(node_id, items, requestor): + """ Publish items to a pubsub node. + + @return: a deferred that fires when the items have been published. + """ + + def register_notifier(observerfn, *args, **kwargs): + """ Register callback which is called for notification. """ + + def get_notification_list(node_id, items): + """ + Get list of entities to notify. + """ + + def get_items(node_id, requestor, max_items=None, item_ids=[]): + """ Retrieve items from persistent storage + + If C{max_items} is given, return the C{max_items} last published + items, else if C{item_ids} is not empty, return the items requested. + If neither is given, return all items. + + @return: a deferred that returns the requested items + """ + + def retract_item(node_id, item_id, requestor): + """ Removes item in node from persistent storage """ + + +class IStorage(Interface): + """ + Storage interface. + """ + + def get_node(node_id): + """ + Get Node. + + @param node_id: NodeID of the desired node. + @type node_id: L{str} + @return: deferred that returns a L{Node} object. + """ + + def get_node_ids(): + """ + Return all NodeIDs. + + @return: deferred that returns a list of NodeIDs (L{str}). + """ + + def create_node(node_id, owner, config = None, type='leaf'): + """ + Create new node. + + The implementation should make sure, the passed owner JID is stripped + of the resource (e.g. using C{owner.userhostJID()}). + + @param node_id: NodeID of the new node. + @type node_id: L{str} + @param owner: JID of the new nodes's owner. + @type owner: L{jid.JID} + @param config: Configuration + @param type: Node type. Can be either C{'leaf'} or C{'collection'}. + @return: deferred that fires on creation. + """ + + def delete_node(node_id): + """ + Delete a node. + + @param node_id: NodeID of the new node. + @type node_id: L{str} + @return: deferred that fires on deletion. + """ + + def get_affiliations(entity): + """ + Get all affiliations for entity. + + The implementation should make sure, the passed owner JID is stripped + of the resource (e.g. using C{owner.userhostJID()}). + + @param entity: JID of the entity. + @type entity: L{jid.JID} + @return: deferred that returns a L{list} of tuples of the form + C{(node_id, affiliation)}, where C{node_id} is of the type + L{str} and C{affiliation} is one of C{'owner'}, C{'publisher'} + and C{'outcast'}. + """ + + def get_subscriptions(entity): + """ + Get all subscriptions for an entity. + + The implementation should make sure, the passed owner JID is stripped + of the resource (e.g. using C{owner.userhostJID()}). + + @param entity: JID of the entity. + @type entity: L{jid.JID} + @return: deferred that returns a L{list} of tuples of the form + C{(node_id, subscriber, state)}, where C{node_id} is of the + type L{str}, C{subscriber} of the type {jid.JID}, and + C{state} is C{'subscribed'} or C{'pending'}. + """ + + +class INode(Interface): + """ + Interface to the class of objects that represent nodes. + """ + + def get_type(): + """ + Get node's type. + + @return: C{'leaf'} or C{'collection'}. + """ + + def get_configuration(): + """ + Get node's configuration. + + The configuration must at least have two options: + C{pubsub#persist_items}, and C{pubsub#deliver_payloads}. + + @return: L{dict} of configuration options. + """ + + def get_meta_data(): + """ + Get node's meta data. + + The meta data must be a superset of the configuration options, and + also at least should have a C{pubsub#node_type} entry. + + @return: L{dict} of meta data. + """ + + def set_configuration(options): + """ + Set node's configuration. + + The elements of {options} will set the new values for those + configuration items. This means that only changing items have to + be given. + + @param options: a dictionary of configuration options. + @returns: a deferred that fires upon success. + """ + + def get_affiliation(entity): + """ + Get affiliation of entity with this node. + + @param entity: JID of entity. + @type entity: L{jid.JID} + @return: deferred that returns C{'owner'}, C{'publisher'}, C{'outcast'} + or C{None}. + """ + + def get_subscription(subscriber): + """ + Get subscription to this node of subscriber. + + @param subscriber: JID of the new subscriptions' entity. + @type subscriber: L{jid.JID} + @return: deferred that returns the subscription state (C{'subscribed'}, + C{'pending'} or C{None}). + """ + + def add_subscription(subscriber, state): + """ + Add new subscription to this node with given state. + + @param subscriber: JID of the new subscriptions' entity. + @type subscriber: L{jid.JID} + @param state: C{'subscribed'} or C{'pending'} + @type state: L{str} + @return: deferred that fires on subscription. + """ + + def remove_subscription(subscriber): + """ + Remove subscription to this node. + + @param subscriber: JID of the subscriptions' entity. + @type subscriber: L{jid.JID} + @return: deferred that fires on removal. + """ + + def get_subscribers(): + """ + Get list of subscribers to this node. + + Retrieves the list of entities that have a subscription to this + node. That is, having the state C{'subscribed'}. + + @return: a deferred that returns a L{list} of L{jid.JID}s. + """ + + def is_subscribed(entity): + """ + Returns whether entity has any subscription to this node. + + Only returns C{True} when the subscription state (if present) is + C{'subscribed'} for any subscription that matches the bare JID. + + @param subscriber: bare JID of the subscriptions' entity. + @type subscriber: L{jid.JID} + @return: deferred that returns a L{bool}. + """ + + def get_affiliations(): + """ + Get affiliations of entities with this node. + + @return: deferred that returns a L{list} of tuples (jid, affiliation), + where jid is a L(jid.JID) and affiliation is one of C{'owner'}, + C{'publisher'}, C{'outcast'}. + """ + +class ILeafNode(Interface): + """ + Interface to the class of objects that represent leaf nodes. + """ + + def store_items(items, publisher): + """ + Store items in persistent storage for later retrieval. + + @param items: The list of items to be stored. Each item is the + L{domish} representation of the XML fragment as defined + for C{} in the + C{http://jabber.org/protocol/pubsub} namespace. + @type items: L{list} of {domish.Element} + @param publisher: JID of the publishing entity. + @type publisher: L{jid.JID} + @return: deferred that fires upon success. + """ + + def remove_items(item_ids): + """ + Remove items by id. + + @param item_ids: L{list} of item ids. + @return: deferred that fires with a L{list} of ids of the items that + were deleted + """ + + def get_items(max_items=None): + """ + Get items. + + If C{max_items} is not given, all items in the node are returned, + just like C{get_items_by_id}. Otherwise, C{max_items} limits + the returned items to a maximum of that number of most recently + published items. + + @param max_items: if given, a natural number (>0) that limits the + returned number of items. + @return: deferred that fires with a L{list} of found items. + """ + + def get_items_by_id(item_ids): + """ + Get items by item id. + + Each item in the returned list is a unicode string that + represent the XML of the item as it was published, including the + item wrapper with item id. + + @param item_ids: L{list} of item ids. + @return: deferred that fires with a L{list} of found items. + """ + + def purge(): + """ + Purge node of all items in persistent storage. + + @return: deferred that fires when the node has been purged. + """ diff -r 5abb017bd687 -r ef22e4150caa idavoll/memory_storage.py --- a/idavoll/memory_storage.py Thu Jan 18 14:08:32 2007 +0000 +++ b/idavoll/memory_storage.py Wed Oct 03 12:41:43 2007 +0000 @@ -1,18 +1,19 @@ -# Copyright (c) 2003-2006 Ralph Meijer +# Copyright (c) 2003-2007 Ralph Meijer # See LICENSE for details. import copy from zope.interface import implements from twisted.internet import defer from twisted.words.protocols.jabber import jid -import storage + +from idavoll import error, iidavoll default_config = {"pubsub#persist_items": True, "pubsub#deliver_payloads": True} class Storage: - implements(storage.IStorage) + implements(iidavoll.IStorage) def __init__(self): self._nodes = {} @@ -21,7 +22,7 @@ try: node = self._nodes[node_id] except KeyError: - return defer.fail(storage.NodeNotFound()) + return defer.fail(error.NodeNotFound()) return defer.succeed(node) @@ -30,7 +31,7 @@ def create_node(self, node_id, owner, config = None, type='leaf'): if node_id in self._nodes: - return defer.fail(storage.NodeExists()) + return defer.fail(error.NodeExists()) if not config: config = copy.copy(default_config) @@ -47,7 +48,7 @@ try: del self._nodes[node_id] except KeyError: - return defer.fail(storage.NodeNotFound()) + return defer.fail(error.NodeNotFound()) return defer.succeed(None) @@ -70,7 +71,7 @@ class Node: - implements(storage.INode) + implements(iidavoll.INode) def __init__(self, node_id, owner, config): self.id = node_id @@ -110,8 +111,8 @@ def add_subscription(self, subscriber, state): if self._subscriptions.get(subscriber.full()): - return defer.fail(storage.SubscriptionExists()) - + return defer.fail(error.SubscriptionExists()) + subscription = Subscription(state) self._subscriptions[subscriber.full()] = subscription return defer.succeed(None) @@ -120,7 +121,7 @@ try: del self._subscriptions[subscriber.full()] except KeyError: - return defer.fail(storage.SubscriptionNotFound()) + return defer.fail(error.SubscriptionNotFound()) return defer.succeed(None) @@ -208,7 +209,7 @@ class LeafNode(Node, LeafNodeMixin): - implements(storage.ILeafNode) + implements(iidavoll.ILeafNode) def __init__(self, node_id, owner, config): Node.__init__(self, node_id, owner, config) @@ -216,7 +217,5 @@ class Subscription: - implements(storage.ISubscription) - def __init__(self, state): self.state = state diff -r 5abb017bd687 -r ef22e4150caa idavoll/pgsql_storage.py --- a/idavoll/pgsql_storage.py Thu Jan 18 14:08:32 2007 +0000 +++ b/idavoll/pgsql_storage.py Wed Oct 03 12:41:43 2007 +0000 @@ -1,16 +1,16 @@ -# Copyright (c) 2003-2006 Ralph Meijer +# Copyright (c) 2003-2007 Ralph Meijer # See LICENSE for details. import copy -import storage from twisted.enterprise import adbapi -from twisted.internet import defer from twisted.words.protocols.jabber import jid from zope.interface import implements +from idavoll import error, iidavoll + class Storage: - implements(storage.IStorage) + implements(iidavoll.IStorage) def __init__(self, user, database, password = None): self._dbpool = adbapi.ConnectionPool('pyPgSQL.PgSQL', @@ -32,7 +32,7 @@ (configuration["pubsub#persist_items"], configuration["pubsub#deliver_payloads"]) = cursor.fetchone() except TypeError: - raise storage.NodeNotFound + raise error.NodeNotFound() else: node = LeafNode(node_id, configuration) node._dbpool = self._dbpool @@ -53,8 +53,8 @@ cursor.execute("""INSERT INTO nodes (node) VALUES (%s)""", (node_id)) except cursor._pool.dbapi.OperationalError: - raise storage.NodeExists - + raise error.NodeExists() + cursor.execute("""SELECT 1 from entities where jid=%s""", (owner)) @@ -78,7 +78,7 @@ (node_id,)) if cursor.rowcount != 1: - raise storage.NodeNotFound + raise error.NodeNotFound() def get_affiliations(self, entity): d = self._dbpool.runQuery("""SELECT node, affiliation FROM entities @@ -110,7 +110,7 @@ class Node: - implements(storage.INode) + implements(iidavoll.INode) def __init__(self, node_id, config): self.id = node_id @@ -120,7 +120,7 @@ cursor.execute("""SELECT id FROM nodes WHERE node=%s""", (self.id)) if not cursor.fetchone(): - raise backend.NodeNotFound + raise error.NodeNotFound() def get_type(self): return self.type @@ -222,7 +222,7 @@ self.id, userhost)) except cursor._pool.dbapi.OperationalError: - raise storage.SubscriptionExists + raise error.SubscriptionExists() def remove_subscription(self, subscriber): return self._dbpool.runInteraction(self._remove_subscription, @@ -242,7 +242,7 @@ userhost, resource)) if cursor.rowcount != 1: - raise storage.SubscriptionNotFound + raise error.SubscriptionNotFound() return None @@ -398,4 +398,4 @@ class LeafNode(Node, LeafNodeMixin): - implements(storage.ILeafNode) + implements(iidavoll.ILeafNode) diff -r 5abb017bd687 -r ef22e4150caa idavoll/pubsub.py --- a/idavoll/pubsub.py Thu Jan 18 14:08:32 2007 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,694 +0,0 @@ -# Copyright (c) 2003-2006 Ralph Meijer -# See LICENSE for details. - -from zope.interface import implements - -from twisted.words.protocols.jabber import component, jid, error -from twisted.words.xish import domish -from twisted.python import components -from twisted.internet import defer - -import backend -import storage -import disco -import data_form - -try: - from twisted.words.protocols.jabber.ijabber import IService -except ImportError: - from twisted.words.protocols.jabber.component import IService - -if issubclass(domish.SerializedXML, str): - # Work around bug # in twisted Xish - class SerializedXML(unicode): - """ Marker class for pre-serialized XML in the DOM. """ - - domish.SerializedXML = SerializedXML - -NS_COMPONENT = 'jabber:component:accept' -NS_PUBSUB = 'http://jabber.org/protocol/pubsub' -NS_PUBSUB_EVENT = NS_PUBSUB + '#event' -NS_PUBSUB_ERRORS = NS_PUBSUB + '#errors' -NS_PUBSUB_OWNER = NS_PUBSUB + "#owner" - -IQ_GET = '/iq[@type="get"]' -IQ_SET = '/iq[@type="set"]' -PUBSUB_ELEMENT = '/pubsub[@xmlns="' + NS_PUBSUB + '"]' -PUBSUB_OWNER_ELEMENT = '/pubsub[@xmlns="' + NS_PUBSUB_OWNER + '"]' -PUBSUB_GET = IQ_GET + PUBSUB_ELEMENT -PUBSUB_SET = IQ_SET + PUBSUB_ELEMENT -PUBSUB_OWNER_GET = IQ_GET + PUBSUB_OWNER_ELEMENT -PUBSUB_OWNER_SET = IQ_SET + PUBSUB_OWNER_ELEMENT -PUBSUB_CREATE = PUBSUB_SET + '/create' -PUBSUB_PUBLISH = PUBSUB_SET + '/publish' -PUBSUB_SUBSCRIBE = PUBSUB_SET + '/subscribe' -PUBSUB_UNSUBSCRIBE = PUBSUB_SET + '/unsubscribe' -PUBSUB_OPTIONS_GET = PUBSUB_GET + '/options' -PUBSUB_OPTIONS_SET = PUBSUB_SET + '/options' -PUBSUB_DEFAULT = PUBSUB_OWNER_GET + '/default' -PUBSUB_CONFIGURE_GET = PUBSUB_OWNER_GET + '/configure' -PUBSUB_CONFIGURE_SET = PUBSUB_OWNER_SET + '/configure' -PUBSUB_SUBSCRIPTIONS = PUBSUB_GET + '/subscriptions' -PUBSUB_AFFILIATIONS = PUBSUB_GET + '/affiliations' -PUBSUB_ITEMS = PUBSUB_GET + '/items' -PUBSUB_RETRACT = PUBSUB_SET + '/retract' -PUBSUB_PURGE = PUBSUB_OWNER_SET + '/purge' -PUBSUB_DELETE = PUBSUB_OWNER_SET + '/delete' - -class BadRequest(error.StanzaError): - def __init__(self): - error.StanzaError.__init__(self, 'bad-request') - -class PubSubError(error.StanzaError): - def __init__(self, condition, pubsubCondition, feature=None, text=None): - appCondition = domish.Element((NS_PUBSUB_ERRORS, pubsubCondition)) - if feature: - appCondition['feature'] = feature - error.StanzaError.__init__(self, condition, - text=text, - appCondition=appCondition) - -class OptionsUnavailable(PubSubError): - def __init__(self): - PubSubError.__init__(self, 'feature-not-implemented', - 'unsupported', - 'subscription-options-unavailable') - -error_map = { - storage.NodeNotFound: ('item-not-found', None, None), - storage.NodeExists: ('conflict', None, None), - storage.SubscriptionNotFound: ('not-authorized', 'not-subscribed', None), - backend.Forbidden: ('forbidden', None, None), - backend.ItemForbidden: ('bad-request', 'item-forbidden', None), - backend.ItemRequired: ('bad-request', 'item-required', None), - backend.NoInstantNodes: ('not-acceptable', 'unsupported', 'instant-nodes'), - backend.NotSubscribed: ('not-authorized', 'not-subscribed', None), - backend.InvalidConfigurationOption: ('not-acceptable', None, None), - backend.InvalidConfigurationValue: ('not-acceptable', None, None), - backend.NodeNotPersistent: ('feature-not-implemented', 'unsupported', - 'persistent-node'), - backend.NoRootNode: ('bad-request', None, None), -} - -class Service(component.Service): - - implements(IService) - - def __init__(self, backend): - self.backend = backend - - def error(self, failure, iq): - try: - e = failure.trap(error.StanzaError, *error_map.keys()) - except: - failure.printBriefTraceback() - return error.StanzaError('internal-server-error').toResponse(iq) - else: - if e == error.StanzaError: - exc = failure.value - else: - condition, pubsubCondition, feature = error_map[e] - msg = failure.value.msg - - if pubsubCondition: - exc = PubSubError(condition, pubsubCondition, feature, msg) - else: - exc = error.StanzaError(condition, text=msg) - - return exc.toResponse(iq) - - def success(self, result, iq): - iq.swapAttributeValues("to", "from") - iq["type"] = 'result' - iq.children = [] - if result: - for child in result: - iq.addChild(child) - - return iq - - def handler_wrapper(self, handler, iq): - try: - d = handler(iq) - except: - d = defer.fail() - - d.addCallback(self.success, iq) - d.addErrback(self.error, iq) - d.addCallback(self.send) - iq.handled = True - -class ComponentServiceFromService(Service): - - def __init__(self, backend): - Service.__init__(self, backend) - self.hide_nodes = False - - def get_disco_info(self, node): - info = [] - - if not node: - info.append(disco.Identity('pubsub', 'generic', - 'Generic Pubsub Service')) - - info.append(disco.Feature(NS_PUBSUB + "#meta-data")) - - if self.backend.supports_outcast_affiliation(): - info.append(disco.Feature(NS_PUBSUB + "#outcast-affiliation")) - - if self.backend.supports_persistent_items(): - info.append(disco.Feature(NS_PUBSUB + "#persistent-items")) - - if self.backend.supports_publisher_affiliation(): - info.append(disco.Feature(NS_PUBSUB + "#publisher-affiliation")) - - return defer.succeed(info) - else: - def trap_not_found(result): - result.trap(storage.NodeNotFound) - return [] - - d = self.backend.get_node_type(node) - d.addCallback(self._add_identity, [], node) - d.addErrback(trap_not_found) - return d - - def _add_identity(self, node_type, result_list, node): - result_list.append(disco.Identity('pubsub', node_type)) - d = self.backend.get_node_meta_data(node) - d.addCallback(self._add_meta_data, node_type, result_list) - return d - - def _add_meta_data(self, meta_data, node_type, result_list): - form = data_form.Form(type="result", - form_type=NS_PUBSUB + "#meta-data") - - for meta_datum in meta_data: - try: - del meta_datum['options'] - except KeyError: - pass - - form.add_field(**meta_datum) - - form.add_field("text-single", - "pubsub#node_type", - "The type of node (collection or leaf)", - node_type) - result_list.append(form) - return result_list - - def get_disco_items(self, node): - if node or self.hide_nodes: - return defer.succeed([]) - - d = self.backend.get_nodes() - d.addCallback(lambda nodes: [disco.Item(self.parent.jabberId, node) - for node in nodes]) - return d - -components.registerAdapter(ComponentServiceFromService, - backend.IBackendService, - IService) - -class ComponentServiceFromNotificationService(Service): - - def componentConnected(self, xmlstream): - self.backend.register_notifier(self.notify) - - def notify(self, object): - node_id = object["node_id"] - items = object["items"] - d = self.backend.get_notification_list(node_id, items) - d.addCallback(self._notify, node_id) - - def _notify(self, list, node_id): - for recipient, items in list: - self._notify_recipient(recipient, node_id, items) - - def _notify_recipient(self, recipient, node_id, itemlist): - message = domish.Element((NS_COMPONENT, "message")) - message["from"] = self.parent.jabberId - message["to"] = recipient.full() - event = message.addElement((NS_PUBSUB_EVENT, "event")) - items = event.addElement("items") - items["node"] = node_id - items.children.extend(itemlist) - self.send(message) - -components.registerAdapter(ComponentServiceFromNotificationService, - backend.INotificationService, - IService) - -class ComponentServiceFromPublishService(Service): - - def componentConnected(self, xmlstream): - xmlstream.addObserver(PUBSUB_PUBLISH, self.onPublish) - - def get_disco_info(self, node): - info = [] - - if not node: - info.append(disco.Feature(NS_PUBSUB + "#item-ids")) - - return defer.succeed(info) - - def onPublish(self, iq): - self.handler_wrapper(self._onPublish, iq) - - def _onPublish(self, iq): - try: - node = iq.pubsub.publish["node"] - except KeyError: - raise BadRequest - - items = [] - for child in iq.pubsub.publish.children: - if child.__class__ == domish.Element and child.name == 'item': - items.append(child) - - return self.backend.publish(node, items, - jid.internJID(iq["from"]).userhostJID()) - -components.registerAdapter(ComponentServiceFromPublishService, - backend.IPublishService, - IService) - -class ComponentServiceFromSubscriptionService(Service): - - def componentConnected(self, xmlstream): - xmlstream.addObserver(PUBSUB_SUBSCRIBE, self.onSubscribe) - xmlstream.addObserver(PUBSUB_UNSUBSCRIBE, self.onUnsubscribe) - xmlstream.addObserver(PUBSUB_OPTIONS_GET, self.onOptionsGet) - xmlstream.addObserver(PUBSUB_OPTIONS_SET, self.onOptionsSet) - xmlstream.addObserver(PUBSUB_SUBSCRIPTIONS, self.onSubscriptions) - - def get_disco_info(self, node): - info = [] - - if not node: - info.append(disco.Feature(NS_PUBSUB + '#subscribe')) - info.append(disco.Feature(NS_PUBSUB + '#retrieve-subscriptions')) - - return defer.succeed(info) - - def onSubscribe(self, iq): - self.handler_wrapper(self._onSubscribe, iq) - - def _onSubscribe(self, iq): - try: - node_id = iq.pubsub.subscribe["node"] - subscriber = jid.internJID(iq.pubsub.subscribe["jid"]) - except KeyError: - raise BadRequest - - requestor = jid.internJID(iq["from"]).userhostJID() - d = self.backend.subscribe(node_id, subscriber, requestor) - d.addCallback(self.return_subscription, subscriber) - return d - - def return_subscription(self, result, subscriber): - node, state = result - - reply = domish.Element((NS_PUBSUB, "pubsub")) - subscription = reply.addElement("subscription") - subscription["node"] = node - subscription["jid"] = subscriber.full() - subscription["subscription"] = state - return [reply] - - def onUnsubscribe(self, iq): - self.handler_wrapper(self._onUnsubscribe, iq) - - def _onUnsubscribe(self, iq): - try: - node_id = iq.pubsub.unsubscribe["node"] - subscriber = jid.internJID(iq.pubsub.unsubscribe["jid"]) - except KeyError: - raise BadRequest - - requestor = jid.internJID(iq["from"]).userhostJID() - return self.backend.unsubscribe(node_id, subscriber, requestor) - - def onOptionsGet(self, iq): - self.handler_wrapper(self._onOptionsGet, iq) - - def _onOptionsGet(self, iq): - raise OptionsUnavailable - - def onOptionsSet(self, iq): - self.handler_wrapper(self._onOptionsSet, iq) - - def _onOptionsSet(self, iq): - raise OptionsUnavailable - - def onSubscriptions(self, iq): - self.handler_wrapper(self._onSubscriptions, iq) - - def _onSubscriptions(self, iq): - entity = jid.internJID(iq["from"]).userhostJID() - d = self.backend.get_subscriptions(entity) - d.addCallback(self._return_subscriptions_response, iq) - return d - - def _return_subscriptions_response(self, result, iq): - reply = domish.Element((NS_PUBSUB, 'pubsub')) - subscriptions = reply.addElement('subscriptions') - for node, subscriber, state in result: - item = subscriptions.addElement('subscription') - item['node'] = node - item['jid'] = subscriber.full() - item['subscription'] = state - return [reply] - -components.registerAdapter(ComponentServiceFromSubscriptionService, - backend.ISubscriptionService, - IService) - -class ComponentServiceFromNodeCreationService(Service): - - def componentConnected(self, xmlstream): - xmlstream.addObserver(PUBSUB_CREATE, self.onCreate) - xmlstream.addObserver(PUBSUB_DEFAULT, self.onDefault) - xmlstream.addObserver(PUBSUB_CONFIGURE_GET, self.onConfigureGet) - xmlstream.addObserver(PUBSUB_CONFIGURE_SET, self.onConfigureSet) - - def get_disco_info(self, node): - info = [] - - if not node: - info.append(disco.Feature(NS_PUBSUB + "#create-nodes")) - info.append(disco.Feature(NS_PUBSUB + "#config-node")) - info.append(disco.Feature(NS_PUBSUB + "#retrieve-default")) - - if self.backend.supports_instant_nodes(): - info.append(disco.Feature(NS_PUBSUB + "#instant-nodes")) - - return defer.succeed(info) - - def onCreate(self, iq): - print "onCreate" - self.handler_wrapper(self._onCreate, iq) - - def _onCreate(self, iq): - node = iq.pubsub.create.getAttribute("node") - - owner = jid.internJID(iq["from"]).userhostJID() - - d = self.backend.create_node(node, owner) - d.addCallback(self._return_create_response, iq) - return d - - def _return_create_response(self, result, iq): - node_id = iq.pubsub.create.getAttribute("node") - if not node_id or node_id != result: - reply = domish.Element((NS_PUBSUB, 'pubsub')) - entity = reply.addElement('create') - entity['node'] = result - return [reply] - - def onDefault(self, iq): - self.handler_wrapper(self._onDefault, iq) - - def _onDefault(self, iq): - d = self.backend.get_default_configuration() - d.addCallback(self._return_default_response) - return d - - def _return_default_response(self, options): - reply = domish.Element((NS_PUBSUB_OWNER, "pubsub")) - default = reply.addElement("default") - default.addChild(self._form_from_configuration(options)) - - return [reply] - - def onConfigureGet(self, iq): - self.handler_wrapper(self._onConfigureGet, iq) - - def _onConfigureGet(self, iq): - node_id = iq.pubsub.configure.getAttribute("node") - - d = self.backend.get_node_configuration(node_id) - d.addCallback(self._return_configuration_response, node_id) - return d - - def _return_configuration_response(self, options, node_id): - reply = domish.Element((NS_PUBSUB_OWNER, "pubsub")) - configure = reply.addElement("configure") - if node_id: - configure["node"] = node_id - configure.addChild(self._form_from_configuration(options)) - - return [reply] - - def _form_from_configuration(self, options): - form = data_form.Form(type="form", - form_type=NS_PUBSUB + "#node_config") - - for option in options: - form.add_field(**option) - - return form - - def onConfigureSet(self, iq): - print "onConfigureSet" - self.handler_wrapper(self._onConfigureSet, iq) - - def _onConfigureSet(self, iq): - node_id = iq.pubsub.configure["node"] - requestor = jid.internJID(iq["from"]).userhostJID() - - for element in iq.pubsub.configure.elements(): - if element.name != 'x' or element.uri != data_form.NS_X_DATA: - continue - - type = element.getAttribute("type") - if type == "cancel": - return None - elif type != "submit": - continue - - options = self._get_form_options(element) - - if options["FORM_TYPE"] == NS_PUBSUB + "#node_config": - del options["FORM_TYPE"] - return self.backend.set_node_configuration(node_id, - options, - requestor) - - raise BadRequest - - def _get_form_options(self, form): - options = {} - - for element in form.elements(): - if element.name == 'field' and element.uri == data_form.NS_X_DATA: - try: - options[element["var"]] = str(element.value) - except (KeyError, AttributeError): - raise BadRequest - - return options - -components.registerAdapter(ComponentServiceFromNodeCreationService, - backend.INodeCreationService, - IService) - -class ComponentServiceFromAffiliationsService(Service): - - def componentConnected(self, xmlstream): - xmlstream.addObserver(PUBSUB_AFFILIATIONS, self.onAffiliations) - - def get_disco_info(self, node): - info = [] - - if not node: - info.append(disco.Feature(NS_PUBSUB + "#retrieve-affiliations")) - - return defer.succeed(info) - - def onAffiliations(self, iq): - self.handler_wrapper(self._onAffiliations, iq) - - def _onAffiliations(self, iq): - entity = jid.internJID(iq["from"]).userhostJID() - d = self.backend.get_affiliations(entity) - d.addCallback(self._return_affiliations_response, iq) - return d - - def _return_affiliations_response(self, result, iq): - reply = domish.Element((NS_PUBSUB, 'pubsub')) - affiliations = reply.addElement('affiliations') - for node, affiliation in result: - item = affiliations.addElement('affiliation') - item['node'] = node - item['affiliation'] = affiliation - return [reply] - -components.registerAdapter(ComponentServiceFromAffiliationsService, - backend.IAffiliationsService, - IService) - -class ComponentServiceFromItemRetrievalService(Service): - - def componentConnected(self, xmlstream): - xmlstream.addObserver(PUBSUB_ITEMS, self.onItems) - - def get_disco_info(self, node): - info = [] - - if not node: - info.append(disco.Feature(NS_PUBSUB + "#retrieve-items")) - - return defer.succeed(info) - - def onItems(self, iq): - self.handler_wrapper(self._onItems, iq) - - def _onItems(self, iq): - try: - node_id = iq.pubsub.items["node"] - except KeyError: - raise BadRequest - - max_items = iq.pubsub.items.getAttribute('max_items') - - if max_items: - try: - max_items = int(max_items) - except ValueError: - raise BadRequest - - item_ids = [] - for child in iq.pubsub.items.elements(): - if child.name == 'item' and child.uri == NS_PUBSUB: - try: - item_ids.append(child["id"]) - except KeyError: - raise BadRequest - - d = self.backend.get_items(node_id, - jid.internJID(iq["from"]), - max_items, - item_ids) - d.addCallback(self._return_items_response, node_id) - return d - - def _return_items_response(self, result, node_id): - reply = domish.Element((NS_PUBSUB, 'pubsub')) - items = reply.addElement('items') - items["node"] = node_id - for r in result: - items.addRawXml(r) - - return [reply] - -components.registerAdapter(ComponentServiceFromItemRetrievalService, - backend.IItemRetrievalService, - IService) - -class ComponentServiceFromRetractionService(Service): - - def componentConnected(self, xmlstream): - xmlstream.addObserver(PUBSUB_RETRACT, self.onRetract) - xmlstream.addObserver(PUBSUB_PURGE, self.onPurge) - - def get_disco_info(self, node): - info = [] - - if not node: - info.append(disco.Feature(NS_PUBSUB + "#delete-any")) - info.append(disco.Feature(NS_PUBSUB + "#retract-items")) - info.append(disco.Feature(NS_PUBSUB + "#purge-nodes")) - - return defer.succeed(info) - - def onRetract(self, iq): - self.handler_wrapper(self._onRetract, iq) - - def _onRetract(self, iq): - try: - node = iq.pubsub.retract["node"] - except KeyError: - raise BadRequest - - item_ids = [] - for child in iq.pubsub.retract.children: - if child.__class__ == domish.Element and child.name == 'item': - try: - item_ids.append(child["id"]) - except KeyError: - raise BadRequest - - requestor = jid.internJID(iq["from"]).userhostJID() - return self.backend.retract_item(node, item_ids, requestor) - - def onPurge(self, iq): - self.handler_wrapper(self._onPurge, iq) - - def _onPurge(self, iq): - try: - node = iq.pubsub.purge["node"] - except KeyError: - raise BadRequest - - return self.backend.purge_node(node, - jid.internJID(iq["from"]).userhostJID()) - -components.registerAdapter(ComponentServiceFromRetractionService, - backend.IRetractionService, - IService) - -class ComponentServiceFromNodeDeletionService(Service): - - def __init__(self, backend): - Service.__init__(self, backend) - self.subscribers = [] - - def componentConnected(self, xmlstream): - self.backend.register_pre_delete(self._pre_delete) - xmlstream.addObserver(PUBSUB_DELETE, self.onDelete) - - def get_disco_info(self, node): - info = [] - - if not node: - info.append(disco.Feature(NS_PUBSUB + "#delete-nodes")) - - return defer.succeed(info) - - def _pre_delete(self, node_id): - d = self.backend.get_subscribers(node_id) - d.addCallback(self._return_deferreds, node_id) - return d - - def _return_deferreds(self, subscribers, node_id): - d = defer.Deferred() - d.addCallback(self._notify, subscribers, node_id) - return [d] - - def _notify(self, result, subscribers, node_id): - message = domish.Element((NS_COMPONENT, "message")) - message["from"] = self.parent.jabberId - event = message.addElement((NS_PUBSUB_EVENT, "event")) - event.addElement("delete")["node"] = node_id - - for subscriber in subscribers: - message["to"] = subscriber - self.send(message) - - def onDelete(self, iq): - self.handler_wrapper(self._onDelete, iq) - - def _onDelete(self, iq): - try: - node = iq.pubsub.delete["node"] - except KeyError: - raise BadRequest - - return self.backend.delete_node(node, - jid.internJID(iq["from"]).userhostJID()) - -components.registerAdapter(ComponentServiceFromNodeDeletionService, - backend.INodeDeletionService, - IService) diff -r 5abb017bd687 -r ef22e4150caa idavoll/storage.py --- a/idavoll/storage.py Thu Jan 18 14:08:32 2007 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,250 +0,0 @@ -# Copyright (c) 2003-2006 Ralph Meijer -# See LICENSE for details. - -from zope.interface import Interface -from twisted.words.protocols.jabber import jid -from twisted.words.xish import domish - -class Error(Exception): - msg = None - -class NodeNotFound(Error): - pass - -class NodeExists(Error): - pass - -class SubscriptionNotFound(Error): - pass - -class SubscriptionExists(Error): - pass - -class IStorage(Interface): - """ Storage interface """ - - def get_node(self, node_id): - """ Get Node. - - @param node_id: NodeID of the desired node. - @type node_id: L{str} - @return: deferred that returns a L{Node} object. - """ - - def get_node_ids(self): - """ Return all NodeIDs. - - @return: deferred that returns a list of NodeIDs (L{str}). - """ - - def create_node(self, node_id, owner, config = None, type='leaf'): - """ Create new node. - - The implementation should make sure, the passed owner JID is stripped - of the resource (e.g. using C{owner.userhostJID()}). - - @param node_id: NodeID of the new node. - @type node_id: L{str} - @param owner: JID of the new nodes's owner. - @type owner: L{jid.JID} - @param config: Configuration - @param type: Node type. Can be either C{'leaf'} or C{'collection'}. - @return: deferred that fires on creation. - """ - - def delete_node(self, node_id): - """ Delete a node. - - @param node_id: NodeID of the new node. - @type node_id: L{str} - @return: deferred that fires on deletion. - """ - - def get_affiliations(self, entity): - """ Get all affiliations for entity. - - The implementation should make sure, the passed owner JID is stripped - of the resource (e.g. using C{owner.userhostJID()}). - - @param entity: JID of the entity. - @type entity: L{jid.JID} - @return: deferred that returns a L{list} of tuples of the form - C{(node_id, affiliation)}, where C{node_id} is of the type - L{str} and C{affiliation} is one of C{'owner'}, C{'publisher'} - and C{'outcast'}. - """ - - def get_subscriptions(self, entity): - """ Get all subscriptions for an entity. - - The implementation should make sure, the passed owner JID is stripped - of the resource (e.g. using C{owner.userhostJID()}). - - @param entity: JID of the entity. - @type entity: L{jid.JID} - @return: deferred that returns a L{list} of tuples of the form - C{(node_id, subscriber, state)}, where C{node_id} is of the - type L{str}, C{subscriber} of the type {jid.JID}, and - C{state} is C{'subscribed'} or C{'pending'}. - """ - - -class INode(Interface): - """ Interface to the class of objects that represent nodes. """ - - def get_type(self): - """ Get node's type. - - @return: C{'leaf'} or C{'collection'}. - """ - - def get_configuration(self): - """ Get node's configuration. - - The configuration must at least have two options: - C{pubsub#persist_items}, and C{pubsub#deliver_payloads}. - - @return: L{dict} of configuration options. - """ - - def get_meta_data(self): - """ Get node's meta data. - - The meta data must be a superset of the configuration options, and - also at least should have a C{pubsub#node_type} entry. - - @return: L{dict} of meta data. - """ - - def set_configuration(self, options): - """ Set node's configuration. - - The elements of {options} will set the new values for those - configuration items. This means that only changing items have to - be given. - - @param options: a dictionary of configuration options. - @returns: a deferred that fires upon success. - """ - - def get_affiliation(self, entity): - """ Get affiliation of entity with this node. - - @param entity: JID of entity. - @type entity: L{jid.JID} - @return: deferred that returns C{'owner'}, C{'publisher'}, C{'outcast'} - or C{None}. - """ - - def get_subscription(self, subscriber): - """ Get subscription to this node of subscriber. - - @param subscriber: JID of the new subscriptions' entity. - @type subscriber: L{jid.JID} - @return: deferred that returns the subscription state (C{'subscribed'}, - C{'pending'} or C{None}). - """ - - def add_subscription(self, subscriber, state): - """ Add new subscription to this node with given state. - - @param subscriber: JID of the new subscriptions' entity. - @type subscriber: L{jid.JID} - @param state: C{'subscribed'} or C{'pending'} - @type state: L{str} - @return: deferred that fires on subscription. - """ - - def remove_subscription(self, subscriber): - """ Remove subscription to this node. - - @param subscriber: JID of the subscriptions' entity. - @type subscriber: L{jid.JID} - @return: deferred that fires on removal. - """ - - def get_subscribers(self): - """ Get list of subscribers to this node. - - Retrieves the list of entities that have a subscription to this - node. That is, having the state C{'subscribed'}. - - @return: a deferred that returns a L{list} of L{jid.JID}s. - """ - - def is_subscribed(self, entity): - """ Returns whether entity has any subscription to this node. - - Only returns C{True} when the subscription state (if present) is - C{'subscribed'} for any subscription that matches the bare JID. - - @param subscriber: bare JID of the subscriptions' entity. - @type subscriber: L{jid.JID} - @return: deferred that returns a L{bool}. - """ - - def get_affiliations(self): - """ Get affiliations of entities with this node. - - @return: deferred that returns a L{list} of tuples (jid, affiliation), - where jid is a L(jid.JID) and affiliation is one of C{'owner'}, - C{'publisher'}, C{'outcast'}. - """ - -class ILeafNode(Interface): - """ Interface to the class of objects that represent leaf nodes. """ - - def store_items(self, items, publisher): - """ Store items in persistent storage for later retrieval. - - @param items: The list of items to be stored. Each item is the - L{domish} representation of the XML fragment as defined - for C{} in the - C{http://jabber.org/protocol/pubsub} namespace. - @type items: L{list} of {domish.Element} - @param publisher: JID of the publishing entity. - @type publisher: L{jid.JID} - @return: deferred that fires upon success. - """ - - def remove_items(self, item_ids): - """ Remove items by id - - @param item_ids: L{list} of item ids. - @return: deferred that fires with a L{list} of ids of the items that - were deleted - """ - - def get_items(self, max_items=None): - """ Get items. - - If C{max_items} is not given, all items in the node are returned, - just like C{get_items_by_id}. Otherwise, C{max_items} limits - the returned items to a maximum of that number of most recently - published items. - - @param max_items: if given, a natural number (>0) that limits the - returned number of items. - @return: deferred that fires with a L{list} of found items. - """ - - def get_items_by_id(self, item_ids): - """ Get items by item id. - - Each item in the returned list is a unicode string that - represent the XML of the item as it was published, including the - item wrapper with item id. - - @param item_ids: L{list} of item ids. - @return: deferred that fires with a L{list} of found items. - """ - - def purge(self): - """ Purge node of all items in persistent storage. - - @return: deferred that fires when the node has been purged. - """ - - -class ISubscription(Interface): - """ """ diff -r 5abb017bd687 -r ef22e4150caa idavoll/tap.py --- a/idavoll/tap.py Thu Jan 18 14:08:32 2007 +0000 +++ b/idavoll/tap.py Wed Oct 03 12:41:43 2007 +0000 @@ -1,10 +1,18 @@ -# Copyright (c) 2003-2006 Ralph Meijer +# Copyright (c) 2003-2007 Ralph Meijer # See LICENSE for details. -from twisted.application import internet, service -from twisted.internet import interfaces +from twisted.application import service from twisted.python import usage -import idavoll +from twisted.words.protocols.jabber.jid import JID + +from wokkel.component import Component +from wokkel.disco import DiscoHandler +from wokkel.generic import FallbackHandler, VersionHandler +from wokkel.iwokkel import IPubSubService + +from idavoll.backend import BackendService + +__version__ = '0.6.0' class Options(usage.Options): optParameters = [ @@ -22,10 +30,42 @@ ('verbose', 'v', 'Show traffic'), ('hide-nodes', None, 'Hide all nodes for disco') ] - + def postOptions(self): if self['backend'] not in ['pgsql', 'memory']: raise usage.UsageError, "Unknown backend!" def makeService(config): - return idavoll.makeService(config) + s = service.MultiService() + + cs = Component(config["rhost"], int(config["rport"]), + config["jid"], config["secret"]) + cs.setServiceParent(s) + + cs.factory.maxDelay = 900 + + if config["verbose"]: + cs.logTraffic = True + + FallbackHandler().setHanderParent(cs) + VersionHandler('Idavoll', __version__).setHanderParent(cs) + DiscoHandler().setHanderParent(cs) + + if config['backend'] == 'pgsql': + from idavoll.pgsql_storage import Storage + st = Storage(user=config['dbuser'], + database=config['dbname'], + password=config['dbpass']) + elif config['backend'] == 'memory': + from idavoll.memory_storage import Storage + st = Storage() + + bs = BackendService(st) + bs.setServiceParent(s) + + ps = IPubSubService(bs) + ps.setHanderParent(cs) + ps.hideNodes = config["hide-nodes"] + ps.serviceJID = JID(config["jid"]) + + return s diff -r 5abb017bd687 -r ef22e4150caa idavoll/test/__init__.py --- a/idavoll/test/__init__.py Thu Jan 18 14:08:32 2007 +0000 +++ b/idavoll/test/__init__.py Wed Oct 03 12:41:43 2007 +0000 @@ -1,3 +1,6 @@ -# Copyright (c) 2003-2006 Ralph Meijer +# Copyright (c) 2003-2007 Ralph Meijer # See LICENSE for details. +""" +Tests for L{idavoll}. +""" diff -r 5abb017bd687 -r ef22e4150caa idavoll/test/test_backend.py --- a/idavoll/test/test_backend.py Thu Jan 18 14:08:32 2007 +0000 +++ b/idavoll/test/test_backend.py Wed Oct 03 12:41:43 2007 +0000 @@ -1,12 +1,16 @@ -# Copyright (c) 2003-2006 Ralph Meijer +# Copyright (c) 2003-2007 Ralph Meijer # See LICENSE for details. +""" +Tests for L{idavoll.backend}. +""" + from twisted.trial import unittest from zope.interface import implements from twisted.internet import defer from twisted.words.protocols.jabber import jid -from idavoll import backend, storage +from idavoll import backend, error, iidavoll OWNER = jid.JID('owner@example.com') @@ -18,25 +22,26 @@ class testStorage: - implements(storage.IStorage) + implements(iidavoll.IStorage) def get_node(self, node_id): return defer.succeed(testNode()) def delete_node(self, node_id): if node_id in ['to-be-deleted']: - self.backend.delete_called = True + self.delete_called = True return defer.succeed(None) else: - return defer.fail(storage.NodeNotFound()) + return defer.fail(error.NodeNotFound()) -class NodeDeletionServiceTests: - pre_delete_called = False - delete_called = False +class BackendTest(unittest.TestCase): + def setUp(self): + self.storage = testStorage() + self.backend = backend.BackendService(self.storage) + self.storage.backend = self.backend - def setUpClass(self): - self.storage = testStorage() - self.storage.backend = self + self.pre_delete_called = False + self.delete_called = False def testDeleteNode(self): def pre_delete(node_id): @@ -44,19 +49,10 @@ return defer.succeed(None) def cb(result): - self.assert_(self.pre_delete_called) - self.assert_(self.delete_called) + self.assertTrue(self.pre_delete_called) + self.assertTrue(self.storage.delete_called) self.backend.register_pre_delete(pre_delete) d = self.backend.delete_node('to-be-deleted', OWNER) d.addCallback(cb) - -class GenericNodeDeletionServiceTestCase(unittest.TestCase, - NodeDeletionServiceTests): - - def setUpClass(self): - NodeDeletionServiceTests.setUpClass(self) - from idavoll.generic_backend import BackendService, NodeDeletionService - bs = BackendService(self.storage) - self.backend = NodeDeletionService() - self.backend.setServiceParent(bs) + return d diff -r 5abb017bd687 -r ef22e4150caa idavoll/test/test_storage.py --- a/idavoll/test/test_storage.py Thu Jan 18 14:08:32 2007 +0000 +++ b/idavoll/test/test_storage.py Wed Oct 03 12:41:43 2007 +0000 @@ -1,12 +1,18 @@ -# Copyright (c) 2003-2006 Ralph Meijer +# Copyright (c) 2003-2007 Ralph Meijer # See LICENSE for details. +""" +Tests for L{idavoll.memory_storage} and L{idavoll.pgsql_storage}. +""" + from twisted.trial import unittest from twisted.words.protocols.jabber import jid from twisted.internet import defer from twisted.words.xish import domish -from idavoll import storage, pubsub +from wokkel import pubsub + +from idavoll import error OWNER = jid.JID('owner@example.com') SUBSCRIBER = jid.JID('subscriber@example.com/Home') @@ -38,7 +44,7 @@ def _assignTestNode(self, node): self.node = node - def setUpClass(self): + def setUp(self): d = self.s.get_node('pre-existing') d.addCallback(self._assignTestNode) return d @@ -48,7 +54,7 @@ def testGetNonExistingNode(self): d = self.s.get_node('non-existing') - self.assertFailure(d, storage.NodeNotFound) + self.assertFailure(d, error.NodeNotFound) return d def testGetNodeIDs(self): @@ -60,7 +66,7 @@ def testCreateExistingNode(self): d = self.s.create_node('pre-existing', OWNER) - self.assertFailure(d, storage.NodeExists) + self.assertFailure(d, error.NodeExists) return d def testCreateNode(self): @@ -74,13 +80,13 @@ def testDeleteNonExistingNode(self): d = self.s.delete_node('non-existing') - self.assertFailure(d, storage.NodeNotFound) + self.assertFailure(d, error.NodeNotFound) return d def testDeleteNode(self): def cb(void): d = self.s.get_node('to-be-deleted') - self.assertFailure(d, storage.NodeNotFound) + self.assertFailure(d, error.NodeNotFound) return d d = self.s.delete_node('to-be-deleted') @@ -98,7 +104,7 @@ def testGetSubscriptions(self): def cb(subscriptions): self.assertIn(('pre-existing', SUBSCRIBER, 'subscribed'), subscriptions) - + d = self.s.get_subscriptions(SUBSCRIBER) d.addCallback(cb) return d @@ -124,7 +130,7 @@ def check_object_config(node): config = node.get_configuration() self.assertEqual(config['pubsub#persist_items'], False) - + def get_node(void): return self.s.get_node('to-be-reconfigured') @@ -177,9 +183,9 @@ def testAddExistingSubscription(self): d = self.node.add_subscription(SUBSCRIBER, 'pending') - self.assertFailure(d, storage.SubscriptionExists) + self.assertFailure(d, error.SubscriptionExists) return d - + def testGetSubscription(self): def cb(subscriptions): self.assertEquals(subscriptions[0][1], 'subscribed') @@ -197,9 +203,9 @@ def testRemoveNonExistingSubscription(self): d = self.node.remove_subscription(OWNER) - self.assertFailure(d, storage.SubscriptionNotFound) + self.assertFailure(d, error.SubscriptionNotFound) return d - + def testGetSubscribers(self): def cb(subscribers): self.assertIn(SUBSCRIBER, subscribers) @@ -309,7 +315,7 @@ def cb2(node): return node.get_items() - + def cb3(result): self.assertEqual([], result) @@ -326,7 +332,7 @@ def cb2(affiliations): affiliations = dict(((a[0].full(), a[1]) for a in affiliations)) self.assertEquals(affiliations[OWNER.full()], 'owner') - + d = self.s.get_node('pre-existing') d.addCallback(cb1) d.addCallback(cb2) @@ -334,7 +340,7 @@ class MemoryStorageStorageTestCase(unittest.TestCase, StorageTests): - def setUpClass(self): + def setUp(self): from idavoll.memory_storage import Storage, LeafNode, Subscription, \ default_config self.s = Storage() @@ -363,18 +369,18 @@ self.s._nodes['pre-existing']._items['current'] = item self.s._nodes['pre-existing']._itemlist.append(item) - return StorageTests.setUpClass(self) + return StorageTests.setUp(self) class PgsqlStorageStorageTestCase(unittest.TestCase, StorageTests): - def _callSuperSetUpClass(self, void): - return StorageTests.setUpClass(self) + def _callSuperSetUp(self, void): + return StorageTests.setUp(self) - def setUpClass(self): + def setUp(self): from idavoll.pgsql_storage import Storage self.s = Storage('ralphm', 'pubsub_test') self.s._dbpool.start() d = self.s._dbpool.runInteraction(self.init) - d.addCallback(self._callSuperSetUpClass) + d.addCallback(self._callSuperSetUp) return d def tearDownClass(self): @@ -444,7 +450,7 @@ WHERE node='pre-existing'""", (PUBLISHER.userhost(), ITEM.toXml())) - + def cleandb(self, cursor): cursor.execute("""DELETE FROM nodes WHERE node in ('non-existing', 'pre-existing', 'to-be-deleted', @@ -460,3 +466,9 @@ SUBSCRIBER_PENDING.userhost()) cursor.execute("""DELETE FROM entities WHERE jid=%s""", PUBLISHER.userhost()) + +try: + import pyPgSQL + pyPgSQL +except ImportError: + PgsqlStorageStorageTestCase.skip = "pyPgSQL not available"