source: ralphm-patches/stanza-module.patch

Last change on this file was 79:0752a1cca356, checked in by Ralph Meijer <ralphm@…>, 6 years ago

Split out stanza module and response tracking patches, fix other patches.

File size: 17.8 KB
  • wokkel/generic.py

    # HG changeset patch
    # Parent  b4c3d0419ab21df80fcab1fe7b90523a90c12533
    
    diff --git a/wokkel/generic.py b/wokkel/generic.py
    a b  
    77Generic XMPP protocol helpers.
    88"""
    99
     10__all__ = ['parseXml', 'FallbackHandler', 'VersionHandler', 'XmlPipe',
     11           'DeferredXmlStreamFactory', 'prepareIDNName',
     12           'Stanza', 'Request', 'ErrorStanza']
     13
    1014from zope.interface import implements
    1115
    1216from twisted.internet import defer, protocol
    13 from twisted.python import reflect
    1417from twisted.python.deprecate import deprecated
    1518from twisted.python.versions import Version
    16 from twisted.words.protocols.jabber import error, jid, xmlstream
     19from twisted.words.protocols.jabber import error, xmlstream
    1720from twisted.words.protocols.jabber.xmlstream import toResponse
    1821from twisted.words.xish import domish, utility
    1922from twisted.words.xish.xmlstream import BootstrapMixin
    2023
    2124from wokkel.iwokkel import IDisco
     25from wokkel.stanza import Stanza, ErrorStanza, Request
    2226from wokkel.subprotocols import XMPPHandler
    2327
    2428IQ_GET = '/iq[@type="get"]'
     
    4751
    4852
    4953
    50 def stripNamespace(rootElement):
    51     namespace = rootElement.uri
    52 
    53     def strip(element):
    54         if element.uri == namespace:
    55             element.uri = None
    56             if element.defaultUri == namespace:
    57                 element.defaultUri = None
    58             for child in element.elements():
    59                 strip(child)
    60 
    61     if namespace is not None:
    62         strip(rootElement)
    63 
    64     return rootElement
    65 
    66 
    67 
    6854class FallbackHandler(XMPPHandler):
    6955    """
    7056    XMPP subprotocol handler that catches unhandled iq requests.
     
    162148
    163149
    164150
    165 class Stanza(object):
    166     """
    167     Abstract representation of a stanza.
    168 
    169     @ivar sender: The sending entity.
    170     @type sender: L{jid.JID}
    171     @ivar recipient: The receiving entity.
    172     @type recipient: L{jid.JID}
    173     """
    174 
    175     recipient = None
    176     sender = None
    177     stanzaKind = None
    178     stanzaID = None
    179     stanzaType = None
    180 
    181     def __init__(self, recipient=None, sender=None):
    182         self.recipient = recipient
    183         self.sender = sender
    184 
    185 
    186     @classmethod
    187     def fromElement(Class, element):
    188         """
    189         Create a stanza from a L{domish.Element}.
    190         """
    191         stanza = Class()
    192         stanza.parseElement(element)
    193         return stanza
    194 
    195 
    196     def parseElement(self, element):
    197         """
    198         Parse the stanza element.
    199 
    200         This is called with the stanza's element when a L{Stanza} is
    201         created using L{fromElement}. It parses the stanza's core attributes
    202         (addressing, type and id), strips the namespace from the stanza
    203         element for easier transport across streams and passes on
    204         child elements for further parsing.
    205 
    206         Child element parsers are defined by providing a C{childParsers}
    207         attribute on a subclass, as a mapping from (URI, name) to the name
    208         of the handler on C{self}. C{parseElement} will accumulate
    209         C{childParsers} from its class hierarchy, iterate over the child
    210         elements and pass it to matching handlers based on the child element's
    211         URI and name. The special key of C{None} can be used to pass all
    212         child elements to.
    213         """
    214         if element.hasAttribute('from'):
    215             self.sender = jid.internJID(element['from'])
    216         if element.hasAttribute('to'):
    217             self.recipient = jid.internJID(element['to'])
    218         self.stanzaType = element.getAttribute('type')
    219         self.stanzaID = element.getAttribute('id')
    220 
    221         # Save element
    222         stripNamespace(element)
    223         self.element = element
    224 
    225         # accumulate all childHandlers in the class hierarchy of Class
    226         handlers = {}
    227         reflect.accumulateClassDict(self.__class__, 'childParsers', handlers)
    228 
    229         for child in element.elements():
    230             try:
    231                 handler = handlers[child.uri, child.name]
    232             except KeyError:
    233                 try:
    234                     handler = handlers[None]
    235                 except KeyError:
    236                     continue
    237 
    238             getattr(self, handler)(child)
    239 
    240 
    241     def toElement(self):
    242         element = domish.Element((None, self.stanzaKind))
    243         if self.sender is not None:
    244             element['from'] = self.sender.full()
    245         if self.recipient is not None:
    246             element['to'] = self.recipient.full()
    247         if self.stanzaType:
    248             element['type'] = self.stanzaType
    249         if self.stanzaID:
    250             element['id'] = self.stanzaID
    251         return element
    252 
    253 
    254 
    255 class ErrorStanza(Stanza):
    256 
    257     def parseElement(self, element):
    258         Stanza.parseElement(self, element)
    259         self.exception = error.exceptionFromStanza(element)
    260 
    261 
    262 
    263 class Request(Stanza):
    264     """
    265     IQ request stanza.
    266 
    267     This is a base class for IQ get or set stanzas, to be used with
    268     L{wokkel.subprotocols.StreamManager.request}.
    269     """
    270 
    271     stanzaKind = 'iq'
    272     stanzaType = 'get'
    273     timeout = None
    274 
    275     childParsers = {None: 'parseRequest'}
    276 
    277     def __init__(self, recipient=None, sender=None, stanzaType=None):
    278         Stanza.__init__(self, recipient=recipient, sender=sender)
    279         if stanzaType is not None:
    280             self.stanzaType = stanzaType
    281 
    282 
    283     def parseRequest(self, element):
    284         """
    285         Called with the request's child element for parsing.
    286 
    287         When a request instance is created using L{fromElement}, this method
    288         is called with the child element of the iq. Override this method for
    289         parsing the request's payload.
    290         """
    291 
    292 
    293     def toElement(self):
    294         element = Stanza.toElement(self)
    295 
    296         if not self.stanzaID:
    297             element.addUniqueId()
    298             self.stanzaID = element['id']
    299 
    300         return element
    301 
    302 
    303 
    304151class DeferredXmlStreamFactory(BootstrapMixin, protocol.ClientFactory):
    305152    protocol = xmlstream.XmlStream
    306153
  • new file wokkel/stanza.py

    diff --git a/wokkel/stanza.py b/wokkel/stanza.py
    new file mode 100644
    - +  
     1# -*- test-case-name: wokkel.test.test_stanza -*-
     2#
     3# Copyright (c) Ralph Meijer.
     4# See LICENSE for details.
     5
     6"""
     7XMPP Stanza helpers.
     8"""
     9
     10from twisted.python import reflect
     11from twisted.words.protocols.jabber import error, jid
     12from twisted.words.xish import domish
     13
     14def stripNamespace(rootElement):
     15    namespace = rootElement.uri
     16
     17    def strip(element):
     18        if element.uri == namespace:
     19            element.uri = None
     20            if element.defaultUri == namespace:
     21                element.defaultUri = None
     22            for child in element.elements():
     23                strip(child)
     24
     25    if namespace is not None:
     26        strip(rootElement)
     27
     28    return rootElement
     29
     30
     31
     32class Stanza(object):
     33    """
     34    Abstract representation of a stanza.
     35
     36    @ivar recipient: The receiving entity.
     37    @type recipient: L{jid.JID}
     38
     39    @ivar sender: The sending entity.
     40    @type sender: L{jid.JID}
     41
     42    @ivar stanzaKind: One of C{'message'}, C{'presence'}, C{'iq'}.
     43    @type stanzaKind: L{unicode}.
     44
     45    @ivar stanzaID: The optional stanza identifier.
     46    @type stanzaID: L{unicode}.
     47
     48    @ivar stanzaType: The optional stanza type.
     49    @type stanzaType: L{unicode}.
     50
     51    @ivar element: The serialized XML of this stanza.
     52    @type element: L{domish.Element}.
     53    """
     54
     55    recipient = None
     56    sender = None
     57    stanzaKind = None
     58    stanzaID = None
     59    stanzaType = None
     60    element = None
     61
     62    def __init__(self, recipient=None, sender=None):
     63        self.recipient = recipient
     64        self.sender = sender
     65
     66
     67    @classmethod
     68    def fromElement(Class, element):
     69        """
     70        Create a stanza from a L{domish.Element}.
     71        """
     72        stanza = Class()
     73        stanza.parseElement(element)
     74        return stanza
     75
     76
     77    def parseElement(self, element):
     78        """
     79        Parse the stanza element.
     80
     81        This is called with the stanza's element when a L{Stanza} is
     82        created using L{fromElement}. It parses the stanza's core attributes
     83        (addressing, type and id), strips the namespace from the stanza
     84        element for easier transport across streams and passes on
     85        child elements for further parsing.
     86
     87        Child element parsers are defined by providing a C{childParsers}
     88        attribute on a subclass, as a mapping from (URI, name) to the name
     89        of the handler on C{self}. C{parseElement} will accumulate
     90        C{childParsers} from its class hierarchy, iterate over the child
     91        elements and pass it to matching handlers based on the child element's
     92        URI and name. The special key of C{None} can be used to pass all
     93        child elements to.
     94        """
     95        if element.hasAttribute('from'):
     96            self.sender = jid.internJID(element['from'])
     97        if element.hasAttribute('to'):
     98            self.recipient = jid.internJID(element['to'])
     99        self.stanzaKind = element.name
     100        self.stanzaType = element.getAttribute('type')
     101        self.stanzaID = element.getAttribute('id')
     102
     103        # Save element
     104        stripNamespace(element)
     105        self.element = element
     106
     107        # accumulate all childHandlers in the class hierarchy of Class
     108        handlers = {}
     109        reflect.accumulateClassDict(self.__class__, 'childParsers', handlers)
     110
     111        for child in element.elements():
     112            try:
     113                handler = handlers[child.uri, child.name]
     114            except KeyError:
     115                try:
     116                    handler = handlers[None]
     117                except KeyError:
     118                    continue
     119
     120            getattr(self, handler)(child)
     121
     122
     123    def toElement(self):
     124        element = domish.Element((None, self.stanzaKind))
     125        self.element = element
     126        if self.sender is not None:
     127            element['from'] = self.sender.full()
     128        if self.recipient is not None:
     129            element['to'] = self.recipient.full()
     130        if self.stanzaType:
     131            element['type'] = self.stanzaType
     132        if self.stanzaID:
     133            element['id'] = self.stanzaID
     134        return element
     135
     136
     137
     138class ErrorStanza(Stanza):
     139
     140    def parseElement(self, element):
     141        Stanza.parseElement(self, element)
     142        self.exception = error.exceptionFromStanza(element)
     143
     144
     145
     146class Request(Stanza):
     147    """
     148    IQ request stanza.
     149
     150    This is a base class for IQ get or set stanzas, to be used with
     151    L{wokkel.subprotocols.StreamManager.request}.
     152    """
     153
     154    stanzaKind = 'iq'
     155    stanzaType = 'get'
     156    timeout = None
     157
     158    childParsers = {None: 'parseRequest'}
     159
     160    def __init__(self, recipient=None, sender=None, stanzaType=None):
     161        Stanza.__init__(self, recipient=recipient, sender=sender)
     162        if stanzaType is not None:
     163            self.stanzaType = stanzaType
     164
     165
     166    def parseRequest(self, element):
     167        """
     168        Called with the request's child element for parsing.
     169
     170        When a request instance is created using L{fromElement}, this method
     171        is called with the child element of the iq. Override this method for
     172        parsing the request's payload.
     173        """
     174
     175
     176    def toElement(self):
     177        element = Stanza.toElement(self)
     178
     179        if not self.stanzaID:
     180            element.addUniqueId()
     181            self.stanzaID = element['id']
     182
     183        return element
     184
     185
     186
  • new file wokkel/test/test_stanza.py

    diff --git a/wokkel/test/test_stanza.py b/wokkel/test/test_stanza.py
    new file mode 100644
    - +  
     1# Copyright (c) Ralph Meijer.
     2# See LICENSE for details.
     3
     4from twisted.trial import unittest
     5from twisted.words.protocols.jabber.jid import JID
     6
     7from wokkel.generic import parseXml
     8from wokkel.stanza import Request, Stanza
     9
     10NS_VERSION = 'jabber:iq:version'
     11
     12"""
     13Tests for L{wokkel.stanza}.
     14"""
     15
     16
     17
     18class StanzaTest(unittest.TestCase):
     19    """
     20    Tests for L{Stanza}.
     21    """
     22
     23    def test_fromElement(self):
     24        xml = """
     25        <message type='chat' from='other@example.org' to='user@example.org'/>
     26        """
     27
     28        element = parseXml(xml)
     29        stanza = Stanza.fromElement(element)
     30        self.assertEqual('chat', stanza.stanzaType)
     31        self.assertEqual(JID('other@example.org'), stanza.sender)
     32        self.assertEqual(JID('user@example.org'), stanza.recipient)
     33        self.assertIdentical(element, stanza.element)
     34
     35
     36    def test_fromElementStanzaKind(self):
     37        """
     38        The stanza kind is also recorded in the stanza.
     39        """
     40        xml = """<presence/>"""
     41        stanza = Stanza.fromElement(parseXml(xml))
     42        self.assertEqual(u'presence', stanza.stanzaKind)
     43
     44
     45    def test_fromElementChildParser(self):
     46        """
     47        Child elements for which no parser is defined are ignored.
     48        """
     49        xml = """
     50        <message from='other@example.org' to='user@example.org'>
     51          <x xmlns='http://example.org/'/>
     52        </message>
     53        """
     54
     55        class Message(Stanza):
     56            childParsers = {('http://example.org/', 'x'): '_childParser_x'}
     57            elements = []
     58
     59            def _childParser_x(self, element):
     60                self.elements.append(element)
     61
     62        message = Message.fromElement(parseXml(xml))
     63        self.assertEqual(1, len(message.elements))
     64
     65
     66    def test_fromElementChildParserAll(self):
     67        """
     68        Child elements for which no parser is defined are ignored.
     69        """
     70        xml = """
     71        <message from='other@example.org' to='user@example.org'>
     72          <x xmlns='http://example.org/'/>
     73        </message>
     74        """
     75
     76        class Message(Stanza):
     77            childParsers = {None: '_childParser'}
     78            elements = []
     79
     80            def _childParser(self, element):
     81                self.elements.append(element)
     82
     83        message = Message.fromElement(parseXml(xml))
     84        self.assertEqual(1, len(message.elements))
     85
     86
     87    def test_fromElementChildParserUnknown(self):
     88        """
     89        Child elements for which no parser is defined are ignored.
     90        """
     91        xml = """
     92        <message from='other@example.org' to='user@example.org'>
     93          <x xmlns='http://example.org/'/>
     94        </message>
     95        """
     96        Stanza.fromElement(parseXml(xml))
     97
     98
     99
     100
     101class RequestTest(unittest.TestCase):
     102    """
     103    Tests for L{Request}.
     104    """
     105
     106    def setUp(self):
     107        self.request = Request()
     108
     109
     110    def test_requestParser(self):
     111        """
     112        The request's child element is passed to requestParser.
     113        """
     114        xml = """
     115        <iq type='get'>
     116          <query xmlns='jabber:iq:version'/>
     117        </iq>
     118        """
     119
     120        class VersionRequest(Request):
     121            elements = []
     122
     123            def parseRequest(self, element):
     124                self.elements.append((element.uri, element.name))
     125
     126        request = VersionRequest.fromElement(parseXml(xml))
     127        self.assertEqual([(NS_VERSION, 'query')], request.elements)
     128
     129
     130    def test_toElementStanzaKind(self):
     131        """
     132        A request is an iq stanza.
     133        """
     134        element = self.request.toElement()
     135        self.assertIdentical(None, element.uri)
     136        self.assertEquals('iq', element.name)
     137
     138
     139    def test_toElementStanzaType(self):
     140        """
     141        The request has type 'get'.
     142        """
     143        self.assertEquals('get', self.request.stanzaType)
     144        element = self.request.toElement()
     145        self.assertEquals('get', element.getAttribute('type'))
     146
     147
     148    def test_toElementStanzaTypeSet(self):
     149        """
     150        The request has type 'set'.
     151        """
     152        self.request.stanzaType = 'set'
     153        element = self.request.toElement()
     154        self.assertEquals('set', element.getAttribute('type'))
     155
     156
     157    def test_toElementStanzaID(self):
     158        """
     159        A request, when rendered, has an identifier.
     160        """
     161        element = self.request.toElement()
     162        self.assertNotIdentical(None, self.request.stanzaID)
     163        self.assertEquals(self.request.stanzaID, element.getAttribute('id'))
     164
     165
     166    def test_toElementRecipient(self):
     167        """
     168        A request without recipient, has no 'to' attribute.
     169        """
     170        self.request = Request(recipient=JID('other@example.org'))
     171        self.assertEquals(JID('other@example.org'), self.request.recipient)
     172        element = self.request.toElement()
     173        self.assertEquals(u'other@example.org', element.getAttribute('to'))
     174
     175
     176    def test_toElementRecipientNone(self):
     177        """
     178        A request without recipient, has no 'to' attribute.
     179        """
     180        element = self.request.toElement()
     181        self.assertFalse(element.hasAttribute('to'))
     182
     183
     184    def test_toElementSender(self):
     185        """
     186        A request with sender, has a 'from' attribute.
     187        """
     188        self.request = Request(sender=JID('user@example.org'))
     189        self.assertEquals(JID('user@example.org'), self.request.sender)
     190        element = self.request.toElement()
     191        self.assertEquals(u'user@example.org', element.getAttribute('from'))
     192
     193
     194    def test_toElementSenderNone(self):
     195        """
     196        A request without sender, has no 'from' attribute.
     197        """
     198        element = self.request.toElement()
     199        self.assertFalse(element.hasAttribute('from'))
     200
     201
     202    def test_timeoutDefault(self):
     203        """
     204        The default is no timeout.
     205        """
     206        self.assertIdentical(None, self.request.timeout)
     207
     208
     209    def test_stanzaTypeInit(self):
     210        """
     211        If stanzaType is passed in __init__, it overrides the class variable.
     212        """
     213
     214        class SetRequest(Request):
     215            stanzaType = 'set'
     216
     217        request = SetRequest(stanzaType='get')
     218        self.assertEqual('get', request.stanzaType)
     219
     220
     221    def test_stanzaTypeClass(self):
     222        """
     223        If stanzaType is not passed in __init__, the class variable is used.
     224        """
     225
     226        class SetRequest(Request):
     227            stanzaType = 'set'
     228
     229        request = SetRequest()
     230        self.assertEqual('set', request.stanzaType)
Note: See TracBrowser for help on using the repository browser.