source: wokkel/pubsub.py @ 99:2c8dc93fbef4

Last change on this file since 99:2c8dc93fbef4 was 97:f11253bd8b69, checked in by Ralph Meijer <ralphm@…>, 11 years ago

Make sure item and subscription elements get the correct namespace.

This complements the changes in [f4aa0d507bc8] and [3fe44cf07366] for item and
subscription elements that are used in different namespaces. These changesets
set the namespace to None, which works nicely when the elements are
serialized. However, in situations where the DOM representation is passed
around (in-process communication) these elements are no longer matched by
namespace. This change makes sure the elements get the proper namespace just
before they are sent out.

  • Property exe set to *
File size: 49.2 KB
Line 
1# -*- test-case-name: wokkel.test.test_pubsub -*-
2#
3# Copyright (c) Ralph Meijer.
4# See LICENSE for details.
5
6"""
7XMPP publish-subscribe protocol.
8
9This protocol is specified in
10U{XEP-0060<http://www.xmpp.org/extensions/xep-0060.html>}.
11"""
12
13from zope.interface import implements
14
15from twisted.internet import defer
16from twisted.python import log
17from twisted.words.protocols.jabber import jid, error
18from twisted.words.xish import domish
19
20from wokkel import disco, data_form, generic, shim
21from wokkel.compat import IQ
22from wokkel.subprotocols import IQHandlerMixin, XMPPHandler
23from wokkel.iwokkel import IPubSubClient, IPubSubService, IPubSubResource
24
25# Iq get and set XPath queries
26IQ_GET = '/iq[@type="get"]'
27IQ_SET = '/iq[@type="set"]'
28
29# Publish-subscribe namespaces
30NS_PUBSUB = 'http://jabber.org/protocol/pubsub'
31NS_PUBSUB_EVENT = NS_PUBSUB + '#event'
32NS_PUBSUB_ERRORS = NS_PUBSUB + '#errors'
33NS_PUBSUB_OWNER = NS_PUBSUB + "#owner"
34NS_PUBSUB_NODE_CONFIG = NS_PUBSUB + "#node_config"
35NS_PUBSUB_META_DATA = NS_PUBSUB + "#meta-data"
36NS_PUBSUB_SUBSCRIBE_OPTIONS = NS_PUBSUB + "#subscribe_options"
37
38# XPath to match pubsub requests
39PUBSUB_REQUEST = '/iq[@type="get" or @type="set"]/' + \
40                    'pubsub[@xmlns="' + NS_PUBSUB + '" or ' + \
41                           '@xmlns="' + NS_PUBSUB_OWNER + '"]'
42
43class SubscriptionPending(Exception):
44    """
45    Raised when the requested subscription is pending acceptance.
46    """
47
48
49
50class SubscriptionUnconfigured(Exception):
51    """
52    Raised when the requested subscription needs to be configured before
53    becoming active.
54    """
55
56
57
58class PubSubError(error.StanzaError):
59    """
60    Exception with publish-subscribe specific condition.
61    """
62    def __init__(self, condition, pubsubCondition, feature=None, text=None):
63        appCondition = domish.Element((NS_PUBSUB_ERRORS, pubsubCondition))
64        if feature:
65            appCondition['feature'] = feature
66        error.StanzaError.__init__(self, condition,
67                                         text=text,
68                                         appCondition=appCondition)
69
70
71
72class BadRequest(error.StanzaError):
73    """
74    Bad request stanza error.
75    """
76    def __init__(self, pubsubCondition=None, text=None):
77        if pubsubCondition:
78            appCondition = domish.Element((NS_PUBSUB_ERRORS, pubsubCondition))
79        else:
80            appCondition = None
81        error.StanzaError.__init__(self, 'bad-request',
82                                         text=text,
83                                         appCondition=appCondition)
84
85
86
87class Unsupported(PubSubError):
88    def __init__(self, feature, text=None):
89        self.feature = feature
90        PubSubError.__init__(self, 'feature-not-implemented',
91                                   'unsupported',
92                                   feature,
93                                   text)
94
95    def __str__(self):
96        message = PubSubError.__str__(self)
97        message += ', feature %r' % self.feature
98        return message
99
100
101class Subscription(object):
102    """
103    A subscription to a node.
104
105    @ivar nodeIdentifier: The identifier of the node subscribed to.  The root
106        node is denoted by C{None}.
107    @type nodeIdentifier: C{unicode}
108
109    @ivar subscriber: The subscribing entity.
110    @type subscriber: L{jid.JID}
111
112    @ivar state: The subscription state. One of C{'subscribed'}, C{'pending'},
113                 C{'unconfigured'}.
114    @type state: C{unicode}
115
116    @ivar options: Optional list of subscription options.
117    @type options: C{dict}
118
119    @ivar subscriptionIdentifier: Optional subscription identifier.
120    @type subscriptionIdentifier: C{unicode}
121    """
122
123    def __init__(self, nodeIdentifier, subscriber, state, options=None,
124                       subscriptionIdentifier=None):
125        self.nodeIdentifier = nodeIdentifier
126        self.subscriber = subscriber
127        self.state = state
128        self.options = options or {}
129        self.subscriptionIdentifier = subscriptionIdentifier
130
131
132    @staticmethod
133    def fromElement(element):
134        return Subscription(
135                element.getAttribute('node'),
136                jid.JID(element.getAttribute('jid')),
137                element.getAttribute('subscription'),
138                subscriptionIdentifier=element.getAttribute('subid'))
139
140
141    def toElement(self, defaultUri=None):
142        """
143        Return the DOM representation of this subscription.
144
145        @rtype: L{domish.Element}
146        """
147        element = domish.Element((defaultUri, 'subscription'))
148        if self.nodeIdentifier:
149            element['node'] = self.nodeIdentifier
150        element['jid'] = unicode(self.subscriber)
151        element['subscription'] = self.state
152        if self.subscriptionIdentifier:
153            element['subid'] = self.subscriptionIdentifier
154        return element
155
156
157
158class Item(domish.Element):
159    """
160    Publish subscribe item.
161
162    This behaves like an object providing L{domish.IElement}.
163
164    Item payload can be added using C{addChild} or C{addRawXml}, or using the
165    C{payload} keyword argument to C{__init__}.
166    """
167
168    def __init__(self, id=None, payload=None):
169        """
170        @param id: optional item identifier
171        @type id: L{unicode}
172        @param payload: optional item payload. Either as a domish element, or
173                        as serialized XML.
174        @type payload: object providing L{domish.IElement} or L{unicode}.
175        """
176
177        domish.Element.__init__(self, (None, 'item'))
178        if id is not None:
179            self['id'] = id
180        if payload is not None:
181            if isinstance(payload, basestring):
182                self.addRawXml(payload)
183            else:
184                self.addChild(payload)
185
186
187
188class PubSubRequest(generic.Stanza):
189    """
190    A publish-subscribe request.
191
192    The set of instance variables used depends on the type of request. If
193    a variable is not applicable or not passed in the request, its value is
194    C{None}.
195
196    @ivar verb: The type of publish-subscribe request. See L{_requestVerbMap}.
197    @type verb: C{str}.
198
199    @ivar affiliations: Affiliations to be modified.
200    @type affiliations: C{set}
201    @ivar items: The items to be published, as L{domish.Element}s.
202    @type items: C{list}
203    @ivar itemIdentifiers: Identifiers of the items to be retrieved or
204                           retracted.
205    @type itemIdentifiers: C{set}
206    @ivar maxItems: Maximum number of items to retrieve.
207    @type maxItems: C{int}.
208    @ivar nodeIdentifier: Identifier of the node the request is about.
209    @type nodeIdentifier: C{unicode}
210    @ivar nodeType: The type of node that should be created, or for which the
211                    configuration is retrieved. C{'leaf'} or C{'collection'}.
212    @type nodeType: C{str}
213    @ivar options: Configurations options for nodes, subscriptions and publish
214                   requests.
215    @type options: L{data_form.Form}
216    @ivar subscriber: The subscribing entity.
217    @type subscriber: L{JID}
218    @ivar subscriptionIdentifier: Identifier for a specific subscription.
219    @type subscriptionIdentifier: C{unicode}
220    @ivar subscriptions: Subscriptions to be modified, as a set of
221                         L{Subscription}.
222    @type subscriptions: C{set}
223    @ivar affiliations: Affiliations to be modified, as a dictionary of entity
224                        (L{JID} to affiliation (C{unicode}).
225    @type affiliations: C{dict}
226    """
227
228    verb = None
229
230    affiliations = None
231    items = None
232    itemIdentifiers = None
233    maxItems = None
234    nodeIdentifier = None
235    nodeType = None
236    options = None
237    subscriber = None
238    subscriptionIdentifier = None
239    subscriptions = None
240    affiliations = None
241
242    # Map request iq type and subelement name to request verb
243    _requestVerbMap = {
244        ('set', NS_PUBSUB, 'publish'): 'publish',
245        ('set', NS_PUBSUB, 'subscribe'): 'subscribe',
246        ('set', NS_PUBSUB, 'unsubscribe'): 'unsubscribe',
247        ('get', NS_PUBSUB, 'options'): 'optionsGet',
248        ('set', NS_PUBSUB, 'options'): 'optionsSet',
249        ('get', NS_PUBSUB, 'subscriptions'): 'subscriptions',
250        ('get', NS_PUBSUB, 'affiliations'): 'affiliations',
251        ('set', NS_PUBSUB, 'create'): 'create',
252        ('get', NS_PUBSUB_OWNER, 'default'): 'default',
253        ('get', NS_PUBSUB_OWNER, 'configure'): 'configureGet',
254        ('set', NS_PUBSUB_OWNER, 'configure'): 'configureSet',
255        ('get', NS_PUBSUB, 'items'): 'items',
256        ('set', NS_PUBSUB, 'retract'): 'retract',
257        ('set', NS_PUBSUB_OWNER, 'purge'): 'purge',
258        ('set', NS_PUBSUB_OWNER, 'delete'): 'delete',
259        ('get', NS_PUBSUB_OWNER, 'affiliations'): 'affiliationsGet',
260        ('set', NS_PUBSUB_OWNER, 'affiliations'): 'affiliationsSet',
261        ('get', NS_PUBSUB_OWNER, 'subscriptions'): 'subscriptionsGet',
262        ('set', NS_PUBSUB_OWNER, 'subscriptions'): 'subscriptionsSet',
263    }
264
265    # Map request verb to request iq type and subelement name
266    _verbRequestMap = dict(((v, k) for k, v in _requestVerbMap.iteritems()))
267
268    # Map request verb to parameter handler names
269    _parameters = {
270        'publish': ['node', 'items'],
271        'subscribe': ['nodeOrEmpty', 'jid', 'optionsWithSubscribe'],
272        'unsubscribe': ['nodeOrEmpty', 'jid', 'subidOrNone'],
273        'optionsGet': ['nodeOrEmpty', 'jid', 'subidOrNone'],
274        'optionsSet': ['nodeOrEmpty', 'jid', 'options', 'subidOrNone'],
275        'subscriptions': [],
276        'affiliations': [],
277        'create': ['nodeOrNone', 'configureOrNone'],
278        'default': ['default'],
279        'configureGet': ['nodeOrEmpty'],
280        'configureSet': ['nodeOrEmpty', 'configure'],
281        'items': ['node', 'maxItems', 'itemIdentifiers', 'subidOrNone'],
282        'retract': ['node', 'itemIdentifiers'],
283        'purge': ['node'],
284        'delete': ['node'],
285        'affiliationsGet': ['nodeOrEmpty'],
286        'affiliationsSet': ['nodeOrEmpty', 'affiliations'],
287        'subscriptionsGet': ['nodeOrEmpty'],
288        'subscriptionsSet': [],
289    }
290
291    def __init__(self, verb=None):
292        self.verb = verb
293
294
295    def _parse_node(self, verbElement):
296        """
297        Parse the required node identifier out of the verbElement.
298        """
299        try:
300            self.nodeIdentifier = verbElement["node"]
301        except KeyError:
302            raise BadRequest('nodeid-required')
303
304
305    def _render_node(self, verbElement):
306        """
307        Render the required node identifier on the verbElement.
308        """
309        if not self.nodeIdentifier:
310            raise Exception("Node identifier is required")
311
312        verbElement['node'] = self.nodeIdentifier
313
314
315    def _parse_nodeOrEmpty(self, verbElement):
316        """
317        Parse the node identifier out of the verbElement. May be empty.
318        """
319        self.nodeIdentifier = verbElement.getAttribute("node", '')
320
321
322    def _render_nodeOrEmpty(self, verbElement):
323        """
324        Render the node identifier on the verbElement. May be empty.
325        """
326        if self.nodeIdentifier:
327            verbElement['node'] = self.nodeIdentifier
328
329
330    def _parse_nodeOrNone(self, verbElement):
331        """
332        Parse the optional node identifier out of the verbElement.
333        """
334        self.nodeIdentifier = verbElement.getAttribute("node")
335
336
337    def _render_nodeOrNone(self, verbElement):
338        """
339        Render the optional node identifier on the verbElement.
340        """
341        if self.nodeIdentifier:
342            verbElement['node'] = self.nodeIdentifier
343
344
345    def _parse_items(self, verbElement):
346        """
347        Parse items out of the verbElement for publish requests.
348        """
349        self.items = []
350        for element in verbElement.elements():
351            if element.uri == NS_PUBSUB and element.name == 'item':
352                self.items.append(element)
353
354
355    def _render_items(self, verbElement):
356        """
357        Render items into the verbElement for publish requests.
358        """
359        if self.items:
360            for item in self.items:
361                item.uri = NS_PUBSUB
362                verbElement.addChild(item)
363
364
365    def _parse_jid(self, verbElement):
366        """
367        Parse subscriber out of the verbElement for un-/subscribe requests.
368        """
369        try:
370            self.subscriber = jid.internJID(verbElement["jid"])
371        except KeyError:
372            raise BadRequest('jid-required')
373
374
375    def _render_jid(self, verbElement):
376        """
377        Render subscriber into the verbElement for un-/subscribe requests.
378        """
379        verbElement['jid'] = self.subscriber.full()
380
381
382    def _parse_default(self, verbElement):
383        """
384        Parse node type out of a request for the default node configuration.
385        """
386        form = data_form.findForm(verbElement, NS_PUBSUB_NODE_CONFIG)
387        if form and form.formType == 'submit':
388            values = form.getValues()
389            self.nodeType = values.get('pubsub#node_type', 'leaf')
390        else:
391            self.nodeType = 'leaf'
392
393
394    def _parse_configure(self, verbElement):
395        """
396        Parse options out of a request for setting the node configuration.
397        """
398        form = data_form.findForm(verbElement, NS_PUBSUB_NODE_CONFIG)
399        if form:
400            if form.formType in ('submit', 'cancel'):
401                self.options = form
402            else:
403                raise BadRequest(text=u"Unexpected form type '%s'" % form.formType)
404        else:
405            raise BadRequest(text="Missing configuration form")
406
407
408    def _parse_configureOrNone(self, verbElement):
409        """
410        Parse optional node configuration form in create request.
411        """
412        for element in verbElement.parent.elements():
413            if element.uri == NS_PUBSUB and element.name == 'configure':
414                form = data_form.findForm(element, NS_PUBSUB_NODE_CONFIG)
415                if form:
416                    if form.formType != 'submit':
417                        raise BadRequest(text=u"Unexpected form type '%s'" %
418                                              form.formType)
419                else:
420                    form = data_form.Form('submit',
421                                          formNamespace=NS_PUBSUB_NODE_CONFIG)
422                self.options = form
423
424
425    def _render_configureOrNone(self, verbElement):
426        """
427        Render optional node configuration form in create request.
428        """
429        if self.options is not None:
430            configure = verbElement.parent.addElement('configure')
431            configure.addChild(self.options.toElement())
432
433
434    def _parse_itemIdentifiers(self, verbElement):
435        """
436        Parse item identifiers out of items and retract requests.
437        """
438        self.itemIdentifiers = []
439        for element in verbElement.elements():
440            if element.uri == NS_PUBSUB and element.name == 'item':
441                try:
442                    self.itemIdentifiers.append(element["id"])
443                except KeyError:
444                    raise BadRequest()
445
446
447    def _render_itemIdentifiers(self, verbElement):
448        """
449        Render item identifiers into items and retract requests.
450        """
451        if self.itemIdentifiers:
452            for itemIdentifier in self.itemIdentifiers:
453                item = verbElement.addElement('item')
454                item['id'] = itemIdentifier
455
456
457    def _parse_maxItems(self, verbElement):
458        """
459        Parse maximum items out of an items request.
460        """
461        value = verbElement.getAttribute('max_items')
462
463        if value:
464            try:
465                self.maxItems = int(value)
466            except ValueError:
467                raise BadRequest(text="Field max_items requires a positive " +
468                                      "integer value")
469
470
471    def _render_maxItems(self, verbElement):
472        """
473        Render maximum items into an items request.
474        """
475        if self.maxItems:
476            verbElement['max_items'] = unicode(self.maxItems)
477
478
479    def _parse_subidOrNone(self, verbElement):
480        """
481        Parse subscription identifier out of a request.
482        """
483        self.subscriptionIdentifier = verbElement.getAttribute("subid")
484
485
486    def _render_subidOrNone(self, verbElement):
487        """
488        Render subscription identifier into a request.
489        """
490        if self.subscriptionIdentifier:
491            verbElement['subid'] = self.subscriptionIdentifier
492
493
494    def _parse_options(self, verbElement):
495        """
496        Parse options form out of a subscription options request.
497        """
498        form = data_form.findForm(verbElement, NS_PUBSUB_SUBSCRIBE_OPTIONS)
499        if form:
500            if form.formType in ('submit', 'cancel'):
501                self.options = form
502            else:
503                raise BadRequest(text=u"Unexpected form type '%s'" % form.formType)
504        else:
505            raise BadRequest(text="Missing options form")
506
507
508
509    def _render_options(self, verbElement):
510        verbElement.addChild(self.options.toElement())
511
512
513    def _parse_optionsWithSubscribe(self, verbElement):
514        for element in verbElement.parent.elements():
515            if element.name == 'options' and element.uri == NS_PUBSUB:
516                form = data_form.findForm(element,
517                                          NS_PUBSUB_SUBSCRIBE_OPTIONS)
518                if form:
519                    if form.formType != 'submit':
520                        raise BadRequest(text=u"Unexpected form type '%s'" %
521                                              form.formType)
522                else:
523                    form = data_form.Form('submit',
524                                          formNamespace=NS_PUBSUB_SUBSCRIBE_OPTIONS)
525                self.options = form
526
527
528    def _render_optionsWithSubscribe(self, verbElement):
529        if self.options:
530            optionsElement = verbElement.parent.addElement('options')
531            self._render_options(optionsElement)
532
533
534    def _parse_affiliations(self, verbElement):
535        self.affiliations = {}
536        for element in verbElement.elements():
537            if (element.uri == NS_PUBSUB_OWNER and
538                element.name == 'affiliation'):
539                try:
540                    entity = jid.internJID(element['jid']).userhostJID()
541                except KeyError:
542                    raise BadRequest(text='Missing jid attribute')
543
544                if entity in self.affiliations:
545                    raise BadRequest(text='Multiple affiliations for an entity')
546
547                try:
548                    affiliation = element['affiliation']
549                except KeyError:
550                    raise BadRequest(text='Missing affiliation attribute')
551
552                self.affiliations[entity] = affiliation
553
554
555    def parseElement(self, element):
556        """
557        Parse the publish-subscribe verb and parameters out of a request.
558        """
559        generic.Stanza.parseElement(self, element)
560
561        verbs = []
562        verbElements = []
563        for child in element.pubsub.elements():
564            key = (self.stanzaType, child.uri, child.name)
565            try:
566                verb = self._requestVerbMap[key]
567            except KeyError:
568                continue
569
570            verbs.append(verb)
571            verbElements.append(child)
572
573        if not verbs:
574            raise NotImplementedError()
575
576        if len(verbs) > 1:
577            if 'optionsSet' in verbs and 'subscribe' in verbs:
578                self.verb = 'subscribe'
579                verbElement = verbElements[verbs.index('subscribe')]
580            else:
581                raise NotImplementedError()
582        else:
583            self.verb = verbs[0]
584            verbElement = verbElements[0]
585
586        for parameter in self._parameters[self.verb]:
587            getattr(self, '_parse_%s' % parameter)(verbElement)
588
589
590
591    def send(self, xs):
592        """
593        Send this request to its recipient.
594
595        This renders all of the relevant parameters for this specific
596        requests into an L{IQ}, and invoke its C{send} method.
597        This returns a deferred that fires upon reception of a response. See
598        L{IQ} for details.
599
600        @param xs: The XML stream to send the request on.
601        @type xs: L{xmlstream.XmlStream}
602        @rtype: L{defer.Deferred}.
603        """
604
605        try:
606            (self.stanzaType,
607             childURI,
608             childName) = self._verbRequestMap[self.verb]
609        except KeyError:
610            raise NotImplementedError()
611
612        iq = IQ(xs, self.stanzaType)
613        iq.addElement((childURI, 'pubsub'))
614        verbElement = iq.pubsub.addElement(childName)
615
616        if self.sender:
617            iq['from'] = self.sender.full()
618        if self.recipient:
619            iq['to'] = self.recipient.full()
620
621        for parameter in self._parameters[self.verb]:
622            getattr(self, '_render_%s' % parameter)(verbElement)
623
624        return iq.send()
625
626
627
628class PubSubEvent(object):
629    """
630    A publish subscribe event.
631
632    @param sender: The entity from which the notification was received.
633    @type sender: L{jid.JID}
634    @param recipient: The entity to which the notification was sent.
635    @type recipient: L{wokkel.pubsub.ItemsEvent}
636    @param nodeIdentifier: Identifier of the node the event pertains to.
637    @type nodeIdentifier: C{unicode}
638    @param headers: SHIM headers, see L{wokkel.shim.extractHeaders}.
639    @type headers: L{dict}
640    """
641
642    def __init__(self, sender, recipient, nodeIdentifier, headers):
643        self.sender = sender
644        self.recipient = recipient
645        self.nodeIdentifier = nodeIdentifier
646        self.headers = headers
647
648
649
650class ItemsEvent(PubSubEvent):
651    """
652    A publish-subscribe event that signifies new, updated and retracted items.
653
654    @param items: List of received items as domish elements.
655    @type items: C{list} of L{domish.Element}
656    """
657
658    def __init__(self, sender, recipient, nodeIdentifier, items, headers):
659        PubSubEvent.__init__(self, sender, recipient, nodeIdentifier, headers)
660        self.items = items
661
662
663
664class DeleteEvent(PubSubEvent):
665    """
666    A publish-subscribe event that signifies the deletion of a node.
667    """
668
669    redirectURI = None
670
671
672
673class PurgeEvent(PubSubEvent):
674    """
675    A publish-subscribe event that signifies the purging of a node.
676    """
677
678
679
680class PubSubClient(XMPPHandler):
681    """
682    Publish subscribe client protocol.
683    """
684
685    implements(IPubSubClient)
686
687    def connectionInitialized(self):
688        self.xmlstream.addObserver('/message/event[@xmlns="%s"]' %
689                                   NS_PUBSUB_EVENT, self._onEvent)
690
691
692    def _onEvent(self, message):
693        if message.getAttribute('type') == 'error':
694            return
695
696        try:
697            sender = jid.JID(message["from"])
698            recipient = jid.JID(message["to"])
699        except KeyError:
700            return
701
702        actionElement = None
703        for element in message.event.elements():
704            if element.uri == NS_PUBSUB_EVENT:
705                actionElement = element
706
707        if not actionElement:
708            return
709
710        eventHandler = getattr(self, "_onEvent_%s" % actionElement.name, None)
711
712        if eventHandler:
713            headers = shim.extractHeaders(message)
714            eventHandler(sender, recipient, actionElement, headers)
715            message.handled = True
716
717
718    def _onEvent_items(self, sender, recipient, action, headers):
719        nodeIdentifier = action["node"]
720
721        items = [element for element in action.elements()
722                         if element.name in ('item', 'retract')]
723
724        event = ItemsEvent(sender, recipient, nodeIdentifier, items, headers)
725        self.itemsReceived(event)
726
727
728    def _onEvent_delete(self, sender, recipient, action, headers):
729        nodeIdentifier = action["node"]
730        event = DeleteEvent(sender, recipient, nodeIdentifier, headers)
731        if action.redirect:
732            event.redirectURI = action.redirect.getAttribute('uri')
733        self.deleteReceived(event)
734
735
736    def _onEvent_purge(self, sender, recipient, action, headers):
737        nodeIdentifier = action["node"]
738        event = PurgeEvent(sender, recipient, nodeIdentifier, headers)
739        self.purgeReceived(event)
740
741
742    def itemsReceived(self, event):
743        pass
744
745
746    def deleteReceived(self, event):
747        pass
748
749
750    def purgeReceived(self, event):
751        pass
752
753
754    def createNode(self, service, nodeIdentifier=None, options=None,
755                         sender=None):
756        """
757        Create a publish subscribe node.
758
759        @param service: The publish subscribe service to create the node at.
760        @type service: L{JID}
761        @param nodeIdentifier: Optional suggestion for the id of the node.
762        @type nodeIdentifier: C{unicode}
763        @param options: Optional node configuration options.
764        @type options: C{dict}
765        """
766        request = PubSubRequest('create')
767        request.recipient = service
768        request.nodeIdentifier = nodeIdentifier
769        request.sender = sender
770
771        if options:
772            form = data_form.Form(formType='submit',
773                                  formNamespace=NS_PUBSUB_NODE_CONFIG)
774            form.makeFields(options)
775            request.options = form
776
777        def cb(iq):
778            try:
779                new_node = iq.pubsub.create["node"]
780            except AttributeError:
781                # the suggested node identifier was accepted
782                new_node = nodeIdentifier
783            return new_node
784
785        d = request.send(self.xmlstream)
786        d.addCallback(cb)
787        return d
788
789
790    def deleteNode(self, service, nodeIdentifier, sender=None):
791        """
792        Delete a publish subscribe node.
793
794        @param service: The publish subscribe service to delete the node from.
795        @type service: L{JID}
796        @param nodeIdentifier: The identifier of the node.
797        @type nodeIdentifier: C{unicode}
798        """
799        request = PubSubRequest('delete')
800        request.recipient = service
801        request.nodeIdentifier = nodeIdentifier
802        request.sender = sender
803        return request.send(self.xmlstream)
804
805
806    def subscribe(self, service, nodeIdentifier, subscriber,
807                        options=None, sender=None):
808        """
809        Subscribe to a publish subscribe node.
810
811        @param service: The publish subscribe service that keeps the node.
812        @type service: L{JID}
813
814        @param nodeIdentifier: The identifier of the node.
815        @type nodeIdentifier: C{unicode}
816
817        @param subscriber: The entity to subscribe to the node. This entity
818            will get notifications of new published items.
819        @type subscriber: L{JID}
820
821        @param options: Subscription options.
822        @type options: C{dict}
823
824        @return: Deferred that fires with L{Subscription} or errbacks with
825            L{SubscriptionPending} or L{SubscriptionUnconfigured}.
826        @rtype: L{defer.Deferred}
827        """
828        request = PubSubRequest('subscribe')
829        request.recipient = service
830        request.nodeIdentifier = nodeIdentifier
831        request.subscriber = subscriber
832        request.sender = sender
833
834        if options:
835            form = data_form.Form(formType='submit',
836                                  formNamespace=NS_PUBSUB_SUBSCRIBE_OPTIONS)
837            form.makeFields(options)
838            request.options = form
839
840        def cb(iq):
841            subscription = Subscription.fromElement(iq.pubsub.subscription)
842
843            if subscription.state == 'pending':
844                raise SubscriptionPending()
845            elif subscription.state == 'unconfigured':
846                raise SubscriptionUnconfigured()
847            else:
848                # we assume subscription == 'subscribed'
849                # any other value would be invalid, but that should have
850                # yielded a stanza error.
851                return subscription
852
853        d = request.send(self.xmlstream)
854        d.addCallback(cb)
855        return d
856
857
858    def unsubscribe(self, service, nodeIdentifier, subscriber,
859                          subscriptionIdentifier=None, sender=None):
860        """
861        Unsubscribe from a publish subscribe node.
862
863        @param service: The publish subscribe service that keeps the node.
864        @type service: L{JID}
865
866        @param nodeIdentifier: The identifier of the node.
867        @type nodeIdentifier: C{unicode}
868
869        @param subscriber: The entity to unsubscribe from the node.
870        @type subscriber: L{JID}
871
872        @param subscriptionIdentifier: Optional subscription identifier.
873        @type subscriptionIdentifier: C{unicode}
874        """
875        request = PubSubRequest('unsubscribe')
876        request.recipient = service
877        request.nodeIdentifier = nodeIdentifier
878        request.subscriber = subscriber
879        request.subscriptionIdentifier = subscriptionIdentifier
880        request.sender = sender
881        return request.send(self.xmlstream)
882
883
884    def publish(self, service, nodeIdentifier, items=None, sender=None):
885        """
886        Publish to a publish subscribe node.
887
888        @param service: The publish subscribe service that keeps the node.
889        @type service: L{JID}
890        @param nodeIdentifier: The identifier of the node.
891        @type nodeIdentifier: C{unicode}
892        @param items: Optional list of L{Item}s to publish.
893        @type items: C{list}
894        """
895        request = PubSubRequest('publish')
896        request.recipient = service
897        request.nodeIdentifier = nodeIdentifier
898        request.items = items
899        request.sender = sender
900        return request.send(self.xmlstream)
901
902
903    def items(self, service, nodeIdentifier, maxItems=None,
904              subscriptionIdentifier=None, sender=None):
905        """
906        Retrieve previously published items from a publish subscribe node.
907
908        @param service: The publish subscribe service that keeps the node.
909        @type service: L{JID}
910
911        @param nodeIdentifier: The identifier of the node.
912        @type nodeIdentifier: C{unicode}
913
914        @param maxItems: Optional limit on the number of retrieved items.
915        @type maxItems: C{int}
916
917        @param subscriptionIdentifier: Optional subscription identifier. In
918            case the node has been subscribed to multiple times, this narrows
919            the results to the specific subscription.
920        @type subscriptionIdentifier: C{unicode}
921        """
922        request = PubSubRequest('items')
923        request.recipient = service
924        request.nodeIdentifier = nodeIdentifier
925        if maxItems:
926            request.maxItems = str(int(maxItems))
927        request.subscriptionIdentifier = subscriptionIdentifier
928        request.sender = sender
929
930        def cb(iq):
931            items = []
932            for element in iq.pubsub.items.elements():
933                if element.uri == NS_PUBSUB and element.name == 'item':
934                    items.append(element)
935            return items
936
937        d = request.send(self.xmlstream)
938        d.addCallback(cb)
939        return d
940
941
942    def getOptions(self, service, nodeIdentifier, subscriber,
943                         subscriptionIdentifier=None, sender=None):
944        """
945        Get subscription options.
946
947        @param service: The publish subscribe service that keeps the node.
948        @type service: L{JID}
949
950        @param nodeIdentifier: The identifier of the node.
951        @type nodeIdentifier: C{unicode}
952
953        @param subscriber: The entity subscribed to the node.
954        @type subscriber: L{JID}
955
956        @param subscriptionIdentifier: Optional subscription identifier.
957        @type subscriptionIdentifier: C{unicode}
958
959        @rtype: L{data_form.Form}
960        """
961        request = PubSubRequest('optionsGet')
962        request.recipient = service
963        request.nodeIdentifier = nodeIdentifier
964        request.subscriber = subscriber
965        request.subscriptionIdentifier = subscriptionIdentifier
966        request.sender = sender
967
968        def cb(iq):
969            form = data_form.findForm(iq.pubsub.options,
970                                      NS_PUBSUB_SUBSCRIBE_OPTIONS)
971            form.typeCheck()
972            return form
973
974        d = request.send(self.xmlstream)
975        d.addCallback(cb)
976        return d
977
978
979    def setOptions(self, service, nodeIdentifier, subscriber,
980                         options, subscriptionIdentifier=None, sender=None):
981        """
982        Set subscription options.
983
984        @param service: The publish subscribe service that keeps the node.
985        @type service: L{JID}
986
987        @param nodeIdentifier: The identifier of the node.
988        @type nodeIdentifier: C{unicode}
989
990        @param subscriber: The entity subscribed to the node.
991        @type subscriber: L{JID}
992
993        @param options: Subscription options.
994        @type options: C{dict}.
995
996        @param subscriptionIdentifier: Optional subscription identifier.
997        @type subscriptionIdentifier: C{unicode}
998        """
999        request = PubSubRequest('optionsSet')
1000        request.recipient = service
1001        request.nodeIdentifier = nodeIdentifier
1002        request.subscriber = subscriber
1003        request.subscriptionIdentifier = subscriptionIdentifier
1004        request.sender = sender
1005
1006        form = data_form.Form(formType='submit',
1007                              formNamespace=NS_PUBSUB_SUBSCRIBE_OPTIONS)
1008        form.makeFields(options)
1009        request.options = form
1010
1011        d = request.send(self.xmlstream)
1012        return d
1013
1014
1015
1016class PubSubService(XMPPHandler, IQHandlerMixin):
1017    """
1018    Protocol implementation for a XMPP Publish Subscribe Service.
1019
1020    The word Service here is used as taken from the Publish Subscribe
1021    specification. It is the party responsible for keeping nodes and their
1022    subscriptions, and sending out notifications.
1023
1024    Methods from the L{IPubSubService} interface that are called as
1025    a result of an XMPP request may raise exceptions. Alternatively the
1026    deferred returned by these methods may have their errback called. These are
1027    handled as follows:
1028
1029     - If the exception is an instance of L{error.StanzaError}, an error
1030       response iq is returned.
1031     - Any other exception is reported using L{log.msg}. An error response
1032       with the condition C{internal-server-error} is returned.
1033
1034    The default implementation of said methods raises an L{Unsupported}
1035    exception and are meant to be overridden.
1036
1037    @ivar discoIdentity: Service discovery identity as a dictionary with
1038                         keys C{'category'}, C{'type'} and C{'name'}.
1039    @ivar pubSubFeatures: List of supported publish-subscribe features for
1040                          service discovery, as C{str}.
1041    @type pubSubFeatures: C{list} or C{None}
1042    """
1043
1044    implements(IPubSubService, disco.IDisco)
1045
1046    iqHandlers = {
1047            '/*': '_onPubSubRequest',
1048            }
1049
1050    _legacyHandlers = {
1051        'publish': ('publish', ['sender', 'recipient',
1052                                'nodeIdentifier', 'items']),
1053        'subscribe': ('subscribe', ['sender', 'recipient',
1054                                    'nodeIdentifier', 'subscriber']),
1055        'unsubscribe': ('unsubscribe', ['sender', 'recipient',
1056                                        'nodeIdentifier', 'subscriber']),
1057        'subscriptions': ('subscriptions', ['sender', 'recipient']),
1058        'affiliations': ('affiliations', ['sender', 'recipient']),
1059        'create': ('create', ['sender', 'recipient', 'nodeIdentifier']),
1060        'getConfigurationOptions': ('getConfigurationOptions', []),
1061        'default': ('getDefaultConfiguration',
1062                    ['sender', 'recipient', 'nodeType']),
1063        'configureGet': ('getConfiguration', ['sender', 'recipient',
1064                                              'nodeIdentifier']),
1065        'configureSet': ('setConfiguration', ['sender', 'recipient',
1066                                              'nodeIdentifier', 'options']),
1067        'items': ('items', ['sender', 'recipient', 'nodeIdentifier',
1068                            'maxItems', 'itemIdentifiers']),
1069        'retract': ('retract', ['sender', 'recipient', 'nodeIdentifier',
1070                                'itemIdentifiers']),
1071        'purge': ('purge', ['sender', 'recipient', 'nodeIdentifier']),
1072        'delete': ('delete', ['sender', 'recipient', 'nodeIdentifier']),
1073    }
1074
1075    hideNodes = False
1076
1077    def __init__(self, resource=None):
1078        self.resource = resource
1079        self.discoIdentity = {'category': 'pubsub',
1080                              'type': 'service',
1081                              'name': 'Generic Publish-Subscribe Service'}
1082
1083        self.pubSubFeatures = []
1084
1085
1086    def connectionMade(self):
1087        self.xmlstream.addObserver(PUBSUB_REQUEST, self.handleRequest)
1088
1089
1090    def getDiscoInfo(self, requestor, target, nodeIdentifier=''):
1091        def toInfo(nodeInfo):
1092            if not nodeInfo:
1093                return
1094
1095            (nodeType, metaData) = nodeInfo['type'], nodeInfo['meta-data']
1096            info.append(disco.DiscoIdentity('pubsub', nodeType))
1097            if metaData:
1098                form = data_form.Form(formType="result",
1099                                      formNamespace=NS_PUBSUB_META_DATA)
1100                form.addField(
1101                        data_form.Field(
1102                            var='pubsub#node_type',
1103                            value=nodeType,
1104                            label='The type of node (collection or leaf)'
1105                        )
1106                )
1107
1108                for metaDatum in metaData:
1109                    form.addField(data_form.Field.fromDict(metaDatum))
1110
1111                info.append(form)
1112
1113            return
1114
1115        info = []
1116
1117        request = PubSubRequest('discoInfo')
1118
1119        if self.resource is not None:
1120            resource = self.resource.locateResource(request)
1121            identity = resource.discoIdentity
1122            features = resource.features
1123            getInfo = resource.getInfo
1124        else:
1125            category = self.discoIdentity['category']
1126            idType = self.discoIdentity['type']
1127            name = self.discoIdentity['name']
1128            identity = disco.DiscoIdentity(category, idType, name)
1129            features = self.pubSubFeatures
1130            getInfo = self.getNodeInfo
1131
1132        if not nodeIdentifier:
1133            info.append(identity)
1134            info.append(disco.DiscoFeature(disco.NS_DISCO_ITEMS))
1135            info.extend([disco.DiscoFeature("%s#%s" % (NS_PUBSUB, feature))
1136                         for feature in features])
1137
1138        d = defer.maybeDeferred(getInfo, requestor, target, nodeIdentifier or '')
1139        d.addCallback(toInfo)
1140        d.addErrback(log.err)
1141        d.addCallback(lambda _: info)
1142        return d
1143
1144
1145    def getDiscoItems(self, requestor, target, nodeIdentifier=''):
1146        if self.hideNodes:
1147            d = defer.succeed([])
1148        elif self.resource is not None:
1149            request = PubSubRequest('discoInfo')
1150            resource = self.resource.locateResource(request)
1151            d = resource.getNodes(requestor, target, nodeIdentifier)
1152        elif nodeIdentifier:
1153            d = self.getNodes(requestor, target)
1154        else:
1155            d = defer.succeed([])
1156
1157        d.addCallback(lambda nodes: [disco.DiscoItem(target, node)
1158                                     for node in nodes])
1159        return d
1160
1161
1162    def _onPubSubRequest(self, iq):
1163        request = PubSubRequest.fromElement(iq)
1164
1165        if self.resource is not None:
1166            resource = self.resource.locateResource(request)
1167        else:
1168            resource = self
1169
1170        # Preprocess the request, knowing the handling resource
1171        try:
1172            preProcessor = getattr(self, '_preProcess_%s' % request.verb)
1173        except AttributeError:
1174            pass
1175        else:
1176            request = preProcessor(resource, request)
1177            if request is None:
1178                return defer.succeed(None)
1179
1180        # Process the request itself,
1181        if resource is not self:
1182            try:
1183                handler = getattr(resource, request.verb)
1184            except AttributeError:
1185                text = "Request verb: %s" % request.verb
1186                return defer.fail(Unsupported('', text))
1187
1188            d = handler(request)
1189        else:
1190            try:
1191                handlerName, argNames = self._legacyHandlers[request.verb]
1192            except KeyError:
1193                text = "Request verb: %s" % request.verb
1194                return defer.fail(Unsupported('', text))
1195
1196            handler = getattr(self, handlerName)
1197
1198            args = [getattr(request, arg) for arg in argNames]
1199            if 'options' in argNames:
1200                args[argNames.index('options')] = request.options.getValues()
1201
1202            d = handler(*args)
1203
1204        # If needed, translate the result into a response
1205        try:
1206            cb = getattr(self, '_toResponse_%s' % request.verb)
1207        except AttributeError:
1208            pass
1209        else:
1210            d.addCallback(cb, resource, request)
1211
1212        return d
1213
1214
1215    def _toResponse_subscribe(self, result, resource, request):
1216        response = domish.Element((NS_PUBSUB, "pubsub"))
1217        response.addChild(result.toElement(NS_PUBSUB))
1218        return response
1219
1220
1221    def _toResponse_subscriptions(self, result, resource, request):
1222        response = domish.Element((NS_PUBSUB, 'pubsub'))
1223        subscriptions = response.addElement('subscriptions')
1224        for subscription in result:
1225            subscriptions.addChild(subscription.toElement(NS_PUBSUB))
1226        return response
1227
1228
1229    def _toResponse_affiliations(self, result, resource, request):
1230        response = domish.Element((NS_PUBSUB, 'pubsub'))
1231        affiliations = response.addElement('affiliations')
1232
1233        for nodeIdentifier, affiliation in result:
1234            item = affiliations.addElement('affiliation')
1235            item['node'] = nodeIdentifier
1236            item['affiliation'] = affiliation
1237
1238        return response
1239
1240
1241    def _toResponse_create(self, result, resource, request):
1242        if not request.nodeIdentifier or request.nodeIdentifier != result:
1243            response = domish.Element((NS_PUBSUB, 'pubsub'))
1244            create = response.addElement('create')
1245            create['node'] = result
1246            return response
1247        else:
1248            return None
1249
1250
1251    def _formFromConfiguration(self, resource, values):
1252        fieldDefs = resource.getConfigurationOptions()
1253        form = data_form.Form(formType="form",
1254                              formNamespace=NS_PUBSUB_NODE_CONFIG)
1255        form.makeFields(values, fieldDefs)
1256        return form
1257
1258
1259    def _checkConfiguration(self, resource, form):
1260        fieldDefs = resource.getConfigurationOptions()
1261        form.typeCheck(fieldDefs, filterUnknown=True)
1262
1263
1264    def _preProcess_create(self, resource, request):
1265        if request.options:
1266            self._checkConfiguration(resource, request.options)
1267        return request
1268
1269
1270    def _preProcess_default(self, resource, request):
1271        if request.nodeType not in ('leaf', 'collection'):
1272            raise error.StanzaError('not-acceptable')
1273        else:
1274            return request
1275
1276
1277    def _toResponse_default(self, options, resource, request):
1278        response = domish.Element((NS_PUBSUB_OWNER, "pubsub"))
1279        default = response.addElement("default")
1280        form = self._formFromConfiguration(resource, options)
1281        default.addChild(form.toElement())
1282        return response
1283
1284
1285    def _toResponse_configureGet(self, options, resource, request):
1286        response = domish.Element((NS_PUBSUB_OWNER, "pubsub"))
1287        configure = response.addElement("configure")
1288        form = self._formFromConfiguration(resource, options)
1289        configure.addChild(form.toElement())
1290
1291        if request.nodeIdentifier:
1292            configure["node"] = request.nodeIdentifier
1293
1294        return response
1295
1296
1297    def _preProcess_configureSet(self, resource, request):
1298        if request.options.formType == 'cancel':
1299            return None
1300        else:
1301            self._checkConfiguration(resource, request.options)
1302            return request
1303
1304
1305    def _toResponse_items(self, result, resource, request):
1306        response = domish.Element((NS_PUBSUB, 'pubsub'))
1307        items = response.addElement('items')
1308        items["node"] = request.nodeIdentifier
1309
1310        for item in result:
1311            item.uri = NS_PUBSUB
1312            items.addChild(item)
1313
1314        return response
1315
1316
1317    def _createNotification(self, eventType, service, nodeIdentifier,
1318                                  subscriber, subscriptions=None):
1319        headers = []
1320
1321        if subscriptions:
1322            for subscription in subscriptions:
1323                if nodeIdentifier != subscription.nodeIdentifier:
1324                    headers.append(('Collection', subscription.nodeIdentifier))
1325
1326        message = domish.Element((None, "message"))
1327        message["from"] = service.full()
1328        message["to"] = subscriber.full()
1329        event = message.addElement((NS_PUBSUB_EVENT, "event"))
1330
1331        element = event.addElement(eventType)
1332        element["node"] = nodeIdentifier
1333
1334        if headers:
1335            message.addChild(shim.Headers(headers))
1336
1337        return message
1338
1339
1340    def _toResponse_affiliationsGet(self, result, resource, request):
1341        response = domish.Element((NS_PUBSUB_OWNER, 'pubsub'))
1342        affiliations = response.addElement('affiliations')
1343
1344        if request.nodeIdentifier:
1345            affiliations['node'] = request.nodeIdentifier
1346
1347        for entity, affiliation in result.iteritems():
1348            item = affiliations.addElement('affiliation')
1349            item['jid'] = entity.full()
1350            item['affiliation'] = affiliation
1351
1352        return response
1353
1354
1355    # public methods
1356
1357    def notifyPublish(self, service, nodeIdentifier, notifications):
1358        for subscriber, subscriptions, items in notifications:
1359            message = self._createNotification('items', service,
1360                                               nodeIdentifier, subscriber,
1361                                               subscriptions)
1362            for item in items:
1363                item.uri = NS_PUBSUB_EVENT
1364                message.event.items.addChild(item)
1365            self.send(message)
1366
1367
1368    def notifyDelete(self, service, nodeIdentifier, subscribers,
1369                           redirectURI=None):
1370        for subscriber in subscribers:
1371            message = self._createNotification('delete', service,
1372                                               nodeIdentifier,
1373                                               subscriber)
1374            if redirectURI:
1375                redirect = message.event.delete.addElement('redirect')
1376                redirect['uri'] = redirectURI
1377            self.send(message)
1378
1379
1380    def getNodeInfo(self, requestor, service, nodeIdentifier):
1381        return None
1382
1383
1384    def getNodes(self, requestor, service):
1385        return []
1386
1387
1388    def publish(self, requestor, service, nodeIdentifier, items):
1389        raise Unsupported('publish')
1390
1391
1392    def subscribe(self, requestor, service, nodeIdentifier, subscriber):
1393        raise Unsupported('subscribe')
1394
1395
1396    def unsubscribe(self, requestor, service, nodeIdentifier, subscriber):
1397        raise Unsupported('subscribe')
1398
1399
1400    def subscriptions(self, requestor, service):
1401        raise Unsupported('retrieve-subscriptions')
1402
1403
1404    def affiliations(self, requestor, service):
1405        raise Unsupported('retrieve-affiliations')
1406
1407
1408    def create(self, requestor, service, nodeIdentifier):
1409        raise Unsupported('create-nodes')
1410
1411
1412    def getConfigurationOptions(self):
1413        return {}
1414
1415
1416    def getDefaultConfiguration(self, requestor, service, nodeType):
1417        raise Unsupported('retrieve-default')
1418
1419
1420    def getConfiguration(self, requestor, service, nodeIdentifier):
1421        raise Unsupported('config-node')
1422
1423
1424    def setConfiguration(self, requestor, service, nodeIdentifier, options):
1425        raise Unsupported('config-node')
1426
1427
1428    def items(self, requestor, service, nodeIdentifier, maxItems,
1429                    itemIdentifiers):
1430        raise Unsupported('retrieve-items')
1431
1432
1433    def retract(self, requestor, service, nodeIdentifier, itemIdentifiers):
1434        raise Unsupported('retract-items')
1435
1436
1437    def purge(self, requestor, service, nodeIdentifier):
1438        raise Unsupported('purge-nodes')
1439
1440
1441    def delete(self, requestor, service, nodeIdentifier):
1442        raise Unsupported('delete-nodes')
1443
1444
1445
1446class PubSubResource(object):
1447
1448    implements(IPubSubResource)
1449
1450    features = []
1451    discoIdentity = disco.DiscoIdentity('pubsub',
1452                                        'service',
1453                                        'Publish-Subscribe Service')
1454
1455
1456    def locateResource(self, request):
1457        return self
1458
1459
1460    def getInfo(self, requestor, service, nodeIdentifier):
1461        return defer.succeed(None)
1462
1463
1464    def getNodes(self, requestor, service, nodeIdentifier):
1465        return defer.succeed([])
1466
1467
1468    def getConfigurationOptions(self):
1469        return {}
1470
1471
1472    def publish(self, request):
1473        return defer.fail(Unsupported('publish'))
1474
1475
1476    def subscribe(self, request):
1477        return defer.fail(Unsupported('subscribe'))
1478
1479
1480    def unsubscribe(self, request):
1481        return defer.fail(Unsupported('subscribe'))
1482
1483
1484    def subscriptions(self, request):
1485        return defer.fail(Unsupported('retrieve-subscriptions'))
1486
1487
1488    def affiliations(self, request):
1489        return defer.fail(Unsupported('retrieve-affiliations'))
1490
1491
1492    def create(self, request):
1493        return defer.fail(Unsupported('create-nodes'))
1494
1495
1496    def default(self, request):
1497        return defer.fail(Unsupported('retrieve-default'))
1498
1499
1500    def configureGet(self, request):
1501        return defer.fail(Unsupported('config-node'))
1502
1503
1504    def configureSet(self, request):
1505        return defer.fail(Unsupported('config-node'))
1506
1507
1508    def items(self, request):
1509        return defer.fail(Unsupported('retrieve-items'))
1510
1511
1512    def retract(self, request):
1513        return defer.fail(Unsupported('retract-items'))
1514
1515
1516    def purge(self, request):
1517        return defer.fail(Unsupported('purge-nodes'))
1518
1519
1520    def delete(self, request):
1521        return defer.fail(Unsupported('delete-nodes'))
1522
1523
1524    def affiliationsGet(self, request):
1525        return defer.fail(Unsupported('modify-affiliations'))
1526
1527
1528    def affiliationsSet(self, request):
1529        return defer.fail(Unsupported('modify-affiliations'))
1530
1531
1532    def subscriptionsGet(self, request):
1533        return defer.fail(Unsupported('manage-subscriptions'))
1534
1535
1536    def subscriptionsSet(self, request):
1537        return defer.fail(Unsupported('manage-subscriptions'))
Note: See TracBrowser for help on using the repository browser.