Ignore:
Files:
5 added
12 edited

Legend:

Unmodified
Added
Removed
  • README

    r19 r21  
    1 Wokkel 0.3.0
     1Wokkel 0.3.1
    22
    33What is this?
  • setup.py

    r19 r21  
    11#!/usr/bin/env python
    22
    3 # Copyright (c) 2003-2007 Ralph Meijer
     3# Copyright (c) 2003-2008 Ralph Meijer
    44# See LICENSE for details.
    55
    6 #from distutils.core import setup
    76from setuptools import setup
    87
    98setup(name='wokkel',
    10       version='0.3.0',
     9      version='0.3.1',
    1110      description='Twisted Jabber support library',
    1211      author='Ralph Meijer',
  • wokkel/data_form.py

    r1 r29  
    1 # Copyright (c) 2003-2007 Ralph Meijer
     1# -*- test-case-name: wokkel.test.test_data_form -*-
     2#
     3# Copyright (c) 2003-2008 Ralph Meijer
    24# See LICENSE for details.
    35
     6"""
     7Data Forms.
     8
     9Support for Data Forms as described in
     10U{XEP-0004<http://www.xmpp.org/extensions/xep-0004.html>}, along with support
     11for Field Standardization for Data Forms as described in
     12U{XEP-0068<http://www.xmpp.org/extensions/xep-0068.html>}.
     13"""
     14
     15from twisted.words.protocols.jabber.jid import JID
    416from twisted.words.xish import domish
    517
    618NS_X_DATA = 'jabber:x:data'
    719
    8 class Field(domish.Element):
    9     def __init__(self, type='text-single', var=None, label=None,
    10                        value=None, values=[], options={}):
    11         domish.Element.__init__(self, (NS_X_DATA, 'field'))
    12         self['type'] = type
    13         if var is not None:
    14             self['var'] = var
    15         if label is not None:
    16             self['label'] = label
     20
     21
     22class Error(Exception):
     23    """
     24    Data Forms error.
     25    """
     26
     27
     28
     29class FieldNameRequiredError(Error):
     30    """
     31    A field name is required for this field type.
     32    """
     33
     34
     35
     36class TooManyValuesError(Error):
     37    """
     38    This field is single-value.
     39    """
     40
     41
     42
     43class Option(object):
     44    """
     45    Data Forms field option.
     46
     47    @ivar value: Value of this option.
     48    @type value: C{unicode}
     49    @ivar label: Optional label for this option.
     50    @type label: C{unicode} or C{NoneType}.
     51    """
     52
     53    def __init__(self, value, label=None):
     54        self.value = value
     55        self.label = label
     56
     57
     58    def __repr__(self):
     59        r = ["Option(", repr(self.value)]
     60        if self.label:
     61            r.append(", ")
     62            r.append(repr(self.label))
     63        r.append(")")
     64        return u"".join(r)
     65
     66
     67    def toElement(self):
     68        """
     69        Return the DOM representation of this option.
     70
     71        @rtype L{domish.Element}.
     72        """
     73        option = domish.Element((NS_X_DATA, 'option'))
     74        option.addElement('value', content=self.value)
     75        if self.label:
     76            option['label'] = self.label
     77        return option
     78
     79    @staticmethod
     80    def fromElement(element):
     81        valueElements = list(domish.generateElementsQNamed(element.children,
     82                                                           'value', NS_X_DATA))
     83        if not valueElements:
     84            raise Error("Option has no value")
     85
     86        label = element.getAttribute('label')
     87        return Option(unicode(valueElements[0]), label)
     88
     89
     90class Field(object):
     91    """
     92    Data Forms field.
     93
     94    @ivar fieldType: Type of this field. One of C{'boolean'}, C{'fixed'},
     95                     C{'hidden'}, C{'jid-multi'}, C{'jid-single'},
     96                     C{'list-multi'}, {'list-single'}, C{'text-multi'},
     97                     C{'text-private'}, C{'text-single'}.
     98
     99                     The default is C{'text-single'}.
     100    @type fieldType: C{str}
     101    @ivar var: Field name. Optional if L{fieldType} is C{'fixed'}.
     102    @type var: C{str}
     103    @ivar label: Human readable label for this field.
     104    @type label: C{unicode}
     105    @ivar values: The values for this field, for multi-valued field
     106                  types, as a list of C{bool}, C{unicode} or L{JID}.
     107    @type values: C{list}
     108    @ivar options: List of possible values to choose from in a response
     109                   to this form as a list of L{Option}s.
     110    @type options: C{list}.
     111    @ivar desc: Human readable description for this field.
     112    @type desc: C{unicode}
     113    @ivar required: Whether the field is required to be provided in a
     114                    response to this form.
     115    @type required: C{bool}.
     116    """
     117
     118    def __init__(self, fieldType='text-single', var=None, value=None,
     119                       values=None, options=None, label=None, desc=None,
     120                       required=False):
     121        """
     122        Initialize this field.
     123
     124        See the identically named instance variables for descriptions.
     125
     126        If C{value} is not C{None}, it overrides C{values}, setting the
     127        given value as the only value for this field.
     128        """
     129
     130        self.fieldType = fieldType
     131        self.var = var
    17132        if value is not None:
    18             self.set_value(value)
     133            self.value = value
    19134        else:
    20             self.set_values(values)
    21         if type in ['list-single', 'list-multi']:
    22             for value, label in options.iteritems():
    23                 self.addChild(Option(value, label))
    24 
    25     def set_value(self, value):
    26         if self['type'] == 'boolean':
    27             value = str(int(bool(value)))
     135            self.values = values or []
     136
     137        try:
     138            self.options = [Option(value, label)
     139                            for value, label in options.iteritems()]
     140        except AttributeError:
     141            self.options = options or []
     142
     143        self.label = label
     144        self.desc = desc
     145        self.required = required
     146
     147
     148    def __repr__(self):
     149        r = ["Field(fieldType=", repr(self.fieldType)]
     150        if self.var:
     151            r.append(", var=")
     152            r.append(repr(self.var))
     153        if self.label:
     154            r.append(", label=")
     155            r.append(repr(self.label))
     156        if self.desc:
     157            r.append(", desc=")
     158            r.append(repr(self.desc))
     159        if self.required:
     160            r.append(", required=")
     161            r.append(repr(self.required))
     162        if self.values:
     163            r.append(", values=")
     164            r.append(repr(self.values))
     165        if self.options:
     166            r.append(", options=")
     167            r.append(repr(self.options))
     168        r.append(")")
     169        return u"".join(r)
     170
     171
     172    def __value_set(self, value):
     173        """
     174        Setter of value property.
     175
     176        Sets C{value} as the only element of L{values}.
     177
     178        @type value: C{bool}, C{unicode} or L{JID}
     179        """
     180        self.values = [value]
     181
     182
     183    def __value_get(self):
     184        """
     185        Getter of value property.
     186
     187        Returns the first element of L{values}, if present, or C{None}.
     188        """
     189
     190        if self.values:
     191            return self.values[0]
    28192        else:
    29             value = str(value)
    30 
    31         value_element = self.value or self.addElement('value')
    32         value_element.children = []
    33         value_element.addContent(value)
    34 
    35     def set_values(self, values):
    36         for value in values:
    37             value = str(value)
    38             self.addElement('value', content=value)
    39 
    40 class Option(domish.Element):
    41     def __init__(self, value, label=None):
    42         domish.Element.__init__(self, (NS_X_DATA, 'option'))
    43         if label is not None:
    44             self['label'] = label
    45         self.addElement('value', content=value)
    46 
    47 class Form(domish.Element):
    48     def __init__(self, type, form_type):
    49         domish.Element.__init__(self, (NS_X_DATA, 'x'),
    50                                 attribs={'type': type})
    51         self.add_field(type='hidden', var='FORM_TYPE', values=[form_type])
    52 
    53     def add_field(self, type='text-single', var=None, label=None,
    54                         value=None, values=[], options={}):
    55         self.addChild(Field(type, var, label, value, values, options))
     193            return None
     194
     195
     196    value = property(__value_get, __value_set, doc="""
     197            The value for this field, for single-valued field types.
     198
     199            This is a special property accessing L{values}.  Writing to this
     200            property empties L{values} and then sets the given value as the
     201            only element of L{values}.  Reading from this propery returns the
     202            first element of L{values}.
     203            """)
     204
     205
     206    def typeCheck(self):
     207        """
     208        Check field properties agains the set field type.
     209        """
     210        if self.var is None and self.fieldType != 'fixed':
     211            raise FieldNameRequiredError()
     212
     213        if self.values:
     214            if (self.fieldType not in ('hidden', 'jid-multi', 'list-multi',
     215                                 'text-multi') and
     216                len(self.values) > 1):
     217                raise TooManyValuesError()
     218
     219            newValues = []
     220            for value in self.values:
     221                if self.fieldType == 'boolean':
     222                    # We send out the textual representation of boolean values
     223                    value = bool(int(value))
     224                elif self.fieldType in ('jid-single', 'jid-multi'):
     225                    value = value.full()
     226
     227                newValues.append(value)
     228
     229            self.values = newValues
     230
     231    def toElement(self):
     232        """
     233        Return the DOM representation of this Field.
     234
     235        @rtype L{domish.Element}.
     236        """
     237
     238        self.typeCheck()
     239
     240        field = domish.Element((NS_X_DATA, 'field'))
     241        field['type'] = self.fieldType
     242
     243        if self.var is not None:
     244            field['var'] = self.var
     245
     246        for value in self.values:
     247            if self.fieldType == 'boolean':
     248                value = unicode(value).lower()
     249            field.addElement('value', content=value)
     250
     251        if self.fieldType in ('list-single', 'list-multi'):
     252            for option in self.options:
     253                field.addChild(option.toElement())
     254
     255        if self.label is not None:
     256            field['label'] = self.label
     257
     258        if self.desc is not None:
     259            field.addElement('desc', content=self.desc)
     260
     261        if self.required:
     262            field.addElement('required')
     263
     264        return field
     265
     266
     267    @staticmethod
     268    def _parse_desc(field, element):
     269        desc = unicode(element)
     270        if desc:
     271            field.desc = desc
     272
     273
     274    @staticmethod
     275    def _parse_option(field, element):
     276        field.options.append(Option.fromElement(element))
     277
     278
     279    @staticmethod
     280    def _parse_required(field, element):
     281        field.required = True
     282
     283
     284    @staticmethod
     285    def _parse_value(field, element):
     286        value = unicode(element)
     287        if field.fieldType == 'boolean':
     288            value = value.lower() in ('1', 'true')
     289        elif field.fieldType in ('jid-multi', 'jid-single'):
     290            value = JID(value)
     291        field.values.append(value)
     292
     293
     294    @staticmethod
     295    def fromElement(element):
     296        field = Field(None)
     297
     298        for eAttr, fAttr in {'type': 'fieldType',
     299                             'var': 'var',
     300                             'label': 'label'}.iteritems():
     301            value = element.getAttribute(eAttr)
     302            if value:
     303                setattr(field, fAttr, value)
     304
     305
     306        for child in element.elements():
     307            if child.uri != NS_X_DATA:
     308                continue
     309
     310            func = getattr(Field, '_parse_' + child.name, None)
     311            if func:
     312                func(field, child)
     313
     314        return field
     315
     316
     317    @staticmethod
     318    def fromDict(dictionary):
     319        kwargs = dictionary.copy()
     320
     321        if 'type' in dictionary:
     322            kwargs['fieldType'] = dictionary['type']
     323            del kwargs['type']
     324
     325        if 'options' in dictionary:
     326            options = []
     327            for value, label in dictionary['options'].iteritems():
     328                options.append(Option(value, label))
     329            kwargs['options'] = options
     330
     331        return Field(**kwargs)
     332
     333
     334
     335class Form(object):
     336    """
     337    Data Form.
     338
     339    There are two similarly named properties of forms. The L{formType} is the
     340    the so-called type of the form, and is set as the C{'type'} attribute
     341    on the form's root element.
     342
     343    The Field Standardization specification in XEP-0068, defines a way to
     344    provide a context for the field names used in this form, by setting a
     345    special hidden field named C{'FORM_TYPE'}, to put the names of all
     346    other fields in the namespace of the value of that field. This namespace
     347    is recorded in the L{formNamespace} instance variable.
     348
     349    @ivar formType: Type of form. One of C{'form'}, C{'submit'}, {'cancel'},
     350                    or {'result'}.
     351    @type formType: C{str}.
     352    @ivar formNamespace: The optional namespace of the field names for this
     353                         form. This goes in the special field named
     354                         C{'FORM_TYPE'}, if set.
     355    @type formNamespace: C{str}.
     356    @ivar fields: Dictionary of fields that have a name. Note that this is
     357                  meant to be used for reading, only. One should use
     358                  L{addField} for adding fields.
     359    @type fields: C{dict}
     360    """
     361
     362    def __init__(self, formType, title=None, instructions=None,
     363                       formNamespace=None, fields=None):
     364        self.formType = formType
     365        self.title = title
     366        self.instructions = instructions or []
     367        self.formNamespace = formNamespace
     368
     369        self.fieldList = []
     370        self.fields = {}
     371
     372        if fields:
     373            for field in fields:
     374                self.addField(field)
     375
     376    def __repr__(self):
     377        r = ["Form(formType=", repr(self.formType)]
     378
     379        if self.title:
     380            r.append(", title=")
     381            r.append(repr(self.title))
     382        if self.instructions:
     383            r.append(", instructions=")
     384            r.append(repr(self.instructions))
     385        if self.formNamespace:
     386            r.append(", formNamespace=")
     387            r.append(repr(self.formNamespace))
     388        if self.fields:
     389            r.append(", fields=")
     390            r.append(repr(self.fieldList))
     391        r.append(")")
     392        return u"".join(r)
     393
     394
     395    def addField(self, field):
     396        """
     397        Add a field to this form.
     398
     399        Fields are added in order, and L{fields} is a dictionary of the
     400        named fields, that is kept in sync only if this method is used for
     401        adding new fields. Multiple fields with the same name are disallowed.
     402        """
     403        if field.var is not None:
     404            if field.var in self.fields:
     405                raise Error("Duplicate field %r" % field.var)
     406
     407            self.fields[field.var] = field
     408
     409        self.fieldList.append(field)
     410
     411
     412    def toElement(self):
     413        form = domish.Element((NS_X_DATA, 'x'))
     414        form['type'] = self.formType
     415
     416        if self.title:
     417            form.addElement('title', content=self.title)
     418
     419        for instruction in self.instructions:
     420            form.addElement('instruction', content=instruction)
     421
     422        if self.formNamespace is not None:
     423            field = Field('hidden', 'FORM_TYPE', self.formNamespace)
     424            form.addChild(field.toElement())
     425
     426        for field in self.fieldList:
     427            form.addChild(field.toElement())
     428
     429        return form
     430
     431
     432    @staticmethod
     433    def _parse_title(form, element):
     434        title = unicode(element)
     435        if title:
     436            form.title = title
     437
     438
     439    @staticmethod
     440    def _parse_instructions(form, element):
     441        instructions = unicode(element)
     442        if instructions:
     443            form.instructions.append(instructions)
     444
     445
     446    @staticmethod
     447    def _parse_field(form, element):
     448        field = Field.fromElement(element)
     449        if (field.var == "FORM_TYPE" and
     450            field.fieldType == 'hidden' and
     451            field.value):
     452            form.formNamespace = field.value
     453        else:
     454            form.addField(field)
     455
     456    @staticmethod
     457    def fromElement(element):
     458        if (element.uri, element.name) != ((NS_X_DATA, 'x')):
     459            raise Error("Element provided is not a Data Form")
     460
     461        form = Form(element.getAttribute("type"))
     462
     463        for child in element.elements():
     464            if child.uri != NS_X_DATA:
     465                continue
     466
     467            func = getattr(Form, '_parse_' + child.name, None)
     468            if func:
     469                func(form, child)
     470
     471        return form
     472
     473    def getValues(self):
     474        values = {}
     475
     476        for name, field in self.fields.iteritems():
     477            if len(field.values) > 1:
     478                value = field.values
     479            else:
     480                value = field.value
     481
     482            values[name] = value
     483
     484        return values
  • wokkel/disco.py

    r6 r30  
    5454    """
    5555
    56     def __init__(self, jid, node = None, name = None):
     56    def __init__(self, jid, node='', name=None):
    5757        domish.Element.__init__(self, (NS_ITEMS, 'item'),
    5858                                attribs={'jid': jid.full()})
     
    8787        requestor = jid.internJID(iq["from"])
    8888        target = jid.internJID(iq["to"])
    89         nodeIdentifier = iq.query.getAttribute("node")
     89        nodeIdentifier = iq.query.getAttribute("node", '')
    9090
    9191        def toResponse(results):
     
    117117        requestor = jid.internJID(iq["from"])
    118118        target = jid.internJID(iq["to"])
    119         nodeIdentifier = iq.query.getAttribute("node")
     119        nodeIdentifier = iq.query.getAttribute("node", '')
    120120
    121121        def toResponse(results):
  • wokkel/generic.py

    r20 r30  
    1313from twisted.words.protocols.jabber import error
    1414from twisted.words.protocols.jabber.xmlstream import toResponse
     15from twisted.words.xish import domish
    1516
    1617from wokkel import disco
     
    2324NS_VERSION = 'jabber:iq:version'
    2425VERSION = IQ_GET + '/query[@xmlns="' + NS_VERSION + '"]'
     26
     27def parseXml(string):
     28    """
     29    Parse serialized XML into a DOM structure.
     30
     31    @param string: The serialized XML to be parsed, UTF-8 encoded.
     32    @type string: C{str}.
     33    @return: The DOM structure, or C{None} on empty or incomplete input.
     34    @rtype: L{domish.Element}
     35    """
     36    roots = []
     37    results = []
     38    elementStream = domish.elementStream()
     39    elementStream.DocumentStartEvent = roots.append
     40    elementStream.ElementEvent = lambda elem: roots[0].addChild(elem)
     41    elementStream.DocumentEndEvent = lambda: results.append(roots[0])
     42    elementStream.parse(string)
     43    return results and results[0] or None
     44
     45
     46
     47def stripNamespace(rootElement):
     48    namespace = rootElement.uri
     49
     50    def strip(element):
     51        if element.uri == namespace:
     52            element.uri = None
     53            if element.defaultUri == namespace:
     54                element.defaultUri = None
     55            for child in element.elements():
     56                strip(child)
     57
     58    if namespace is not None:
     59        strip(rootElement)
     60
     61    return rootElement
     62
     63
    2564
    2665class FallbackHandler(XMPPHandler):
  • wokkel/iwokkel.py

    r16 r30  
    107107    """
    108108
    109     def getDiscoInfo(requestor, target, nodeIdentifier=None):
     109    def getDiscoInfo(requestor, target, nodeIdentifier=''):
    110110        """
    111111        Get identity and features from this entity, node.
     
    117117        @param nodeIdentifier: The optional identifier of the node at this
    118118                               entity to retrieve the identify and features of.
    119                                The default is C{None}, meaning the root node.
    120         @type nodeIdentifier: C{unicode}
    121         """
    122 
    123     def getDiscoItems(requestor, target, nodeIdentifier=None):
     119                               The default is C{''}, meaning the root node.
     120        @type nodeIdentifier: C{unicode}
     121        """
     122
     123    def getDiscoItems(requestor, target, nodeIdentifier=''):
    124124        """
    125125        Get contained items for this entity, node.
     
    131131        @param nodeIdentifier: The optional identifier of the node at this
    132132                               entity to retrieve the identify and features of.
    133                                The default is C{None}, meaning the root node.
     133                               The default is C{''}, meaning the root node.
    134134        @type nodeIdentifier: C{unicode}
    135135        """
     
    138138class IPubSubClient(Interface):
    139139
    140     def itemsReceived(recipient, service, nodeIdentifier, items):
     140    def itemsReceived(event):
    141141        """
    142142        Called when an items notification has been received for a node.
     
    146146        accompanied with an item identifier in the C{id} attribute.
    147147
    148         @param recipient: The entity to which the notification was sent.
    149         @type recipient: L{jid.JID}
    150         @param service: The entity from which the notification was received.
    151         @type service: L{jid.JID}
    152         @param nodeIdentifier: Identifier of the node the items belong to.
    153         @type nodeIdentifier: C{unicode}
    154         @param items: List of received items as domish elements.
    155         @type items: C{list} of L{domish.Element}
    156         """
    157 
    158     def deleteReceived(recipient, service, nodeIdentifier, items):
     148        @param event: The items event.
     149        @type event: L{ItemsEvent<wokkel.pubsub.ItemsEvent>}
     150        """
     151
     152
     153    def deleteReceived(event):
    159154        """
    160155        Called when a deletion notification has been received for a node.
    161156
    162         @param recipient: The entity to which the notification was sent.
    163         @type recipient: L{jid.JID}
    164         @param service: The entity from which the notification was received.
    165         @type service: L{jid.JID}
    166         @param nodeIdentifier: Identifier of the node that has been deleted.
    167         @type nodeIdentifier: C{unicode}
    168         """
    169 
    170     def purgeReceived(recipient, service, nodeIdentifier, items):
     157        @param event: The items event.
     158        @type event: L{ItemsEvent<wokkel.pubsub.DeleteEvent>}
     159        """
     160
     161
     162    def purgeReceived(event):
    171163        """
    172164        Called when a purge notification has been received for a node.
     
    175167        considered retracted.
    176168
    177         @param recipient: The entity to which the notification was sent.
    178         @type recipient: L{jid.JID}
    179         @param service: The entity from which the notification was received.
    180         @type service: L{jid.JID}
    181         @param nodeIdentifier: Identifier of the node that has been purged.
    182         @type nodeIdentifier: C{unicode}
     169        @param event: The items event.
     170        @type event: L{ItemsEvent<wokkel.pubsub.PurgeEvent>}
    183171        """
    184172
     
    390378        """
    391379
     380    def getConfigurationOptions():
     381        """
     382        Retrieve all known node configuration options.
     383
     384        The returned dictionary holds the possible node configuration options
     385        by option name. The value of each entry represents the specifics for
     386        that option in a dictionary:
     387
     388        - C{'type'} (C{str}): The option's type (see
     389          L{Field<wokkel.data_form.Field>}'s doc string for possible values).
     390        - C{'label'} (C{unicode}): A human readable label for this option.
     391        - C{'options'} (C{dict}): Optional list of possible values for this
     392          option.
     393
     394        Example::
     395
     396            {
     397            "pubsub#persist_items":
     398                {"type": "boolean",
     399                 "label": "Persist items to storage"},
     400            "pubsub#deliver_payloads":
     401                {"type": "boolean",
     402                 "label": "Deliver payloads with event notifications"},
     403            "pubsub#send_last_published_item":
     404                {"type": "list-single",
     405                 "label": "When to send the last published item",
     406                 "options": {
     407                     "never": "Never",
     408                     "on_sub": "When a new subscription is processed"}
     409                }
     410            }
     411
     412        @rtype: C{dict}.
     413        """
     414
    392415    def getDefaultConfiguration(requestor, service):
    393416        """
  • wokkel/pubsub.py

    r19 r30  
    1717from twisted.words.xish import domish
    1818
    19 from wokkel import disco, data_form
     19from wokkel import disco, data_form, shim
    2020from wokkel.subprotocols import IQHandlerMixin, XMPPHandler
    2121from wokkel.iwokkel import IPubSubClient, IPubSubService
     
    7070PUBSUB_DELETE = PUBSUB_OWNER_SET + '/delete' + IN_NS_PUBSUB_OWNER
    7171
    72 class BadRequest(error.StanzaError):
    73     """
    74     Bad request stanza error.
    75     """
    76     def __init__(self):
    77         error.StanzaError.__init__(self, 'bad-request')
    78 
    79 
    80 
    8172class SubscriptionPending(Exception):
    8273    """
     
    10899
    109100
     101class BadRequest(PubSubError):
     102    """
     103    Bad request stanza error.
     104    """
     105    def __init__(self, pubsubCondition=None, text=None):
     106        PubSubError.__init__(self, 'bad-request', pubsubCondition, text)
     107
     108
     109
    110110class Unsupported(PubSubError):
    111111    def __init__(self, feature, text=None):
     
    117117
    118118
    119 class OptionsUnavailable(Unsupported):
    120     def __init__(self):
    121         Unsupported.__init__(self, 'subscription-options-unavailable')
     119class Subscription(object):
     120    """
     121    A subscription to a node.
     122
     123    @ivar nodeIdentifier: The identifier of the node subscribed to.
     124                          The root node is denoted by C{None}.
     125    @ivar subscriber: The subscribing entity.
     126    @ivar state: The subscription state. One of C{'subscribed'}, C{'pending'},
     127                 C{'unconfigured'}.
     128    @ivar options: Optional list of subscription options.
     129    @type options: C{dict}.
     130    """
     131
     132    def __init__(self, nodeIdentifier, subscriber, state, options=None):
     133        self.nodeIdentifier = nodeIdentifier
     134        self.subscriber = subscriber
     135        self.state = state
     136        self.options = options or {}
    122137
    123138
     
    174189        self.command = self.pubsub.addElement(verb)
    175190
     191
    176192    def send(self, to):
    177193        """
     
    186202        destination = to.full()
    187203        return xmlstream.IQ.send(self, destination)
     204
     205
     206
     207class PubSubEvent(object):
     208    """
     209    A publish subscribe event.
     210
     211    @param sender: The entity from which the notification was received.
     212    @type sender: L{jid.JID}
     213    @param recipient: The entity to which the notification was sent.
     214    @type recipient: L{wokkel.pubsub.ItemsEvent}
     215    @param nodeIdentifier: Identifier of the node the event pertains to.
     216    @type nodeIdentifier: C{unicode}
     217    @param headers: SHIM headers, see L{wokkel.shim.extractHeaders}.
     218    @type headers: L{dict}
     219    """
     220
     221    def __init__(self, sender, recipient, nodeIdentifier, headers):
     222        self.sender = sender
     223        self.recipient = recipient
     224        self.nodeIdentifier = nodeIdentifier
     225        self.headers = headers
     226
     227
     228
     229class ItemsEvent(PubSubEvent):
     230    """
     231    A publish-subscribe event that signifies new, updated and retracted items.
     232
     233    @param items: List of received items as domish elements.
     234    @type items: C{list} of L{domish.Element}
     235    """
     236
     237    def __init__(self, sender, recipient, nodeIdentifier, items, headers):
     238        PubSubEvent.__init__(self, sender, recipient, nodeIdentifier, headers)
     239        self.items = items
     240
     241
     242
     243class DeleteEvent(PubSubEvent):
     244    """
     245    A publish-subscribe event that signifies the deletion of a node.
     246    """
     247
     248
     249
     250class PurgeEvent(PubSubEvent):
     251    """
     252    A publish-subscribe event that signifies the purging of a node.
     253    """
    188254
    189255
     
    200266                                   NS_PUBSUB_EVENT, self._onEvent)
    201267
     268
    202269    def _onEvent(self, message):
    203270        try:
    204             service = jid.JID(message["from"])
     271            sender = jid.JID(message["from"])
    205272            recipient = jid.JID(message["to"])
    206273        except KeyError:
     
    218285
    219286        if eventHandler:
    220             eventHandler(service, recipient, actionElement)
     287            headers = shim.extractHeaders(message)
     288            eventHandler(sender, recipient, actionElement, headers)
    221289            message.handled = True
    222290
    223     def _onEvent_items(self, service, recipient, action):
     291
     292    def _onEvent_items(self, sender, recipient, action, headers):
    224293        nodeIdentifier = action["node"]
    225294
     
    227296                         if element.name in ('item', 'retract')]
    228297
    229         self.itemsReceived(recipient, service, nodeIdentifier, items)
    230 
    231     def _onEvent_delete(self, service, recipient, action):
     298        event = ItemsEvent(sender, recipient, nodeIdentifier, items, headers)
     299        self.itemsReceived(event)
     300
     301
     302    def _onEvent_delete(self, sender, recipient, action, headers):
    232303        nodeIdentifier = action["node"]
    233         self.deleteReceived(recipient, service, nodeIdentifier)
    234 
    235     def _onEvent_purge(self, service, recipient, action):
     304        event = DeleteEvent(sender, recipient, nodeIdentifier, headers)
     305        self.deleteReceived(event)
     306
     307
     308    def _onEvent_purge(self, sender, recipient, action, headers):
    236309        nodeIdentifier = action["node"]
    237         self.purgeReceived(recipient, service, nodeIdentifier)
    238 
    239     def itemsReceived(self, recipient, service, nodeIdentifier, items):
     310        event = PurgeEvent(sender, recipient, nodeIdentifier, headers)
     311        self.purgeReceived(event)
     312
     313
     314    def itemsReceived(self, event):
    240315        pass
    241316
    242     def deleteReceived(self, recipient, service, nodeIdentifier):
     317
     318    def deleteReceived(self, event):
    243319        pass
    244320
    245     def purgeReceived(self, recipient, service, nodeIdentifier):
     321
     322    def purgeReceived(self, event):
    246323        pass
     324
    247325
    248326    def createNode(self, service, nodeIdentifier=None):
     
    271349        return request.send(service).addCallback(cb)
    272350
     351
    273352    def deleteNode(self, service, nodeIdentifier):
    274353        """
     
    283362        request.command['node'] = nodeIdentifier
    284363        return request.send(service)
     364
    285365
    286366    def subscribe(self, service, nodeIdentifier, subscriber):
     
    297377        """
    298378        request = _PubSubRequest(self.xmlstream, 'subscribe')
    299         request.command['node'] = nodeIdentifier
     379        if nodeIdentifier:
     380            request.command['node'] = nodeIdentifier
    300381        request.command['jid'] = subscriber.full()
    301382
     
    315396        return request.send(service).addCallback(cb)
    316397
     398
    317399    def unsubscribe(self, service, nodeIdentifier, subscriber):
    318400        """
     
    327409        """
    328410        request = _PubSubRequest(self.xmlstream, 'unsubscribe')
    329         request.command['node'] = nodeIdentifier
     411        if nodeIdentifier:
     412            request.command['node'] = nodeIdentifier
    330413        request.command['jid'] = subscriber.full()
    331414        return request.send(service)
     415
    332416
    333417    def publish(self, service, nodeIdentifier, items=None):
     
    350434        return request.send(service)
    351435
     436
    352437    def items(self, service, nodeIdentifier, maxItems=None):
    353438        """
     
    362447        """
    363448        request = _PubSubRequest(self.xmlstream, 'items', method='get')
    364         request.command['node'] = nodeIdentifier
     449        if nodeIdentifier:
     450            request.command['node'] = nodeIdentifier
    365451        if maxItems:
    366452            request.command["max_items"] = str(int(maxItems))
     
    390476    handled as follows:
    391477
    392     * If the exception is an instance of L{error.StanzaError}, an error
    393       response iq is returned.
    394     * Any other exception is reported using L{log.msg}. An error response
    395       with the condition C{internal-server-error} is returned.
     478     - If the exception is an instance of L{error.StanzaError}, an error
     479       response iq is returned.
     480     - Any other exception is reported using L{log.msg}. An error response
     481       with the condition C{internal-server-error} is returned.
    396482
    397483    The default implementation of said methods raises an L{Unsupported}
     
    402488    @ivar pubSubFeatures: List of supported publish-subscribe features for
    403489                          service discovery, as C{str}.
    404     @type pubSubFeatures: C{list} or C{None}.
     490    @type pubSubFeatures: C{list} or C{None}
    405491    """
    406492
     
    431517            }
    432518
     519
    433520    def __init__(self):
    434521        self.discoIdentity = {'category': 'pubsub',
     
    437524
    438525        self.pubSubFeatures = []
     526
    439527
    440528    def connectionMade(self):
     
    444532        self.xmlstream.addObserver(PUBSUB_OWNER_SET, self.handleRequest)
    445533
     534
    446535    def getDiscoInfo(self, requestor, target, nodeIdentifier):
    447536        info = []
     
    454543                         for feature in self.pubSubFeatures])
    455544
    456             return defer.succeed(info)
    457         else:
    458             def toInfo(nodeInfo):
    459                 if not nodeInfo:
    460                     return []
    461 
    462                 (nodeType, metaData) = nodeInfo['type'], nodeInfo['meta-data']
    463                 info.append(disco.DiscoIdentity('pubsub', nodeType))
    464                 if metaData:
    465                     form = data_form.Form(type="result",
    466                                           form_type=NS_PUBSUB_META_DATA)
    467                     form.add_field("text-single",
    468                                    "pubsub#node_type",
    469                                    "The type of node (collection or leaf)",
    470                                    nodeType)
    471 
    472                     for metaDatum in metaData:
    473                         form.add_field(**metaDatum)
    474 
    475                     info.append(form)
    476                 return info
    477 
    478             d = self.getNodeInfo(requestor, target, nodeIdentifier)
    479             d.addCallback(toInfo)
    480             return d
     545        def toInfo(nodeInfo):
     546            if not nodeInfo:
     547                return
     548
     549            (nodeType, metaData) = nodeInfo['type'], nodeInfo['meta-data']
     550            info.append(disco.DiscoIdentity('pubsub', nodeType))
     551            if metaData:
     552                form = data_form.Form(formType="result",
     553                                      formNamespace=NS_PUBSUB_META_DATA)
     554                form.addField(
     555                        data_form.Field(
     556                            var='pubsub#node_type',
     557                            value=nodeType,
     558                            label='The type of node (collection or leaf)'
     559                        )
     560                )
     561
     562                for metaDatum in metaData:
     563                    form.addField(data_form.Field.fromDict(metaDatum))
     564
     565                info.append(form.toElement())
     566
     567        d = self.getNodeInfo(requestor, target, nodeIdentifier or '')
     568        d.addCallback(toInfo)
     569        d.addBoth(lambda result: info)
     570        return d
     571
    481572
    482573    def getDiscoItems(self, requestor, target, nodeIdentifier):
     
    489580        return d
    490581
    491     def _onPublish(self, iq):
     582
     583    def _findForm(self, element, formNamespace):
     584        if not element:
     585            return None
     586
     587        form = None
     588        for child in element.elements():
     589            try:
     590                form = data_form.Form.fromElement(child)
     591            except data_form.Error:
     592                continue
     593
     594            if form.formNamespace != NS_PUBSUB_NODE_CONFIG:
     595                continue
     596
     597        return form
     598
     599
     600    def _getParameter_node(self, commandElement):
     601        try:
     602            return commandElement["node"]
     603        except KeyError:
     604            raise BadRequest('nodeid-required')
     605
     606
     607    def _getParameter_nodeOrEmpty(self, commandElement):
     608        return commandElement.getAttribute("node", '')
     609
     610
     611    def _getParameter_jid(self, commandElement):
     612        try:
     613            return jid.internJID(commandElement["jid"])
     614        except KeyError:
     615            raise BadRequest('jid-required')
     616
     617
     618    def _getParameter_max_items(self, commandElement):
     619        value = commandElement.getAttribute('max_items')
     620
     621        if value:
     622            try:
     623                return int(value)
     624            except ValueError:
     625                raise BadRequest(text="Field max_items requires a positive " +
     626                                      "integer value")
     627        else:
     628            return None
     629
     630
     631    def _getParameters(self, iq, *names):
    492632        requestor = jid.internJID(iq["from"]).userhostJID()
    493633        service = jid.internJID(iq["to"])
    494634
    495         try:
    496             nodeIdentifier = iq.pubsub.publish["node"]
    497         except KeyError:
    498             raise BadRequest
     635        params = [requestor, service]
     636
     637        if names:
     638            command = names[0]
     639            commandElement = getattr(iq.pubsub, command)
     640            if not commandElement:
     641                raise Exception("Could not find command element %r" % command)
     642
     643        for name in names[1:]:
     644            try:
     645                getter = getattr(self, '_getParameter_' + name)
     646            except KeyError:
     647                raise Exception("No parameter getter for this name")
     648
     649            params.append(getter(commandElement))
     650
     651        return params
     652
     653
     654    def _onPublish(self, iq):
     655        requestor, service, nodeIdentifier = self._getParameters(
     656                iq, 'publish', 'node')
    499657
    500658        items = []
     
    505663        return self.publish(requestor, service, nodeIdentifier, items)
    506664
     665
    507666    def _onSubscribe(self, iq):
    508         requestor = jid.internJID(iq["from"]).userhostJID()
    509         service = jid.internJID(iq["to"])
    510 
    511         try:
    512             nodeIdentifier = iq.pubsub.subscribe["node"]
    513             subscriber = jid.internJID(iq.pubsub.subscribe["jid"])
    514         except KeyError:
    515             raise BadRequest
    516 
    517         def toResponse(subscription):
    518             nodeIdentifier, state = subscription
     667        requestor, service, nodeIdentifier, subscriber = self._getParameters(
     668                iq, 'subscribe', 'nodeOrEmpty', 'jid')
     669
     670        def toResponse(result):
    519671            response = domish.Element((NS_PUBSUB, "pubsub"))
    520672            subscription = response.addElement("subscription")
    521             subscription["node"] = nodeIdentifier
    522             subscription["jid"] = subscriber.full()
    523             subscription["subscription"] = state
     673            if result.nodeIdentifier:
     674                subscription["node"] = result.nodeIdentifier
     675            subscription["jid"] = result.subscriber.full()
     676            subscription["subscription"] = result.state
    524677            return response
    525678
     
    528681        return d
    529682
     683
    530684    def _onUnsubscribe(self, iq):
    531         requestor = jid.internJID(iq["from"]).userhostJID()
    532         service = jid.internJID(iq["to"])
    533 
    534         try:
    535             nodeIdentifier = iq.pubsub.unsubscribe["node"]
    536             subscriber = jid.internJID(iq.pubsub.unsubscribe["jid"])
    537         except KeyError:
    538             raise BadRequest
     685        requestor, service, nodeIdentifier, subscriber = self._getParameters(
     686                iq, 'unsubscribe', 'nodeOrEmpty', 'jid')
    539687
    540688        return self.unsubscribe(requestor, service, nodeIdentifier, subscriber)
    541689
     690
    542691    def _onOptionsGet(self, iq):
    543         raise Unsupported('subscription-options-unavailable')
     692        raise Unsupported('subscription-options')
     693
    544694
    545695    def _onOptionsSet(self, iq):
    546         raise Unsupported('subscription-options-unavailable')
     696        raise Unsupported('subscription-options')
     697
    547698
    548699    def _onSubscriptions(self, iq):
    549         requestor = jid.internJID(iq["from"]).userhostJID()
    550         service = jid.internJID(iq["to"])
     700        requestor, service = self._getParameters(iq)
    551701
    552702        def toResponse(result):
     
    564714        return d
    565715
     716
    566717    def _onAffiliations(self, iq):
    567         requestor = jid.internJID(iq["from"]).userhostJID()
    568         service = jid.internJID(iq["to"])
     718        requestor, service = self._getParameters(iq)
    569719
    570720        def toResponse(result):
     
    583733        return d
    584734
     735
    585736    def _onCreate(self, iq):
    586         requestor = jid.internJID(iq["from"]).userhostJID()
    587         service = jid.internJID(iq["to"])
     737        requestor, service = self._getParameters(iq)
    588738        nodeIdentifier = iq.pubsub.create.getAttribute("node")
    589739
     
    601751        return d
    602752
    603     def _formFromConfiguration(self, options):
    604         form = data_form.Form(type="form", form_type=NS_PUBSUB_NODE_CONFIG)
    605 
    606         for option in options:
    607             form.add_field(**option)
     753
     754    def _makeFields(self, options, values):
     755        fields = []
     756        for name, value in values.iteritems():
     757            if name not in options:
     758                continue
     759
     760            option = {'var': name}
     761            option.update(options[name])
     762            if isinstance(value, list):
     763                option['values'] = value
     764            else:
     765                option['value'] = value
     766            fields.append(data_form.Field.fromDict(option))
     767        return fields
     768
     769    def _formFromConfiguration(self, values):
     770        options = self.getConfigurationOptions()
     771        fields = self._makeFields(options, values)
     772        form = data_form.Form(formType="form",
     773                              formNamespace=NS_PUBSUB_NODE_CONFIG,
     774                              fields=fields)
    608775
    609776        return form
    610777
     778    def _checkConfiguration(self, values):
     779        options = self.getConfigurationOptions()
     780        processedValues = {}
     781
     782        for key, value in values.iteritems():
     783            if key not in options:
     784                continue
     785
     786            option = {'var': key}
     787            option.update(options[key])
     788            field = data_form.Field.fromDict(option)
     789            if isinstance(value, list):
     790                field.values = value
     791            else:
     792                field.value = value
     793            field.typeCheck()
     794
     795            if isinstance(value, list):
     796                processedValues[key] = field.values
     797            else:
     798                processedValues[key] = field.value
     799
     800        return processedValues
     801
     802
    611803    def _onDefault(self, iq):
    612         requestor = jid.internJID(iq["from"]).userhostJID()
    613         service = jid.internJID(iq["to"])
     804        requestor, service = self._getParameters(iq)
    614805
    615806        def toResponse(options):
    616807            response = domish.Element((NS_PUBSUB_OWNER, "pubsub"))
    617808            default = response.addElement("default")
    618             default.addChild(self._formFromConfiguration(options))
     809            default.addChild(self._formFromConfiguration(options).toElement())
    619810            return response
    620811
    621         d = self.getDefaultConfiguration(requestor, service)
     812        form = self._findForm(iq.pubsub.config, NS_PUBSUB_NODE_CONFIG)
     813        values = form and form.formType == 'result' and form.getValues() or {}
     814        nodeType = values.get('pubsub#node_type', 'leaf')
     815
     816        if nodeType not in ('leaf', 'collections'):
     817            return defer.fail(error.StanzaError('not-acceptable'))
     818
     819        d = self.getDefaultConfiguration(requestor, service, nodeType)
    622820        d.addCallback(toResponse)
    623821        return d
    624822
     823
    625824    def _onConfigureGet(self, iq):
    626         requestor = jid.internJID(iq["from"]).userhostJID()
    627         service = jid.internJID(iq["to"])
    628         nodeIdentifier = iq.pubsub.configure.getAttribute("node")
     825        requestor, service, nodeIdentifier = self._getParameters(
     826                iq, 'configure', 'nodeOrEmpty')
    629827
    630828        def toResponse(options):
    631829            response = domish.Element((NS_PUBSUB_OWNER, "pubsub"))
    632830            configure = response.addElement("configure")
    633             configure.addChild(self._formFromConfiguration(options))
     831            configure.addChild(self._formFromConfiguration(options).toElement())
    634832
    635833            if nodeIdentifier:
     
    642840        return d
    643841
     842
    644843    def _onConfigureSet(self, iq):
    645         requestor = jid.internJID(iq["from"]).userhostJID()
    646         service = jid.internJID(iq["to"])
    647         nodeIdentifier = iq.pubsub.configure["node"]
    648 
    649         def getFormOptions(self, form):
    650             options = {}
    651 
    652             for element in form.elements():
    653                 if element.name == 'field' and \
    654                    element.uri == data_form.NS_X_DATA:
    655                     try:
    656                         options[element["var"]] = str(element.value)
    657                     except (KeyError, AttributeError):
    658                         raise BadRequest
    659 
    660             return options
     844        requestor, service, nodeIdentifier = self._getParameters(
     845                iq, 'configure', 'nodeOrEmpty')
    661846
    662847        # Search configuration form with correct FORM_TYPE and process it
    663848
    664         for element in iq.pubsub.configure.elements():
    665             if element.name != 'x' or element.uri != data_form.NS_X_DATA:
    666                 continue
    667 
    668             type = element.getAttribute("type")
    669             if type == "cancel":
    670                 return None
    671             elif type != "submit":
    672                 continue
    673 
    674             options = getFormOptions(element)
    675 
    676             if options["FORM_TYPE"] == NS_PUBSUB + "#node_config":
    677                 del options["FORM_TYPE"]
     849        form = self._findForm(iq.pubsub.configure, NS_PUBSUB_NODE_CONFIG)
     850
     851        if form:
     852            if form.formType == 'submit':
     853                options = self._checkConfiguration(form.getValues())
     854
    678855                return self.setConfiguration(requestor, service,
    679856                                             nodeIdentifier, options)
    680 
    681         raise BadRequest
     857            elif form.formType == 'cancel':
     858                return None
     859
     860        raise BadRequest()
     861
    682862
    683863    def _onItems(self, iq):
    684         requestor = jid.internJID(iq["from"]).userhostJID()
    685         service = jid.internJID(iq["to"])
    686 
    687         try:
    688             nodeIdentifier = iq.pubsub.items["node"]
    689         except KeyError:
    690             raise BadRequest
    691 
    692         maxItems = iq.pubsub.items.getAttribute('max_items')
    693 
    694         if maxItems:
    695             try:
    696                 maxItems = int(maxItems)
    697             except ValueError:
    698                 raise BadRequest
     864        requestor, service, nodeIdentifier, maxItems = self._getParameters(
     865                iq, 'items', 'nodeOrEmpty', 'max_items')
    699866
    700867        itemIdentifiers = []
     
    704871                    itemIdentifiers.append(child["id"])
    705872                except KeyError:
    706                     raise BadRequest
     873                    raise BadRequest()
    707874
    708875        def toResponse(result):
    709876            response = domish.Element((NS_PUBSUB, 'pubsub'))
    710877            items = response.addElement('items')
    711             items["node"] = nodeIdentifier
     878            if nodeIdentifier:
     879                items["node"] = nodeIdentifier
    712880
    713881            for item in result:
    714                 items.addRawXml(item)
     882                items.addChild(item)
    715883
    716884            return response
     
    721889        return d
    722890
     891
    723892    def _onRetract(self, iq):
    724         requestor = jid.internJID(iq["from"]).userhostJID()
    725         service = jid.internJID(iq["to"])
    726 
    727         try:
    728             nodeIdentifier = iq.pubsub.retract["node"]
    729         except KeyError:
    730             raise BadRequest
     893        requestor, service, nodeIdentifier = self._getParameters(
     894                iq, 'retract', 'node')
    731895
    732896        itemIdentifiers = []
    733897        for child in iq.pubsub.retract.elements():
    734             if child.uri == NS_PUBSUB_OWNER and child.name == 'item':
     898            if child.uri == NS_PUBSUB and child.name == 'item':
    735899                try:
    736900                    itemIdentifiers.append(child["id"])
    737901                except KeyError:
    738                     raise BadRequest
     902                    raise BadRequest()
    739903
    740904        return self.retract(requestor, service, nodeIdentifier,
    741905                            itemIdentifiers)
    742906
     907
    743908    def _onPurge(self, iq):
    744         requestor = jid.internJID(iq["from"]).userhostJID()
    745         service = jid.internJID(iq["to"])
    746 
    747         try:
    748             nodeIdentifier = iq.pubsub.purge["node"]
    749         except KeyError:
    750             raise BadRequest
    751 
     909        requestor, service, nodeIdentifier = self._getParameters(
     910                iq, 'purge', 'node')
    752911        return self.purge(requestor, service, nodeIdentifier)
    753912
     913
    754914    def _onDelete(self, iq):
    755         requestor = jid.internJID(iq["from"]).userhostJID()
    756         service = jid.internJID(iq["to"])
    757 
    758         try:
    759             nodeIdentifier = iq.pubsub.delete["node"]
    760         except KeyError:
    761             raise BadRequest
    762 
     915        requestor, service, nodeIdentifier = self._getParameters(
     916                iq, 'delete', 'node')
    763917        return self.delete(requestor, service, nodeIdentifier)
     918
    764919
    765920    def _onAffiliationsGet(self, iq):
    766921        raise Unsupported('modify-affiliations')
    767922
     923
    768924    def _onAffiliationsSet(self, iq):
    769925        raise Unsupported('modify-affiliations')
    770926
     927
    771928    def _onSubscriptionsGet(self, iq):
    772929        raise Unsupported('manage-subscriptions')
    773930
     931
    774932    def _onSubscriptionsSet(self, iq):
    775933        raise Unsupported('manage-subscriptions')
     
    777935    # public methods
    778936
     937    def _createNotification(self, eventType, service, nodeIdentifier,
     938                                  subscriber, subscriptions=None):
     939        headers = []
     940
     941        if subscriptions:
     942            for subscription in subscriptions:
     943                if nodeIdentifier != subscription.nodeIdentifier:
     944                    headers.append(('Collection', subscription.nodeIdentifier))
     945
     946        message = domish.Element((None, "message"))
     947        message["from"] = service.full()
     948        message["to"] = subscriber.full()
     949        event = message.addElement((NS_PUBSUB_EVENT, "event"))
     950
     951        element = event.addElement(eventType)
     952        element["node"] = nodeIdentifier
     953
     954        if headers:
     955            message.addChild(shim.Headers(headers))
     956
     957        return message
     958
    779959    def notifyPublish(self, service, nodeIdentifier, notifications):
    780         for recipient, items in notifications:
    781             message = domish.Element((None, "message"))
    782             message["from"] = service.full()
    783             message["to"] = recipient.full()
    784             event = message.addElement((NS_PUBSUB_EVENT, "event"))
    785             element = event.addElement("items")
    786             element["node"] = nodeIdentifier
    787             element.children = items
     960        for subscriber, subscriptions, items in notifications:
     961            message = self._createNotification('items', service,
     962                                               nodeIdentifier, subscriber,
     963                                               subscriptions)
     964            message.event.items.children = items
    788965            self.send(message)
    789966
    790     def notifyDelete(self, service, nodeIdentifier, recipients):
    791         for recipient in recipients:
    792             message = domish.Element((None, "message"))
    793             message["from"] = service.full()
    794             message["to"] = recipient.full()
    795             event = message.addElement((NS_PUBSUB_EVENT, "event"))
    796             element = event.addElement("delete")
    797             element["node"] = nodeIdentifier
     967
     968    def notifyDelete(self, service, nodeIdentifier, subscriptions):
     969        for subscription in subscriptions:
     970            message = self._createNotification('delete', service,
     971                                               nodeIdentifier,
     972                                               subscription.subscriber)
    798973            self.send(message)
     974
    799975
    800976    def getNodeInfo(self, requestor, service, nodeIdentifier):
    801977        return None
    802978
     979
    803980    def getNodes(self, requestor, service):
    804981        return []
    805982
     983
    806984    def publish(self, requestor, service, nodeIdentifier, items):
    807985        raise Unsupported('publish')
    808986
     987
    809988    def subscribe(self, requestor, service, nodeIdentifier, subscriber):
    810989        raise Unsupported('subscribe')
    811990
     991
    812992    def unsubscribe(self, requestor, service, nodeIdentifier, subscriber):
    813993        raise Unsupported('subscribe')
    814994
     995
    815996    def subscriptions(self, requestor, service):
    816997        raise Unsupported('retrieve-subscriptions')
    817998
     999
    8181000    def affiliations(self, requestor, service):
    8191001        raise Unsupported('retrieve-affiliations')
    8201002
     1003
    8211004    def create(self, requestor, service, nodeIdentifier):
    8221005        raise Unsupported('create-nodes')
    8231006
     1007
     1008    def getConfigurationOptions(self):
     1009        return {}
     1010
     1011
    8241012    def getDefaultConfiguration(self, requestor, service):
    8251013        raise Unsupported('retrieve-default')
    8261014
     1015
    8271016    def getConfiguration(self, requestor, service, nodeIdentifier):
    8281017        raise Unsupported('config-node')
    8291018
     1019
    8301020    def setConfiguration(self, requestor, service, nodeIdentifier, options):
    8311021        raise Unsupported('config-node')
     1022
    8321023
    8331024    def items(self, requestor, service, nodeIdentifier, maxItems,
     
    8351026        raise Unsupported('retrieve-items')
    8361027
     1028
    8371029    def retract(self, requestor, service, nodeIdentifier, itemIdentifiers):
    8381030        raise Unsupported('retract-items')
    8391031
     1032
    8401033    def purge(self, requestor, service, nodeIdentifier):
    8411034        raise Unsupported('purge-nodes')
    8421035
     1036
    8431037    def delete(self, requestor, service, nodeIdentifier):
    8441038        raise Unsupported('delete-nodes')
  • wokkel/subprotocols.py

    r8 r22  
    235235    response of C{internal-server-error} to be sent.
    236236
     237    A typical way to use this mixin, is to set up L{xpath} observers on the
     238    C{xmlstream} to call handleRequest, for example in an overridden
     239    L{XMPPHandler.connectionMade}. It is likely a good idea to only listen for
     240    incoming iq get and/org iq set requests, and not for any iq, to prevent
     241    hijacking incoming responses to outgoing iq requests. An example:
     242
     243        >>> QUERY_ROSTER = "/query[@xmlns='jabber:iq:roster']"
     244        >>> class MyHandler(XMPPHandler, IQHandlerMixin):
     245        ...    iqHandlers = {"/iq[@type='get']" + QUERY_ROSTER: 'onRosterGet',
     246        ...                  "/iq[@type='set']" + QUERY_ROSTER: 'onRosterSet'}
     247        ...    def connectionMade(self):
     248        ...        self.xmlstream.addObserver(
     249        ...          "/iq[@type='get' or @type='set']" + QUERY_ROSTER,
     250        ...          self.handleRequest)
     251        ...    def onRosterGet(self, iq):
     252        ...        pass
     253        ...    def onRosterSet(self, iq):
     254        ...        pass
     255
    237256    @cvar iqHandlers: Mapping from XPath queries (as a string) to the method
    238257                      name that will handle requests that match the query.
  • wokkel/test/test_disco.py

    r6 r30  
    2222
    2323    def getDiscoInfo(self, requestor, target, nodeIdentifier):
    24         if nodeIdentifier is None:
     24        if not nodeIdentifier:
    2525            return defer.succeed([
    2626                disco.DiscoIdentity('dummy', 'generic', 'Generic Dummy Entity'),
  • wokkel/test/test_pubsub.py

    r19 r30  
    66"""
    77
     8from zope.interface import verify
     9
    810from twisted.trial import unittest
    911from twisted.internet import defer
    10 from twisted.words.xish import domish
     12from twisted.words.xish import domish, xpath
    1113from twisted.words.protocols.jabber import error
    1214from twisted.words.protocols.jabber.jid import JID
    1315
    14 from wokkel import pubsub
     16from wokkel import data_form, iwokkel, pubsub, shim
     17from wokkel.generic import parseXml
    1518from wokkel.test.helpers import XmlStreamStub
    1619
     
    2124
    2225NS_PUBSUB = 'http://jabber.org/protocol/pubsub'
     26NS_PUBSUB_CONFIG = 'http://jabber.org/protocol/pubsub#node_config'
    2327NS_PUBSUB_ERRORS = 'http://jabber.org/protocol/pubsub#errors'
    2428NS_PUBSUB_EVENT = 'http://jabber.org/protocol/pubsub#event'
     29NS_PUBSUB_OWNER = 'http://jabber.org/protocol/pubsub#owner'
    2530
    2631def calledAsync(fn):
     
    5156
    5257
    53     def test_event_items(self):
     58    def test_interface(self):
     59        """
     60        Do instances of L{pubsub.PubSubClient} provide L{iwokkel.IPubSubClient}?
     61        """
     62        verify.verifyObject(iwokkel.IPubSubClient, self.protocol)
     63
     64
     65    def test_eventItems(self):
    5466        """
    5567        Test receiving an items event resulting in a call to itemsReceived.
     
    6880        item3['id'] = 'item3'
    6981
    70         def itemsReceived(recipient, service, nodeIdentifier, items):
    71             self.assertEquals(JID('user@example.org/home'), recipient)
    72             self.assertEquals(JID('pubsub.example.org'), service)
    73             self.assertEquals('test', nodeIdentifier)
    74             self.assertEquals([item1, item2, item3], items)
     82        def itemsReceived(event):
     83            self.assertEquals(JID('user@example.org/home'), event.recipient)
     84            self.assertEquals(JID('pubsub.example.org'), event.sender)
     85            self.assertEquals('test', event.nodeIdentifier)
     86            self.assertEquals([item1, item2, item3], event.items)
     87
     88        d, self.protocol.itemsReceived = calledAsync(itemsReceived)
     89        self.stub.send(message)
     90        return d
     91
     92
     93    def test_eventItemsCollection(self):
     94        """
     95        Test receiving an items event resulting in a call to itemsReceived.
     96        """
     97        message = domish.Element((None, 'message'))
     98        message['from'] = 'pubsub.example.org'
     99        message['to'] = 'user@example.org/home'
     100        event = message.addElement((NS_PUBSUB_EVENT, 'event'))
     101        items = event.addElement('items')
     102        items['node'] = 'test'
     103
     104        headers = shim.Headers([('Collection', 'collection')])
     105        message.addChild(headers)
     106
     107        def itemsReceived(event):
     108            self.assertEquals(JID('user@example.org/home'), event.recipient)
     109            self.assertEquals(JID('pubsub.example.org'), event.sender)
     110            self.assertEquals('test', event.nodeIdentifier)
     111            self.assertEquals({'Collection': ['collection']}, event.headers)
    75112
    76113        d, self.protocol.itemsReceived = calledAsync(itemsReceived)
     
    90127        items['node'] = 'test'
    91128
    92         def deleteReceived(recipient, service, nodeIdentifier):
    93             self.assertEquals(JID('user@example.org/home'), recipient)
    94             self.assertEquals(JID('pubsub.example.org'), service)
    95             self.assertEquals('test', nodeIdentifier)
     129        def deleteReceived(event):
     130            self.assertEquals(JID('user@example.org/home'), event.recipient)
     131            self.assertEquals(JID('pubsub.example.org'), event.sender)
     132            self.assertEquals('test', event.nodeIdentifier)
    96133
    97134        d, self.protocol.deleteReceived = calledAsync(deleteReceived)
     
    111148        items['node'] = 'test'
    112149
    113         def purgeReceived(recipient, service, nodeIdentifier):
    114             self.assertEquals(JID('user@example.org/home'), recipient)
    115             self.assertEquals(JID('pubsub.example.org'), service)
    116             self.assertEquals('test', nodeIdentifier)
     150        def purgeReceived(event):
     151            self.assertEquals(JID('user@example.org/home'), event.recipient)
     152            self.assertEquals(JID('pubsub.example.org'), event.sender)
     153            self.assertEquals('test', event.nodeIdentifier)
    117154
    118155        d, self.protocol.purgeReceived = calledAsync(purgeReceived)
     
    432469
    433470class PubSubServiceTest(unittest.TestCase):
     471    """
     472    Tests for L{pubsub.PubSubService}.
     473    """
    434474
    435475    def setUp(self):
    436         self.output = []
    437 
    438     def send(self, obj):
    439         self.output.append(obj)
     476        self.service = pubsub.PubSubService()
     477
     478    def handleRequest(self, xml):
     479        """
     480        Find a handler and call it directly
     481        """
     482        handler = None
     483        iq = parseXml(xml)
     484        for queryString, method in self.service.iqHandlers.iteritems():
     485            if xpath.internQuery(queryString).matches(iq):
     486                handler = getattr(self.service, method)
     487
     488        if handler:
     489            d = defer.maybeDeferred(handler, iq)
     490        else:
     491            d = defer.fail(NotImplementedError())
     492
     493        return d
     494
     495
     496    def test_interface(self):
     497        """
     498        Do instances of L{pubsub.PubSubService} provide L{iwokkel.IPubSubService}?
     499        """
     500        verify.verifyObject(iwokkel.IPubSubService, self.service)
     501
    440502
    441503    def test_onPublishNoNode(self):
    442         handler = pubsub.PubSubService()
    443         handler.parent = self
    444         iq = domish.Element((None, 'iq'))
    445         iq['from'] = 'user@example.org'
    446         iq['to'] = 'pubsub.example.org'
    447         iq['type'] = 'set'
    448         iq.addElement((NS_PUBSUB, 'pubsub'))
    449         iq.pubsub.addElement('publish')
    450         handler.handleRequest(iq)
    451 
    452         e = error.exceptionFromStanza(self.output[-1])
    453         self.assertEquals('bad-request', e.condition)
     504        """
     505        The root node is always a collection, publishing is a bad request.
     506        """
     507        xml = """
     508        <iq type='set' to='pubsub.example.org'
     509                       from='user@example.org'>
     510          <pubsub xmlns='http://jabber.org/protocol/pubsub'>
     511            <publish/>
     512          </pubsub>
     513        </iq>
     514        """
     515
     516        def cb(result):
     517            self.assertEquals('bad-request', result.condition)
     518
     519        d = self.handleRequest(xml)
     520        self.assertFailure(d, error.StanzaError)
     521        d.addCallback(cb)
     522        return d
     523
    454524
    455525    def test_onPublish(self):
    456         class Handler(pubsub.PubSubService):
    457             def publish(self, *args, **kwargs):
    458                 self.args = args
    459                 self.kwargs = kwargs
    460 
    461         handler = Handler()
    462         handler.parent = self
    463         iq = domish.Element((None, 'iq'))
    464         iq['type'] = 'set'
    465         iq['from'] = 'user@example.org'
    466         iq['to'] = 'pubsub.example.org'
    467         iq.addElement((NS_PUBSUB, 'pubsub'))
    468         iq.pubsub.addElement('publish')
    469         iq.pubsub.publish['node'] = 'test'
    470         handler.handleRequest(iq)
    471 
    472         self.assertEqual((JID('user@example.org'),
    473                           JID('pubsub.example.org'), 'test', []), handler.args)
     526        """
     527        A publish request should result in L{PubSubService.publish} being
     528        called.
     529        """
     530
     531        xml = """
     532        <iq type='set' to='pubsub.example.org'
     533                       from='user@example.org'>
     534          <pubsub xmlns='http://jabber.org/protocol/pubsub'>
     535            <publish node='test'/>
     536          </pubsub>
     537        </iq>
     538        """
     539
     540        def publish(requestor, service, nodeIdentifier, items):
     541            self.assertEqual(JID('user@example.org'), requestor)
     542            self.assertEqual(JID('pubsub.example.org'), service)
     543            self.assertEqual('test', nodeIdentifier)
     544            self.assertEqual([], items)
     545            return defer.succeed(None)
     546
     547        self.service.publish = publish
     548        return self.handleRequest(xml)
     549
    474550
    475551    def test_onOptionsGet(self):
    476         handler = pubsub.PubSubService()
    477         handler.parent = self
    478         iq = domish.Element((None, 'iq'))
    479         iq['from'] = 'user@example.org'
    480         iq['to'] = 'pubsub.example.org'
    481         iq['type'] = 'get'
    482         iq.addElement((NS_PUBSUB, 'pubsub'))
    483         iq.pubsub.addElement('options')
    484         handler.handleRequest(iq)
    485 
    486         e = error.exceptionFromStanza(self.output[-1])
    487         self.assertEquals('feature-not-implemented', e.condition)
    488         self.assertEquals('unsupported', e.appCondition.name)
    489         self.assertEquals(NS_PUBSUB_ERRORS, e.appCondition.uri)
     552        """
     553        Subscription options are not supported.
     554        """
     555
     556        xml = """
     557        <iq type='get' to='pubsub.example.org'
     558                       from='user@example.org'>
     559          <pubsub xmlns='http://jabber.org/protocol/pubsub'>
     560            <options/>
     561          </pubsub>
     562        </iq>
     563        """
     564
     565        def cb(result):
     566            self.assertEquals('feature-not-implemented', result.condition)
     567            self.assertEquals('unsupported', result.appCondition.name)
     568            self.assertEquals(NS_PUBSUB_ERRORS, result.appCondition.uri)
     569
     570        d = self.handleRequest(xml)
     571        self.assertFailure(d, error.StanzaError)
     572        d.addCallback(cb)
     573        return d
     574
     575
     576    def test_onDefault(self):
     577        """
     578        A default request should result in
     579        L{PubSubService.getDefaultConfiguration} being called.
     580        """
     581
     582        xml = """
     583        <iq type='get' to='pubsub.example.org'
     584                       from='user@example.org'>
     585          <pubsub xmlns='http://jabber.org/protocol/pubsub#owner'>
     586            <default/>
     587          </pubsub>
     588        </iq>
     589        """
     590
     591        def getConfigurationOptions():
     592            return {
     593                "pubsub#persist_items":
     594                    {"type": "boolean",
     595                     "label": "Persist items to storage"},
     596                "pubsub#deliver_payloads":
     597                    {"type": "boolean",
     598                     "label": "Deliver payloads with event notifications"}
     599                }
     600
     601        def getDefaultConfiguration(requestor, service, nodeType):
     602            self.assertEqual(JID('user@example.org'), requestor)
     603            self.assertEqual(JID('pubsub.example.org'), service)
     604            self.assertEqual('leaf', nodeType)
     605            return defer.succeed({})
     606
     607        def cb(element):
     608            self.assertEqual('pubsub', element.name)
     609            self.assertEqual(NS_PUBSUB_OWNER, element.uri)
     610            self.assertEqual(NS_PUBSUB_OWNER, element.default.uri)
     611            form = data_form.Form.fromElement(element.default.x)
     612            self.assertEqual(NS_PUBSUB_CONFIG, form.formNamespace)
     613
     614        self.service.getConfigurationOptions = getConfigurationOptions
     615        self.service.getDefaultConfiguration = getDefaultConfiguration
     616        d = self.handleRequest(xml)
     617        d.addCallback(cb)
     618        return d
     619
     620
     621    def test_onConfigureGet(self):
     622        """
     623        On a node configuration get request L{PubSubService.getConfiguration}
     624        is called and results in a data form with the configuration.
     625        """
     626
     627        xml = """
     628        <iq type='get' to='pubsub.example.org'
     629                       from='user@example.org'>
     630          <pubsub xmlns='http://jabber.org/protocol/pubsub#owner'>
     631            <configure node='test'/>
     632          </pubsub>
     633        </iq>
     634        """
     635
     636        def getConfigurationOptions():
     637            return {
     638                "pubsub#persist_items":
     639                    {"type": "boolean",
     640                     "label": "Persist items to storage"},
     641                "pubsub#deliver_payloads":
     642                    {"type": "boolean",
     643                     "label": "Deliver payloads with event notifications"}
     644                }
     645
     646        def getConfiguration(requestor, service, nodeIdentifier):
     647            self.assertEqual(JID('user@example.org'), requestor)
     648            self.assertEqual(JID('pubsub.example.org'), service)
     649            self.assertEqual('test', nodeIdentifier)
     650
     651            return defer.succeed({'pubsub#deliver_payloads': '0',
     652                                  'pubsub#persist_items': '1'})
     653
     654        def cb(element):
     655            self.assertEqual('pubsub', element.name)
     656            self.assertEqual(NS_PUBSUB_OWNER, element.uri)
     657            self.assertEqual(NS_PUBSUB_OWNER, element.configure.uri)
     658            form = data_form.Form.fromElement(element.configure.x)
     659            self.assertEqual(NS_PUBSUB_CONFIG, form.formNamespace)
     660            fields = form.fields
     661
     662            self.assertIn('pubsub#deliver_payloads', fields)
     663            field = fields['pubsub#deliver_payloads']
     664            self.assertEqual('boolean', field.fieldType)
     665            self.assertEqual(False, field.value)
     666
     667            self.assertIn('pubsub#persist_items', fields)
     668            field = fields['pubsub#persist_items']
     669            self.assertEqual('boolean', field.fieldType)
     670            self.assertEqual(True, field.value)
     671
     672        self.service.getConfigurationOptions = getConfigurationOptions
     673        self.service.getConfiguration = getConfiguration
     674        d = self.handleRequest(xml)
     675        d.addCallback(cb)
     676        return d
     677
     678
     679    def test_onConfigureSet(self):
     680        """
     681        On a node configuration set request the Data Form is parsed and
     682        L{PubSubService.setConfiguration} is called with the passed options.
     683        """
     684
     685        xml = """
     686        <iq type='set' to='pubsub.example.org'
     687                       from='user@example.org'>
     688          <pubsub xmlns='http://jabber.org/protocol/pubsub#owner'>
     689            <configure node='test'>
     690              <x xmlns='jabber:x:data' type='submit'>
     691                <field var='FORM_TYPE' type='hidden'>
     692                  <value>http://jabber.org/protocol/pubsub#node_config</value>
     693                </field>
     694                <field var='pubsub#deliver_payloads'><value>0</value></field>
     695                <field var='pubsub#persist_items'><value>1</value></field>
     696              </x>
     697            </configure>
     698          </pubsub>
     699        </iq>
     700        """
     701
     702        def getConfigurationOptions():
     703            return {
     704                "pubsub#persist_items":
     705                    {"type": "boolean",
     706                     "label": "Persist items to storage"},
     707                "pubsub#deliver_payloads":
     708                    {"type": "boolean",
     709                     "label": "Deliver payloads with event notifications"}
     710                }
     711
     712        def setConfiguration(requestor, service, nodeIdentifier, options):
     713            self.assertEqual(JID('user@example.org'), requestor)
     714            self.assertEqual(JID('pubsub.example.org'), service)
     715            self.assertEqual('test', nodeIdentifier)
     716            self.assertEqual({'pubsub#deliver_payloads': False,
     717                              'pubsub#persist_items': True}, options)
     718            return defer.succeed(None)
     719
     720        self.service.getConfigurationOptions = getConfigurationOptions
     721        self.service.setConfiguration = setConfiguration
     722        return self.handleRequest(xml)
     723
     724
     725    def test_onConfigureSetCancel(self):
     726        """
     727        The node configuration is cancelled, L{PubSubService.setConfiguration}
     728        not called.
     729        """
     730
     731        xml = """
     732        <iq type='set' to='pubsub.example.org'
     733                       from='user@example.org'>
     734          <pubsub xmlns='http://jabber.org/protocol/pubsub#owner'>
     735            <configure node='test'>
     736              <x xmlns='jabber:x:data' type='cancel'>
     737                <field var='FORM_TYPE' type='hidden'>
     738                  <value>http://jabber.org/protocol/pubsub#node_config</value>
     739                </field>
     740              </x>
     741            </configure>
     742          </pubsub>
     743        </iq>
     744        """
     745
     746        def setConfiguration(requestor, service, nodeIdentifier, options):
     747            self.fail("Unexpected call to setConfiguration")
     748
     749        self.service.setConfiguration = setConfiguration
     750        return self.handleRequest(xml)
     751
     752
     753    def test_onConfigureSetIgnoreUnknown(self):
     754        """
     755        On a node configuration set request unknown fields should be ignored.
     756        """
     757
     758        xml = """
     759        <iq type='set' to='pubsub.example.org'
     760                       from='user@example.org'>
     761          <pubsub xmlns='http://jabber.org/protocol/pubsub#owner'>
     762            <configure node='test'>
     763              <x xmlns='jabber:x:data' type='submit'>
     764                <field var='FORM_TYPE' type='hidden'>
     765                  <value>http://jabber.org/protocol/pubsub#node_config</value>
     766                </field>
     767                <field var='pubsub#deliver_payloads'><value>0</value></field>
     768                <field var='x-myfield'><value>1</value></field>
     769              </x>
     770            </configure>
     771          </pubsub>
     772        </iq>
     773        """
     774
     775        def getConfigurationOptions():
     776            return {
     777                "pubsub#persist_items":
     778                    {"type": "boolean",
     779                     "label": "Persist items to storage"},
     780                "pubsub#deliver_payloads":
     781                    {"type": "boolean",
     782                     "label": "Deliver payloads with event notifications"}
     783                }
     784
     785        def setConfiguration(requestor, service, nodeIdentifier, options):
     786            self.assertEquals(['pubsub#deliver_payloads'], options.keys())
     787
     788        self.service.getConfigurationOptions = getConfigurationOptions
     789        self.service.setConfiguration = setConfiguration
     790        return self.handleRequest(xml)
     791
     792
     793    def test_onItems(self):
     794        """
     795        On a items request, return all items for the given node.
     796        """
     797        xml = """
     798        <iq type='get' to='pubsub.example.org'
     799                       from='user@example.org'>
     800          <pubsub xmlns='http://jabber.org/protocol/pubsub'>
     801            <items node='test'/>
     802          </pubsub>
     803        </iq>
     804        """
     805
     806        def items(requestor, service, nodeIdentifier, maxItems, items):
     807            self.assertEqual(JID('user@example.org'), requestor)
     808            self.assertEqual(JID('pubsub.example.org'), service)
     809            self.assertEqual('test', nodeIdentifier)
     810            self.assertIdentical(None, maxItems)
     811            self.assertEqual([], items)
     812            return defer.succeed([pubsub.Item('current')])
     813
     814        def cb(element):
     815            self.assertEqual(NS_PUBSUB, element.uri)
     816            self.assertEqual(NS_PUBSUB, element.items.uri)
     817            self.assertEqual(1, len(element.items.children))
     818            item = element.items.children[-1]
     819            self.assertTrue(domish.IElement.providedBy(item))
     820            self.assertEqual('item', item.name)
     821            self.assertEqual(NS_PUBSUB, item.uri)
     822            self.assertEqual('current', item['id'])
     823
     824        self.service.items = items
     825        d = self.handleRequest(xml)
     826        d.addCallback(cb)
     827        return d
     828
     829
     830    def test_onRetract(self):
     831        """
     832        A retract request should result in L{PubSubService.retract} being
     833        called.
     834        """
     835
     836        xml = """
     837        <iq type='set' to='pubsub.example.org'
     838                       from='user@example.org'>
     839          <pubsub xmlns='http://jabber.org/protocol/pubsub'>
     840            <retract node='test'>
     841              <item id='item1'/>
     842              <item id='item2'/>
     843            </retract>
     844          </pubsub>
     845        </iq>
     846        """
     847
     848        def retract(requestor, service, nodeIdentifier, itemIdentifiers):
     849            self.assertEqual(JID('user@example.org'), requestor)
     850            self.assertEqual(JID('pubsub.example.org'), service)
     851            self.assertEqual('test', nodeIdentifier)
     852            self.assertEqual(['item1', 'item2'], itemIdentifiers)
     853            return defer.succeed(None)
     854
     855        self.service.retract = retract
     856        return self.handleRequest(xml)
     857
     858
     859    def test_onPurge(self):
     860        """
     861        A purge request should result in L{PubSubService.purge} being
     862        called.
     863        """
     864
     865        xml = """
     866        <iq type='set' to='pubsub.example.org'
     867                       from='user@example.org'>
     868          <pubsub xmlns='http://jabber.org/protocol/pubsub#owner'>
     869            <purge node='test'/>
     870          </pubsub>
     871        </iq>
     872        """
     873
     874        def purge(requestor, service, nodeIdentifier):
     875            self.assertEqual(JID('user@example.org'), requestor)
     876            self.assertEqual(JID('pubsub.example.org'), service)
     877            self.assertEqual('test', nodeIdentifier)
     878            return defer.succeed(None)
     879
     880        self.service.purge = purge
     881        return self.handleRequest(xml)
     882
     883
     884    def test_onDelete(self):
     885        """
     886        A delete request should result in L{PubSubService.delete} being
     887        called.
     888        """
     889
     890        xml = """
     891        <iq type='set' to='pubsub.example.org'
     892                       from='user@example.org'>
     893          <pubsub xmlns='http://jabber.org/protocol/pubsub#owner'>
     894            <delete node='test'/>
     895          </pubsub>
     896        </iq>
     897        """
     898
     899        def delete(requestor, service, nodeIdentifier):
     900            self.assertEqual(JID('user@example.org'), requestor)
     901            self.assertEqual(JID('pubsub.example.org'), service)
     902            self.assertEqual('test', nodeIdentifier)
     903            return defer.succeed(None)
     904
     905        self.service.delete = delete
     906        return self.handleRequest(xml)
  • wokkel/test/test_xmppim.py

    r9 r28  
    88from twisted.trial import unittest
    99from twisted.words.protocols.jabber.jid import JID
     10from twisted.words.protocols.jabber.xmlstream import toResponse
     11from twisted.words.xish import domish
     12
    1013from wokkel import xmppim
     14from wokkel.test.helpers import XmlStreamStub
     15
     16NS_ROSTER = 'jabber:iq:roster'
    1117
    1218class PresenceClientProtocolTest(unittest.TestCase):
     
    6874        self.assertEquals(None, presence.getAttribute('to'))
    6975        self.assertEquals("unavailable", presence.getAttribute('type'))
     76
     77
     78class RosterClientProtocolTest(unittest.TestCase):
     79    """
     80    Tests for L{xmppim.RosterClientProtocol}.
     81    """
     82
     83    def setUp(self):
     84        self.stub = XmlStreamStub()
     85        self.protocol = xmppim.RosterClientProtocol()
     86        self.protocol.xmlstream = self.stub.xmlstream
     87        self.protocol.connectionInitialized()
     88
     89
     90    def test_removeItem(self):
     91        """
     92        Removing a roster item is setting an item with subscription C{remove}.
     93        """
     94        d = self.protocol.removeItem(JID('test@example.org'))
     95
     96        # Inspect outgoing iq request
     97
     98        iq = self.stub.output[-1]
     99        self.assertEquals('set', iq.getAttribute('type'))
     100        self.assertNotIdentical(None, iq.query)
     101        self.assertEquals(NS_ROSTER, iq.query.uri)
     102
     103        children = list(domish.generateElementsQNamed(iq.query.children,
     104                                                      'item', NS_ROSTER))
     105        self.assertEquals(1, len(children))
     106        child = children[0]
     107        self.assertEquals('test@example.org', child['jid'])
     108        self.assertEquals('remove', child['subscription'])
     109
     110        # Fake successful response
     111
     112        response = toResponse(iq, 'result')
     113        self.stub.send(response)
     114        return d
  • wokkel/xmppim.py

    r12 r28  
    328328        return d
    329329
     330
     331    def removeItem(self, entity):
     332        """
     333        Remove an item from the contact list.
     334
     335        @param entity: The contact to remove the roster item for.
     336        @type entity: L{JID<twisted.words.protocols.jabber.jid.JID>}
     337        @rtype: L{twisted.internet.defer.Deferred}
     338        """
     339        iq = IQ(self.xmlstream, 'set')
     340        iq.addElement((NS_ROSTER, 'query'))
     341        item = iq.query.addElement('item')
     342        item['jid'] = entity.full()
     343        item['subscription'] = 'remove'
     344        return iq.send()
     345
     346
    330347    def _onRosterSet(self, iq):
    331348        if iq.handled or \
Note: See TracChangeset for help on using the changeset viewer.