source: wokkel/pubsub.py @ 223:92014ac5e10c

Last change on this file since 223:92014ac5e10c was 223:92014ac5e10c, checked in by Ralph Meijer <ralphm@…>, 4 years ago

Move new keyword argument to the end, fix docstring.

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