source: wokkel/pubsub.py @ 227:a420a85b5b9b

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

Undo dependency on _request_class from #13.

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