source: wokkel/pubsub.py @ 166:d9c10a5b5c0d

Last change on this file since 166:d9c10a5b5c0d was 166:d9c10a5b5c0d, checked in by Ralph Meijer <ralphm@…>, 8 years ago

Documentation fixes for pydoctor.

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