source: wokkel/pubsub.py @ 202:645fbb5f02f4

Last change on this file since 202:645fbb5f02f4 was 202:645fbb5f02f4, checked in by Ralph Meijer <ralphm@…>, 4 years ago

imported patch py3-pubsub.patch

  • Property exe set to *
File size: 49.8 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 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, basestring):
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):
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        request = PubSubRequest('items')
936        request.recipient = service
937        request.nodeIdentifier = nodeIdentifier
938        if maxItems:
939            request.maxItems = str(int(maxItems))
940        request.subscriptionIdentifier = subscriptionIdentifier
941        request.sender = sender
942
943        def cb(iq):
944            items = []
945            for element in iq.pubsub.items.elements():
946                if element.uri == NS_PUBSUB and element.name == 'item':
947                    items.append(element)
948            return items
949
950        d = request.send(self.xmlstream)
951        d.addCallback(cb)
952        return d
953
954
955    def getOptions(self, service, nodeIdentifier, subscriber,
956                         subscriptionIdentifier=None, sender=None):
957        """
958        Get subscription options.
959
960        @param service: The publish subscribe service that keeps the node.
961        @type service: L{JID<twisted.words.protocols.jabber.jid.JID>}
962
963        @param nodeIdentifier: The identifier of the node.
964        @type nodeIdentifier: L{unicode}
965
966        @param subscriber: The entity subscribed to the node.
967        @type subscriber: L{JID<twisted.words.protocols.jabber.jid.JID>}
968
969        @param subscriptionIdentifier: Optional subscription identifier.
970        @type subscriptionIdentifier: L{unicode}
971
972        @rtype: L{data_form.Form}
973        """
974        request = PubSubRequest('optionsGet')
975        request.recipient = service
976        request.nodeIdentifier = nodeIdentifier
977        request.subscriber = subscriber
978        request.subscriptionIdentifier = subscriptionIdentifier
979        request.sender = sender
980
981        def cb(iq):
982            form = data_form.findForm(iq.pubsub.options,
983                                      NS_PUBSUB_SUBSCRIBE_OPTIONS)
984            form.typeCheck()
985            return form
986
987        d = request.send(self.xmlstream)
988        d.addCallback(cb)
989        return d
990
991
992    def setOptions(self, service, nodeIdentifier, subscriber,
993                         options, subscriptionIdentifier=None, sender=None):
994        """
995        Set subscription options.
996
997        @param service: The publish subscribe service that keeps the node.
998        @type service: L{JID<twisted.words.protocols.jabber.jid.JID>}
999
1000        @param nodeIdentifier: The identifier of the node.
1001        @type nodeIdentifier: L{unicode}
1002
1003        @param subscriber: The entity subscribed to the node.
1004        @type subscriber: L{JID<twisted.words.protocols.jabber.jid.JID>}
1005
1006        @param options: Subscription options.
1007        @type options: L{dict}.
1008
1009        @param subscriptionIdentifier: Optional subscription identifier.
1010        @type subscriptionIdentifier: L{unicode}
1011        """
1012        request = PubSubRequest('optionsSet')
1013        request.recipient = service
1014        request.nodeIdentifier = nodeIdentifier
1015        request.subscriber = subscriber
1016        request.subscriptionIdentifier = subscriptionIdentifier
1017        request.sender = sender
1018
1019        form = data_form.Form(formType='submit',
1020                              formNamespace=NS_PUBSUB_SUBSCRIBE_OPTIONS)
1021        form.makeFields(options)
1022        request.options = form
1023
1024        d = request.send(self.xmlstream)
1025        return d
1026
1027
1028
1029@implementer(IPubSubService, disco.IDisco)
1030class PubSubService(XMPPHandler, IQHandlerMixin):
1031    """
1032    Protocol implementation for a XMPP Publish Subscribe Service.
1033
1034    The word Service here is used as taken from the Publish Subscribe
1035    specification. It is the party responsible for keeping nodes and their
1036    subscriptions, and sending out notifications.
1037
1038    Methods from the L{IPubSubService} interface that are called as a result
1039    of an XMPP request may raise exceptions. Alternatively the deferred
1040    returned by these methods may have their errback called. These are handled
1041    as follows:
1042
1043     - If the exception is an instance of L{error.StanzaError}, an error
1044       response iq is returned.
1045     - Any other exception is reported using L{log.msg}. An error response
1046       with the condition C{internal-server-error} is returned.
1047
1048    The default implementation of said methods raises an L{Unsupported}
1049    exception and are meant to be overridden.
1050
1051    @ivar discoIdentity: Service discovery identity as a dictionary with
1052                         keys C{'category'}, C{'type'} and C{'name'}.
1053    @ivar pubSubFeatures: List of supported publish-subscribe features for
1054                          service discovery, as L{str}.
1055    @type pubSubFeatures: L{list} or L{None}
1056    """
1057
1058    iqHandlers = {
1059            '/*': '_onPubSubRequest',
1060            }
1061
1062    _legacyHandlers = {
1063        'publish': ('publish', ['sender', 'recipient',
1064                                'nodeIdentifier', 'items']),
1065        'subscribe': ('subscribe', ['sender', 'recipient',
1066                                    'nodeIdentifier', 'subscriber']),
1067        'unsubscribe': ('unsubscribe', ['sender', 'recipient',
1068                                        'nodeIdentifier', 'subscriber']),
1069        'subscriptions': ('subscriptions', ['sender', 'recipient']),
1070        'affiliations': ('affiliations', ['sender', 'recipient']),
1071        'create': ('create', ['sender', 'recipient', 'nodeIdentifier']),
1072        'getConfigurationOptions': ('getConfigurationOptions', []),
1073        'default': ('getDefaultConfiguration',
1074                    ['sender', 'recipient', 'nodeType']),
1075        'configureGet': ('getConfiguration', ['sender', 'recipient',
1076                                              'nodeIdentifier']),
1077        'configureSet': ('setConfiguration', ['sender', 'recipient',
1078                                              'nodeIdentifier', 'options']),
1079        'items': ('items', ['sender', 'recipient', 'nodeIdentifier',
1080                            'maxItems', 'itemIdentifiers']),
1081        'retract': ('retract', ['sender', 'recipient', 'nodeIdentifier',
1082                                'itemIdentifiers']),
1083        'purge': ('purge', ['sender', 'recipient', 'nodeIdentifier']),
1084        'delete': ('delete', ['sender', 'recipient', 'nodeIdentifier']),
1085    }
1086
1087    hideNodes = False
1088
1089    def __init__(self, resource=None):
1090        self.resource = resource
1091        self.discoIdentity = {'category': 'pubsub',
1092                              'type': 'service',
1093                              'name': 'Generic Publish-Subscribe Service'}
1094
1095        self.pubSubFeatures = []
1096
1097
1098    def connectionMade(self):
1099        self.xmlstream.addObserver(PUBSUB_REQUEST, self.handleRequest)
1100
1101
1102    def getDiscoInfo(self, requestor, target, nodeIdentifier=''):
1103        def toInfo(nodeInfo):
1104            if not nodeInfo:
1105                return
1106
1107            (nodeType, metaData) = nodeInfo['type'], nodeInfo['meta-data']
1108            info.append(disco.DiscoIdentity('pubsub', nodeType))
1109            if metaData:
1110                form = data_form.Form(formType="result",
1111                                      formNamespace=NS_PUBSUB_META_DATA)
1112                form.addField(
1113                        data_form.Field(
1114                            var='pubsub#node_type',
1115                            value=nodeType,
1116                            label='The type of node (collection or leaf)'
1117                        )
1118                )
1119
1120                for metaDatum in metaData:
1121                    form.addField(data_form.Field.fromDict(metaDatum))
1122
1123                info.append(form)
1124
1125            return
1126
1127        info = []
1128
1129        request = PubSubRequest('discoInfo')
1130
1131        if self.resource is not None:
1132            resource = self.resource.locateResource(request)
1133            identity = resource.discoIdentity
1134            features = resource.features
1135            getInfo = resource.getInfo
1136        else:
1137            category = self.discoIdentity['category']
1138            idType = self.discoIdentity['type']
1139            name = self.discoIdentity['name']
1140            identity = disco.DiscoIdentity(category, idType, name)
1141            features = self.pubSubFeatures
1142            getInfo = self.getNodeInfo
1143
1144        if not nodeIdentifier:
1145            info.append(identity)
1146            info.append(disco.DiscoFeature(disco.NS_DISCO_ITEMS))
1147            info.extend([disco.DiscoFeature("%s#%s" % (NS_PUBSUB, feature))
1148                         for feature in features])
1149
1150        d = defer.maybeDeferred(getInfo, requestor, target, nodeIdentifier or '')
1151        d.addCallback(toInfo)
1152        d.addErrback(log.err)
1153        d.addCallback(lambda _: info)
1154        return d
1155
1156
1157    def getDiscoItems(self, requestor, target, nodeIdentifier=''):
1158        if self.hideNodes:
1159            d = defer.succeed([])
1160        elif self.resource is not None:
1161            request = PubSubRequest('discoInfo')
1162            resource = self.resource.locateResource(request)
1163            d = resource.getNodes(requestor, target, nodeIdentifier)
1164        elif nodeIdentifier:
1165            d = self.getNodes(requestor, target)
1166        else:
1167            d = defer.succeed([])
1168
1169        d.addCallback(lambda nodes: [disco.DiscoItem(target, node)
1170                                     for node in nodes])
1171        return d
1172
1173
1174    def _onPubSubRequest(self, iq):
1175        request = PubSubRequest.fromElement(iq)
1176
1177        if self.resource is not None:
1178            resource = self.resource.locateResource(request)
1179        else:
1180            resource = self
1181
1182        # Preprocess the request, knowing the handling resource
1183        try:
1184            preProcessor = getattr(self, '_preProcess_%s' % request.verb)
1185        except AttributeError:
1186            pass
1187        else:
1188            request = preProcessor(resource, request)
1189            if request is None:
1190                return defer.succeed(None)
1191
1192        # Process the request itself,
1193        if resource is not self:
1194            try:
1195                handler = getattr(resource, request.verb)
1196            except AttributeError:
1197                text = "Request verb: %s" % request.verb
1198                return defer.fail(Unsupported('', text))
1199
1200            d = handler(request)
1201        else:
1202            try:
1203                handlerName, argNames = self._legacyHandlers[request.verb]
1204            except KeyError:
1205                text = "Request verb: %s" % request.verb
1206                return defer.fail(Unsupported('', text))
1207
1208            handler = getattr(self, handlerName)
1209            args = [getattr(request, arg) for arg in argNames]
1210            d = handler(*args)
1211
1212        # If needed, translate the result into a response
1213        try:
1214            cb = getattr(self, '_toResponse_%s' % request.verb)
1215        except AttributeError:
1216            pass
1217        else:
1218            d.addCallback(cb, resource, request)
1219
1220        return d
1221
1222
1223    def _toResponse_subscribe(self, result, resource, request):
1224        response = domish.Element((NS_PUBSUB, "pubsub"))
1225        response.addChild(result.toElement(NS_PUBSUB))
1226        return response
1227
1228
1229    def _toResponse_subscriptions(self, result, resource, request):
1230        response = domish.Element((NS_PUBSUB, 'pubsub'))
1231        subscriptions = response.addElement('subscriptions')
1232        for subscription in result:
1233            subscriptions.addChild(subscription.toElement(NS_PUBSUB))
1234        return response
1235
1236
1237    def _toResponse_affiliations(self, result, resource, request):
1238        response = domish.Element((NS_PUBSUB, 'pubsub'))
1239        affiliations = response.addElement('affiliations')
1240
1241        for nodeIdentifier, affiliation in result:
1242            item = affiliations.addElement('affiliation')
1243            item['node'] = nodeIdentifier
1244            item['affiliation'] = affiliation
1245
1246        return response
1247
1248
1249    def _toResponse_create(self, result, resource, request):
1250        if not request.nodeIdentifier or request.nodeIdentifier != result:
1251            response = domish.Element((NS_PUBSUB, 'pubsub'))
1252            create = response.addElement('create')
1253            create['node'] = result
1254            return response
1255        else:
1256            return None
1257
1258
1259    def _formFromConfiguration(self, resource, values):
1260        fieldDefs = resource.getConfigurationOptions()
1261        form = data_form.Form(formType="form",
1262                              formNamespace=NS_PUBSUB_NODE_CONFIG)
1263        form.makeFields(values, fieldDefs)
1264        return form
1265
1266
1267    def _checkConfiguration(self, resource, form):
1268        fieldDefs = resource.getConfigurationOptions()
1269        form.typeCheck(fieldDefs, filterUnknown=True)
1270
1271
1272    def _preProcess_create(self, resource, request):
1273        if request.options:
1274            self._checkConfiguration(resource, request.options)
1275        return request
1276
1277
1278    def _preProcess_default(self, resource, request):
1279        if request.nodeType not in ('leaf', 'collection'):
1280            raise error.StanzaError('not-acceptable')
1281        else:
1282            return request
1283
1284
1285    def _toResponse_default(self, options, resource, request):
1286        response = domish.Element((NS_PUBSUB_OWNER, "pubsub"))
1287        default = response.addElement("default")
1288        form = self._formFromConfiguration(resource, options)
1289        default.addChild(form.toElement())
1290        return response
1291
1292
1293    def _toResponse_configureGet(self, options, resource, request):
1294        response = domish.Element((NS_PUBSUB_OWNER, "pubsub"))
1295        configure = response.addElement("configure")
1296        form = self._formFromConfiguration(resource, options)
1297        configure.addChild(form.toElement())
1298
1299        if request.nodeIdentifier:
1300            configure["node"] = request.nodeIdentifier
1301
1302        return response
1303
1304
1305    def _preProcess_configureSet(self, resource, request):
1306        if request.options.formType == 'cancel':
1307            return None
1308        else:
1309            self._checkConfiguration(resource, request.options)
1310            return request
1311
1312
1313    def _toResponse_items(self, result, resource, request):
1314        response = domish.Element((NS_PUBSUB, 'pubsub'))
1315        items = response.addElement('items')
1316        items["node"] = request.nodeIdentifier
1317
1318        for item in result:
1319            item.uri = NS_PUBSUB
1320            items.addChild(item)
1321
1322        return response
1323
1324
1325    def _createNotification(self, eventType, service, nodeIdentifier,
1326                                  subscriber, subscriptions=None):
1327        headers = []
1328
1329        if subscriptions:
1330            for subscription in subscriptions:
1331                if nodeIdentifier != subscription.nodeIdentifier:
1332                    headers.append(('Collection', subscription.nodeIdentifier))
1333
1334        message = domish.Element((None, "message"))
1335        message["from"] = service.full()
1336        message["to"] = subscriber.full()
1337        event = message.addElement((NS_PUBSUB_EVENT, "event"))
1338
1339        element = event.addElement(eventType)
1340        element["node"] = nodeIdentifier
1341
1342        if headers:
1343            message.addChild(shim.Headers(headers))
1344
1345        return message
1346
1347
1348    def _toResponse_affiliationsGet(self, result, resource, request):
1349        response = domish.Element((NS_PUBSUB_OWNER, 'pubsub'))
1350        affiliations = response.addElement('affiliations')
1351
1352        if request.nodeIdentifier:
1353            affiliations['node'] = request.nodeIdentifier
1354
1355        for entity, affiliation in iteritems(result):
1356            item = affiliations.addElement('affiliation')
1357            item['jid'] = entity.full()
1358            item['affiliation'] = affiliation
1359
1360        return response
1361
1362
1363    # public methods
1364
1365    def notifyPublish(self, service, nodeIdentifier, notifications):
1366        for subscriber, subscriptions, items in notifications:
1367            message = self._createNotification('items', service,
1368                                               nodeIdentifier, subscriber,
1369                                               subscriptions)
1370            for item in items:
1371                item.uri = NS_PUBSUB_EVENT
1372                message.event.items.addChild(item)
1373            self.send(message)
1374
1375
1376    def notifyDelete(self, service, nodeIdentifier, subscribers,
1377                           redirectURI=None):
1378        for subscriber in subscribers:
1379            message = self._createNotification('delete', service,
1380                                               nodeIdentifier,
1381                                               subscriber)
1382            if redirectURI:
1383                redirect = message.event.delete.addElement('redirect')
1384                redirect['uri'] = redirectURI
1385            self.send(message)
1386
1387
1388    def getNodeInfo(self, requestor, service, nodeIdentifier):
1389        return None
1390
1391
1392    def getNodes(self, requestor, service):
1393        return []
1394
1395
1396    def publish(self, requestor, service, nodeIdentifier, items):
1397        raise Unsupported('publish')
1398
1399
1400    def subscribe(self, requestor, service, nodeIdentifier, subscriber):
1401        raise Unsupported('subscribe')
1402
1403
1404    def unsubscribe(self, requestor, service, nodeIdentifier, subscriber):
1405        raise Unsupported('subscribe')
1406
1407
1408    def subscriptions(self, requestor, service):
1409        raise Unsupported('retrieve-subscriptions')
1410
1411
1412    def affiliations(self, requestor, service):
1413        raise Unsupported('retrieve-affiliations')
1414
1415
1416    def create(self, requestor, service, nodeIdentifier):
1417        raise Unsupported('create-nodes')
1418
1419
1420    def getConfigurationOptions(self):
1421        return {}
1422
1423
1424    def getDefaultConfiguration(self, requestor, service, nodeType):
1425        raise Unsupported('retrieve-default')
1426
1427
1428    def getConfiguration(self, requestor, service, nodeIdentifier):
1429        raise Unsupported('config-node')
1430
1431
1432    def setConfiguration(self, requestor, service, nodeIdentifier, options):
1433        raise Unsupported('config-node')
1434
1435
1436    def items(self, requestor, service, nodeIdentifier, maxItems,
1437                    itemIdentifiers):
1438        raise Unsupported('retrieve-items')
1439
1440
1441    def retract(self, requestor, service, nodeIdentifier, itemIdentifiers):
1442        raise Unsupported('retract-items')
1443
1444
1445    def purge(self, requestor, service, nodeIdentifier):
1446        raise Unsupported('purge-nodes')
1447
1448
1449    def delete(self, requestor, service, nodeIdentifier):
1450        raise Unsupported('delete-nodes')
1451
1452
1453
1454@implementer(IPubSubResource)
1455class PubSubResource(object):
1456
1457    features = []
1458    discoIdentity = disco.DiscoIdentity('pubsub',
1459                                        'service',
1460                                        'Publish-Subscribe Service')
1461
1462
1463    def locateResource(self, request):
1464        return self
1465
1466
1467    def getInfo(self, requestor, service, nodeIdentifier):
1468        return defer.succeed(None)
1469
1470
1471    def getNodes(self, requestor, service, nodeIdentifier):
1472        return defer.succeed([])
1473
1474
1475    def getConfigurationOptions(self):
1476        return {}
1477
1478
1479    def publish(self, request):
1480        return defer.fail(Unsupported('publish'))
1481
1482
1483    def subscribe(self, request):
1484        return defer.fail(Unsupported('subscribe'))
1485
1486
1487    def unsubscribe(self, request):
1488        return defer.fail(Unsupported('subscribe'))
1489
1490
1491    def subscriptions(self, request):
1492        return defer.fail(Unsupported('retrieve-subscriptions'))
1493
1494
1495    def affiliations(self, request):
1496        return defer.fail(Unsupported('retrieve-affiliations'))
1497
1498
1499    def create(self, request):
1500        return defer.fail(Unsupported('create-nodes'))
1501
1502
1503    def default(self, request):
1504        return defer.fail(Unsupported('retrieve-default'))
1505
1506
1507    def configureGet(self, request):
1508        return defer.fail(Unsupported('config-node'))
1509
1510
1511    def configureSet(self, request):
1512        return defer.fail(Unsupported('config-node'))
1513
1514
1515    def items(self, request):
1516        return defer.fail(Unsupported('retrieve-items'))
1517
1518
1519    def retract(self, request):
1520        return defer.fail(Unsupported('retract-items'))
1521
1522
1523    def purge(self, request):
1524        return defer.fail(Unsupported('purge-nodes'))
1525
1526
1527    def delete(self, request):
1528        return defer.fail(Unsupported('delete-nodes'))
1529
1530
1531    def affiliationsGet(self, request):
1532        return defer.fail(Unsupported('modify-affiliations'))
1533
1534
1535    def affiliationsSet(self, request):
1536        return defer.fail(Unsupported('modify-affiliations'))
1537
1538
1539    def subscriptionsGet(self, request):
1540        return defer.fail(Unsupported('manage-subscriptions'))
1541
1542
1543    def subscriptionsSet(self, request):
1544        return defer.fail(Unsupported('manage-subscriptions'))
Note: See TracBrowser for help on using the repository browser.