source: wokkel/pubsub.py @ 2:47f1cb624f14

Last change on this file since 2:47f1cb624f14 was 2:47f1cb624f14, checked in by Ralph Meijer <ralphm@…>, 14 years ago

Add in pubsub client support, client helpers and generic XMPP subprotocol handlers.

File size: 23.5 KB
Line 
1# -*- test-case-name: wokkel.test.test_pubsub -*-
2#
3# Copyright (c) 2003-2007 Ralph Meijer
4# See LICENSE for details.
5
6"""
7XMPP publish-subscribe protocol.
8
9This protocol is specified in
10U{XEP-0060<http://www.xmpp.org/extensions/xep-0060.html>}.
11"""
12
13from zope.interface import implements
14
15from twisted.internet import defer
16from twisted.words.protocols.jabber import jid, error, xmlstream
17from twisted.words.xish import domish
18
19from wokkel import disco, data_form
20from wokkel.subprotocols import IQHandlerMixin, XMPPHandler
21from wokkel.iwokkel import IPubSubClient, IPubSubService
22
23# Iq get and set XPath queries
24IQ_GET = '/iq[@type="get"]'
25IQ_SET = '/iq[@type="set"]'
26
27# Publish-subscribe namespaces
28NS_PUBSUB = 'http://jabber.org/protocol/pubsub'
29NS_PUBSUB_EVENT = NS_PUBSUB + '#event'
30NS_PUBSUB_ERRORS = NS_PUBSUB + '#errors'
31NS_PUBSUB_OWNER = NS_PUBSUB + "#owner"
32NS_PUBSUB_NODE_CONFIG = NS_PUBSUB + "#node_config"
33NS_PUBSUB_META_DATA = NS_PUBSUB + "#meta-data"
34
35# In publish-subscribe namespace XPath query selector.
36IN_NS_PUBSUB = '[@xmlns="' + NS_PUBSUB + '"]'
37IN_NS_PUBSUB_OWNER = '[@xmlns="' + NS_PUBSUB_OWNER + '"]'
38
39# Publish-subscribe XPath queries
40PUBSUB_ELEMENT = '/pubsub' + IN_NS_PUBSUB
41PUBSUB_OWNER_ELEMENT = '/pubsub' + IN_NS_PUBSUB_OWNER
42PUBSUB_GET = IQ_GET + PUBSUB_ELEMENT
43PUBSUB_SET = IQ_SET + PUBSUB_ELEMENT
44PUBSUB_OWNER_GET = IQ_GET + PUBSUB_OWNER_ELEMENT
45PUBSUB_OWNER_SET = IQ_SET + PUBSUB_OWNER_ELEMENT
46
47# Publish-subscribe command XPath queries
48PUBSUB_PUBLISH = PUBSUB_SET + '/publish' + IN_NS_PUBSUB
49PUBSUB_CREATE = PUBSUB_SET + '/create' + IN_NS_PUBSUB
50PUBSUB_SUBSCRIBE = PUBSUB_SET + '/subscribe' + IN_NS_PUBSUB
51PUBSUB_UNSUBSCRIBE = PUBSUB_SET + '/unsubscribe' + IN_NS_PUBSUB
52PUBSUB_OPTIONS_GET = PUBSUB_GET + '/options' + IN_NS_PUBSUB
53PUBSUB_OPTIONS_SET = PUBSUB_SET + '/options' + IN_NS_PUBSUB
54PUBSUB_DEFAULT = PUBSUB_OWNER_GET + '/default' + IN_NS_PUBSUB_OWNER
55PUBSUB_CONFIGURE_GET = PUBSUB_OWNER_GET + '/configure' + IN_NS_PUBSUB_OWNER
56PUBSUB_CONFIGURE_SET = PUBSUB_OWNER_SET + '/configure' + IN_NS_PUBSUB_OWNER
57PUBSUB_SUBSCRIPTIONS = PUBSUB_GET + '/subscriptions' + IN_NS_PUBSUB
58PUBSUB_AFFILIATIONS = PUBSUB_GET + '/affiliations' + IN_NS_PUBSUB
59PUBSUB_AFFILIATIONS_GET = PUBSUB_OWNER_GET + '/affiliations' + \
60                          IN_NS_PUBSUB_OWNER
61PUBSUB_AFFILIATIONS_SET = PUBSUB_OWNER_SET + '/affiliations' + \
62                          IN_NS_PUBSUB_OWNER
63PUBSUB_SUBSCRIPTIONS_GET = PUBSUB_OWNER_GET + '/subscriptions' + \
64                          IN_NS_PUBSUB_OWNER
65PUBSUB_SUBSCRIPTIONS_SET = PUBSUB_OWNER_SET + '/subscriptions' + \
66                          IN_NS_PUBSUB_OWNER
67PUBSUB_ITEMS = PUBSUB_GET + '/items' + IN_NS_PUBSUB
68PUBSUB_RETRACT = PUBSUB_SET + '/retract' + IN_NS_PUBSUB
69PUBSUB_PURGE = PUBSUB_OWNER_SET + '/purge' + IN_NS_PUBSUB_OWNER
70PUBSUB_DELETE = PUBSUB_OWNER_SET + '/delete' + IN_NS_PUBSUB_OWNER
71
72class BadRequest(error.StanzaError):
73    """
74    Bad request stanza error.
75    """
76    def __init__(self):
77        error.StanzaError.__init__(self, 'bad-request')
78
79
80
81class SubscriptionPending(Exception):
82    """
83    Raised when the requested subscription is pending acceptance.
84    """
85
86
87class SubscriptionUnconfigured(Exception):
88    """
89    Raised when the requested subscription needs to be configured before
90    becoming active.
91    """
92
93
94class PubSubError(error.StanzaError):
95    """
96    Exception with publish-subscribe specific condition.
97    """
98    def __init__(self, condition, pubsubCondition, feature=None, text=None):
99        appCondition = domish.Element((NS_PUBSUB_ERRORS, pubsubCondition))
100        if feature:
101            appCondition['feature'] = feature
102        error.StanzaError.__init__(self, condition,
103                                         text=text,
104                                         appCondition=appCondition)
105
106
107class Unsupported(PubSubError):
108    def __init__(self, feature, text=None):
109        PubSubError.__init__(self, 'feature-not-implemented',
110                                   'unsupported',
111                                   feature,
112                                   text)
113
114
115class OptionsUnavailable(Unsupported):
116    def __init__(self):
117        Unsupported.__init__(self, 'subscription-options-unavailable')
118
119
120class Item(domish.Element):
121    """
122    Publish subscribe item.
123
124    This behaves like an object providing L{domish.IElement}.
125
126    Item payload can be added using C{addChild} or C{addRawXml}, or using the
127    C{payload} keyword argument to C{__init__}.
128    """
129
130    def __init__(self, id=None, payload=None):
131        """
132        @param id: optional item identifier
133        @type id: L{unicode}
134        @param payload: optional item payload. Either as a domish element, or
135                        as serialized XML.
136        @type payload: object providing L{domish.IElement} or L{unicode}.
137        """
138
139        domish.Element.__init__(self, (None, 'item'))
140        if id is not None:
141            self['id'] = id
142        if payload is not None:
143            if isinstance(payload, basestring):
144                self.addRawXml(payload)
145            else:
146                self.addChild(payload)
147
148class PubSubRequest(xmlstream.IQ):
149    """
150    Base class for publish subscribe user requests.
151
152    @cvar namespace: request namespace
153    @cvar verb: request verb
154    @cvar method: type attribute of the IQ request. Either C{'set'} or C{'get'}
155    @ivar command: command element of the request. This is the direct child of
156                   the C{pubsub} element in the C{namespace} with the name
157                   C{verb}.
158    """
159
160    namespace = NS_PUBSUB
161    method = 'set'
162
163    def __init__(self, xs):
164        xmlstream.IQ.__init__(self, xs, self.method)
165        self.addElement((self.namespace, 'pubsub'))
166
167        self.command = self.pubsub.addElement(self.verb)
168
169    def send(self, to):
170        destination = unicode(to)
171        return xmlstream.IQ.send(self, destination)
172
173class CreateNode(PubSubRequest):
174    verb = 'create'
175
176    def __init__(self, xs, node=None):
177        PubSubRequest.__init__(self, xs)
178        if node:
179            self.command["node"] = node
180
181class DeleteNode(PubSubRequest):
182    verb = 'delete'
183    def __init__(self, xs, node):
184        PubSubRequest.__init__(self, xs)
185        self.command["node"] = node
186
187class Subscribe(PubSubRequest):
188    verb = 'subscribe'
189
190    def __init__(self, xs, node, subscriber):
191        PubSubRequest.__init__(self, xs)
192        self.command["node"] = node
193        self.command["jid"] = subscriber.full()
194
195class Publish(PubSubRequest):
196    verb = 'publish'
197
198    def __init__(self, xs, node):
199        PubSubRequest.__init__(self, xs)
200        self.command["node"] = node
201
202    def addItem(self, id=None, payload=None):
203        item = self.command.addElement("item")
204        item.addChild(payload)
205
206        if id is not None:
207            item["id"] = id
208
209        return item
210
211class PubSubClient(XMPPHandler):
212    """
213    Publish subscribe client protocol.
214    """
215
216    implements(IPubSubClient)
217
218    def connectionInitialized(self):
219        self.xmlstream.addObserver('/message/event[@xmlns="%s"]/items' %
220                                   NS_PUBSUB_EVENT, self._onItems)
221
222    def _onItems(self, message):
223        try:
224            notifier = jid.JID(message["from"])
225            node = message.event.items["node"]
226        except KeyError:
227            return
228
229        items = [element for element in message.event.items.elements()
230                         if element.name == 'item']
231
232        self.itemsReceived(notifier, node, items)
233
234    def itemsReceived(self, notifier, node, items):
235        pass
236
237    def createNode(self, service, node=None):
238        request = CreateNode(self.xmlstream, node)
239
240        def cb(iq):
241            try:
242                new_node = iq.pubsub.create["node"]
243            except AttributeError:
244                # the suggested node identifier was accepted
245                new_node = node
246            return new_node
247
248        return request.send(service).addCallback(cb)
249
250    def deleteNode(self, service, node):
251        return DeleteNode(self.xmlstream, node).send(service)
252
253    def subscribe(self, service, node, subscriber):
254        request = Subscribe(self.xmlstream, node, subscriber)
255
256        def cb(iq):
257            subscription = iq.pubsub.subscription["subscription"]
258
259            if subscription == 'pending':
260                raise SubscriptionPending
261            elif subscription == 'unconfigured':
262                raise SubscriptionUnconfigured
263            else:
264                # we assume subscription == 'subscribed'
265                # any other value would be invalid, but that should have
266                # yielded a stanza error.
267                return None
268
269        return request.send(service).addCallback(cb)
270
271    def publish(self, service, node, items=[]):
272        request = Publish(self.xmlstream, node)
273        for item in items:
274            request.command.addChild(item)
275
276        return request.send(service)
277
278class PubSubService(XMPPHandler, IQHandlerMixin):
279    """
280    Protocol implementation for a XMPP Publish Subscribe Service.
281
282    The word Service here is used as taken from the Publish Subscribe
283    specification. It is the party responsible for keeping nodes and their
284    subscriptions, and sending out notifications.
285
286    Methods from the L{IPubSubService} interface that are called as
287    a result of an XMPP request may raise exceptions. Alternatively the
288    deferred returned by these methods may have their errback called. These are
289    handled as follows:
290
291    * If the exception is an instance of L{error.StanzaError}, an error
292      response iq is returned.
293    * Any other exception is reported using L{log.msg}. An error response
294      with the condition C{internal-server-error} is returned.
295
296    The default implementation of said methods raises an L{Unsupported}
297    exception and are meant to be overridden.
298
299    @ivar discoIdentity: Service discovery identity as a dictionary with
300                         keys C{'category'}, C{'type'} and C{'name'}.
301    @ivar pubSubFeatures: List of supported publish-subscribe features for
302                          service discovery, as C{str}.
303    @type pubSubFeatures: C{list} or C{None}.
304    """
305
306    implements(IPubSubService)
307
308    iqHandlers = {
309            PUBSUB_PUBLISH: '_onPublish',
310            PUBSUB_CREATE: '_onCreate',
311            PUBSUB_SUBSCRIBE: '_onSubscribe',
312            PUBSUB_OPTIONS_GET: '_onOptionsGet',
313            PUBSUB_OPTIONS_SET: '_onOptionsSet',
314            PUBSUB_AFFILIATIONS: '_onAffiliations',
315            PUBSUB_ITEMS: '_onItems',
316            PUBSUB_RETRACT: '_onRetract',
317            PUBSUB_SUBSCRIPTIONS: '_onSubscriptions',
318            PUBSUB_UNSUBSCRIBE: '_onUnsubscribe',
319
320            PUBSUB_AFFILIATIONS_GET: '_onAffiliationsGet',
321            PUBSUB_AFFILIATIONS_SET: '_onAffiliationsSet',
322            PUBSUB_CONFIGURE_GET: '_onConfigureGet',
323            PUBSUB_CONFIGURE_SET: '_onConfigureSet',
324            PUBSUB_DEFAULT: '_onDefault',
325            PUBSUB_PURGE: '_onPurge',
326            PUBSUB_DELETE: '_onDelete',
327            PUBSUB_SUBSCRIPTIONS_GET: '_onSubscriptionsGet',
328            PUBSUB_SUBSCRIPTIONS_SET: '_onSubscriptionsSet',
329
330            }
331
332    def __init__(self):
333        self.discoIdentity = {'category': 'pubsub',
334                              'type': 'generic',
335                              'name': 'Generic Publish-Subscribe Service'}
336
337        self.pubSubFeatures = []
338
339    def connectionMade(self):
340        self.xmlstream.addObserver(PUBSUB_GET, self.handleRequest)
341        self.xmlstream.addObserver(PUBSUB_SET, self.handleRequest)
342        self.xmlstream.addObserver(PUBSUB_OWNER_GET, self.handleRequest)
343        self.xmlstream.addObserver(PUBSUB_OWNER_SET, self.handleRequest)
344
345    def getDiscoInfo(self, target, requestor, nodeIdentifier):
346        info = []
347
348        if not nodeIdentifier:
349            info.append(disco.DiscoIdentity(**self.discoIdentity))
350
351            info.append(disco.DiscoFeature(disco.NS_ITEMS))
352            info.extend([disco.DiscoFeature("%s#%s" % (NS_PUBSUB, feature))
353                         for feature in self.pubSubFeatures])
354
355            return defer.succeed(info)
356        else:
357            def toInfo(nodeInfo):
358                if not nodeInfo:
359                    return []
360
361                (nodeType, metaData) = nodeInfo
362                info.append(disco.Identity('pubsub', nodeType))
363                if metaData:
364                    form = data_form.Form(type="result",
365                                          form_type=NS_PUBSUB_META_DATA) 
366                    form.add_field("text-single",
367                                   "pubsub#node_type",
368                                   "The type of node (collection or leaf)",
369                                   nodeType)
370
371                    for metaDatum in metaData:
372                        form.add_field(**metaDatum)
373
374                    info.append(form)
375                return info
376
377            d = self.getNodeInfo(requestor, nodeIdentifier)
378            d.addCallback(toInfo)
379            return d
380
381    def getDiscoItems(self, target, requestor, nodeIdentifier):
382        if nodeIdentifier or self.hideNodes:
383            return defer.succeed([])
384
385        d = self.getNodes(requestor)
386        d.addCallback(lambda nodes: [disco.DiscoItem(target, node)
387                                     for node in nodes])
388        return d
389
390    def _onPublish(self, iq):
391        requestor = jid.internJID(iq["from"]).userhostJID()
392
393        try:
394            nodeIdentifier = iq.pubsub.publish["node"]
395        except KeyError:
396            raise BadRequest
397
398        items = []
399        for element in iq.pubsub.publish.elements():
400            if element.uri == NS_PUBSUB and element.name == 'item':
401                items.append(element)
402
403        return self.publish(requestor, nodeIdentifier, items)
404
405    def _onSubscribe(self, iq):
406        requestor = jid.internJID(iq["from"]).userhostJID()
407
408        try:
409            nodeIdentifier = iq.pubsub.subscribe["node"]
410            subscriber = jid.internJID(iq.pubsub.subscribe["jid"])
411        except KeyError:
412            raise BadRequest
413
414        def toResponse(subscription):
415            nodeIdentifier, state = subscription
416            response = domish.Element((NS_PUBSUB, "pubsub"))
417            subscription = response.addElement("subscription")
418            subscription["node"] = nodeIdentifier
419            subscription["jid"] = subscriber.full()
420            subscription["subscription"] = state
421            return response
422
423        d = self.subscribe(requestor, nodeIdentifier, subscriber)
424        d.addCallback(toResponse)
425        return d
426
427    def _onUnsubscribe(self, iq):
428        requestor = jid.internJID(iq["from"]).userhostJID()
429
430        try:
431            nodeIdentifier = iq.pubsub.unsubscribe["node"]
432            subscriber = jid.internJID(iq.pubsub.unsubscribe["jid"])
433        except KeyError:
434            raise BadRequest
435
436        return self.unsubscribe(requestor, nodeIdentifier, subscriber)
437
438    def _onOptionsGet(self, iq):
439        raise Unsupported('subscription-options-unavailable')
440
441    def _onOptionsSet(self, iq):
442        raise Unsupported('subscription-options-unavailable')
443
444    def _onSubscriptions(self, iq):
445        requestor = jid.internJID(iq["from"]).userhostJID()
446
447        def toResponse(result):
448            response = domish.Element((NS_PUBSUB, 'pubsub'))
449            subscriptions = response.addElement('subscriptions')
450            for node, subscriber, state in result:
451                item = subscriptions.addElement('subscription')
452                item['node'] = node
453                item['jid'] = subscriber.full()
454                item['subscription'] = state
455            return response
456
457        d = self.subscriptions(requestor)
458        d.addCallback(toResponse)
459        return d
460
461    def _onAffiliations(self, iq):
462        requestor = jid.internJID(iq["from"]).userhostJID()
463
464        def toResponse(result):
465            response = domish.Element((NS_PUBSUB, 'pubsub'))
466            affiliations = response.addElement('affiliations')
467
468            for nodeIdentifier, affiliation in result:
469                item = affiliations.addElement('affiliation')
470                item['node'] = nodeIdentifier
471                item['affiliation'] = affiliation
472
473            return response
474
475        d = self.affiliations(requestor)
476        d.addCallback(toResponse)
477        return d
478
479    def _onCreate(self, iq):
480        requestor = jid.internJID(iq["from"]).userhostJID()
481        nodeIdentifier = iq.pubsub.create.getAttribute("node")
482
483        def toResponse(result):
484            if not nodeIdentifier or nodeIdentifier != result:
485                response = domish.Element((NS_PUBSUB, 'pubsub'))
486                create = response.addElement('create')
487                create['node'] = result
488                return response
489            else:
490                return None
491
492        d = self.create(requestor, nodeIdentifier)
493        d.addCallback(toResponse)
494        return d
495
496    def _formFromConfiguration(self, options):
497        form = data_form.Form(type="form", form_type=NS_PUBSUB_NODE_CONFIG)
498
499        for option in options:
500            form.add_field(**option)
501
502        return form
503
504    def _onDefault(self, iq):
505        requestor = jid.internJID(iq["from"]).userhostJID()
506
507        def toResponse(options):
508            response = domish.Element((NS_PUBSUB_OWNER, "pubsub"))
509            default = response.addElement("default")
510            default.addChild(self._formFromConfiguration(options))
511            return response
512
513        d = self.getDefaultConfiguration(requestor)
514        d.addCallback(toResponse)
515        return d
516
517    def _onConfigureGet(self, iq):
518        requestor = jid.internJID(iq["from"]).userhostJID()
519        nodeIdentifier = iq.pubsub.configure.getAttribute("node")
520
521        def toResponse(options):
522            response = domish.Element((NS_PUBSUB_OWNER, "pubsub"))
523            configure = response.addElement("configure")
524            configure.addChild(self._formFromConfiguration(options))
525
526            if nodeIdentifier:
527                configure["node"] = nodeIdentifier
528
529            return response
530
531        d = self.getConfiguration(requestor, nodeIdentifier)
532        d.addCallback(toResponse)
533        return d
534
535    def _onConfigureSet(self, iq):
536        requestor = jid.internJID(iq["from"]).userhostJID()
537        nodeIdentifier = iq.pubsub.configure["node"]
538
539        def getFormOptions(self, form):
540            options = {}
541
542            for element in form.elements():
543                if element.name == 'field' and \
544                   element.uri == data_form.NS_X_DATA:
545                    try:
546                        options[element["var"]] = str(element.value)
547                    except (KeyError, AttributeError):
548                        raise BadRequest
549
550            return options
551
552        # Search configuration form with correct FORM_TYPE and process it
553
554        for element in iq.pubsub.configure.elements():
555            if element.name != 'x' or element.uri != data_form.NS_X_DATA:
556                continue
557
558            type = element.getAttribute("type")
559            if type == "cancel":
560                return None
561            elif type != "submit":
562                continue
563
564            options = getFormOptions(element)
565
566            if options["FORM_TYPE"] == NS_PUBSUB + "#node_config":
567                del options["FORM_TYPE"]
568                return self.setConfiguration(requestor, nodeIdentifier,
569                                             options)
570
571        raise BadRequest
572
573    def _onItems(self, iq):
574        requestor = jid.internJID(iq["from"]).userhostJID()
575
576        try:
577            nodeIdentifier = iq.pubsub.items["node"]
578        except KeyError:
579            raise BadRequest
580
581        maxItems = iq.pubsub.items.getAttribute('max_items')
582
583        if maxItems:
584            try:
585                maxItems = int(maxItems)
586            except ValueError:
587                raise BadRequest
588
589        itemIdentifiers = []
590        for child in iq.pubsub.items.elements():
591            if child.name == 'item' and child.uri == NS_PUBSUB:
592                try:
593                    itemIdentifiers.append(child["id"])
594                except KeyError:
595                    raise BadRequest
596
597        def toResponse(result):
598            response = domish.Element((NS_PUBSUB, 'pubsub'))
599            items = response.addElement('items')
600            items["node"] = nodeIdentifier
601
602            for item in result:
603                items.addRawXml(item)
604
605            return response
606
607        d = self.items(requestor, nodeIdentifier, maxItems, itemIdentifiers)
608        d.addCallback(toResponse)
609        return d
610
611    def _onRetract(self, iq):
612        requestor = jid.internJID(iq["from"]).userhostJID()
613
614        try:
615            nodeIdentifier = iq.pubsub.retract["node"]
616        except KeyError:
617            raise BadRequest
618
619        itemIdentifiers = []
620        for child in iq.pubsub.retract.elements():
621            if child.uri == NS_PUBSUB_OWNER and child.name == 'item':
622                try:
623                    itemIdentifiers.append(child["id"])
624                except KeyError:
625                    raise BadRequest
626
627        return self.retract(requestor, nodeIdentifier, itemIdentifiers)
628
629    def _onPurge(self, iq):
630        requestor = jid.internJID(iq["from"]).userhostJID()
631
632        try:
633            nodeIdentifier = iq.pubsub.purge["node"]
634        except KeyError:
635            raise BadRequest
636
637        return self.purge(requestor, nodeIdentifier)
638
639    def _onDelete(self, iq):
640        requestor = jid.internJID(iq["from"]).userhostJID()
641
642        try:
643            nodeIdentifier = iq.pubsub.delete["node"]
644        except KeyError:
645            raise BadRequest
646
647        return self.delete(requestor, nodeIdentifier)
648
649    def _onAffiliationsGet(self, iq):
650        raise Unsupported('modify-affiliations')
651
652    def _onAffiliationsSet(self, iq):
653        raise Unsupported('modify-affiliations')
654
655    def _onSubscriptionsGet(self, iq):
656        raise Unsupported('manage-subscriptions')
657
658    def _onSubscriptionsSet(self, iq):
659        raise Unsupported('manage-subscriptions')
660
661    # public methods
662
663    def notifyPublish(self, entity, nodeIdentifier, notifications):
664
665        print notifications
666        for recipient, items in notifications:
667            message = domish.Element((None, "message"))
668            message["from"] = entity.full()
669            message["to"] = recipient.full()
670            event = message.addElement((NS_PUBSUB_EVENT, "event"))
671            element = event.addElement("items")
672            element["node"] = nodeIdentifier
673            element.children = items
674            self.send(message)
675
676    def getNodeInfo(self, requestor, nodeIdentifier):
677        return None
678
679    def getNodes(self, requestor):
680        return []
681
682    def publish(self, requestor, nodeIdentifier, items):
683        raise Unsupported('publish')
684
685    def subscribe(self, requestor, nodeIdentifier, subscriber):
686        raise Unsupported('subscribe')
687
688    def unsubscribe(self, requestor, nodeIdentifier, subscriber):
689        raise Unsupported('subscribe')
690
691    def subscriptions(self, requestor):
692        raise Unsupported('retrieve-subscriptions')
693
694    def affiliations(self, requestor):
695        raise Unsupported('retrieve-affiliations')
696
697    def create(self, requestor, nodeIdentifier):
698        raise Unsupported('create-nodes')
699
700    def getDefaultConfiguration(self, requestor):
701        raise Unsupported('retrieve-default')
702
703    def getConfiguration(self, requestor, nodeIdentifier):
704        raise Unsupported('config-node')
705
706    def setConfiguration(self, requestor, nodeIdentifier, options):
707        raise Unsupported('config-node')
708
709    def items(self, requestor, nodeIdentifier, maxItems, itemIdentifiers):
710        raise Unsupported('retrieve-items')
711
712    def retract(self, requestor, nodeIdentifier, itemIdentifiers):
713        raise Unsupported('retract-items')
714
715    def purge(self, requestor, nodeIdentifier):
716        raise Unsupported('purge-nodes')
717
718    def delete(self, requestor, nodeIdentifier):
719        raise Unsupported('delete-nodes')
720
Note: See TracBrowser for help on using the repository browser.