source: wokkel/pubsub.py @ 165:76a61f5aa343

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

Cleanups leading up to Wokkel 0.7.0.

As we now depend on Twisted 10.0.0 or higher, the following classes and
interfaces were deprecated:

This also resolves all Pyflakes warnings, changes links for www.xmpp.org to
xmpp.org and fixes the copyright notice in LICENSE to include 2012.

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