Ignore:
Files:
11 added
13 edited

Legend:

Unmodified
Added
Removed
  • NEWS

    r60 r73  
     10.6.3 (2009-08-20)
     2==================
     3
     4Features
     5--------
     6
     7 - Add a jid attribute to XMPPClient (#18).
     8 - Add a better presence protocol handler PresenceProtocol. This handler
     9   is also useful for component or in-server use.
     10
     11Fixes
     12-----
     13
     14 - Use fallback port 5222 for failed SRV lookups for clients (#26).
     15
     16
     170.6.2 (2009-07-08)
     18==================
     19
     20Features
     21--------
     22
     23 - Add support for XMPP Ping (XEP-0199), doubling as example protocol
     24   handler (#55).
     25 - Provide examples for setting up clients, components and servers (#55).
     26 - Make Service Discovery support accept non-deferred results from getDiscoInfo
     27   and getDiscoItems (#55).
     28
     29
     300.6.1 (2009-07-06)
     31==================
     32
     33Features
     34--------
     35
     36 - Add an optional sender parameter for Service Discovery requests (#52).
     37
     38Fixes:
     39------
     40
     41 - Fix regression in DeferredClientFactory (#51).
     42 - Make IQ timeouts work with InternalComponent (#53).
     43
     44
    1450.6.0 (2009-04-22)
    246==================
  • README

    r60 r73  
    1 Wokkel 0.6.0
     1Wokkel 0.6.3
    22
    33What is this?
  • setup.py

    r60 r73  
    77
    88setup(name='wokkel',
    9       version='0.6.0',
     9      version='0.6.3',
    1010      description='Twisted Jabber support library',
    1111      author='Ralph Meijer',
  • wokkel/client.py

    r55 r73  
    11# -*- test-case-name: wokkel.test.test_client -*-
    22#
    3 # Copyright (c) 2003-2007 Ralph Meijer
     3# Copyright (c) 2003-2009 Ralph Meijer
    44# See LICENSE for details.
    55
     
    8080
    8181
     82
    8283class XMPPClient(StreamManager, service.Service):
    8384    """
     
    8687
    8788    def __init__(self, jid, password, host=None, port=5222):
     89        self.jid = jid
    8890        self.domain = jid.host
    8991        self.host = host
     
    9496        StreamManager.__init__(self, factory)
    9597
     98
    9699    def startService(self):
    97100        service.Service.startService(self)
    98101
    99102        self._connection = self._getConnection()
     103
    100104
    101105    def stopService(self):
     
    104108        self.factory.stopTrying()
    105109        self._connection.disconnect()
     110
     111
     112    def _authd(self, xs):
     113        """
     114        Called when the stream has been initialized.
     115
     116        Save the JID that we were assigned by the server, as the resource might
     117        differ from the JID we asked for. This is stored on the authenticator
     118        by its constituent initializers.
     119        """
     120        self.jid = self.factory.authenticator.jid
     121        StreamManager._authd(self, xs)
     122
    106123
    107124    def initializationFailed(self, reason):
     
    115132        reason.raiseException()
    116133
     134
    117135    def _getConnection(self):
    118136        if self.host:
    119137            return reactor.connectTCP(self.host, self.port, self.factory)
    120138        else:
    121             c = SRVConnector(reactor, 'xmpp-client', self.domain, self.factory)
     139            c = XMPPClientConnector(reactor, self.domain, self.factory)
    122140            c.connect()
    123141            return c
     142
    124143
    125144
     
    129148        authenticator = client.XMPPAuthenticator(jid, password)
    130149        generic.DeferredXmlStreamFactory.__init__(self, authenticator)
     150        self.streamManager = StreamManager(self)
    131151
    132152
     
    146166
    147167
     168class XMPPClientConnector(SRVConnector):
     169    def __init__(self, reactor, domain, factory):
     170        SRVConnector.__init__(self, reactor, 'xmpp-client', domain, factory)
     171
     172
     173    def pickServer(self):
     174        host, port = SRVConnector.pickServer(self)
     175
     176        if not self.servers and not self.orderedServers:
     177            # no SRV record, fall back..
     178            port = 5222
     179
     180        return host, port
     181
     182
     183
    148184def clientCreator(factory):
    149185    domain = factory.authenticator.jid.host
    150     c = SRVConnector(reactor, 'xmpp-client', domain, factory)
     186    c = XMPPClientConnector(reactor, domain, factory)
    151187    c.connect()
    152188    return factory.deferred
  • wokkel/compat.py

    r53 r63  
    11# -*- test-case-name: wokkel.test.test_compat -*-
    22#
    3 # Copyright (c) 2001-2008 Twisted Matrix Laboratories.
     3# Copyright (c) 2001-2009 Twisted Matrix Laboratories.
    44# See LICENSE for details.
    55
    66from twisted.internet import protocol
    77from twisted.words.protocols.jabber import xmlstream
    8 from twisted.words.xish import domish
    98
    109class BootstrapMixin(object):
     
    9594        self.installBootstraps(xs)
    9695        return xs
     96
     97
     98
     99class IQ(xmlstream.IQ):
     100    def __init__(self, *args, **kwargs):
     101        # Make sure we have a reactor parameter
     102        try:
     103            reactor = kwargs['reactor']
     104        except KeyError:
     105            from twisted.internet import reactor
     106        kwargs['reactor'] = reactor
     107
     108        # Check if IQ's init accepts the reactor parameter
     109        try:
     110            xmlstream.IQ.__init__(self, *args, **kwargs)
     111        except TypeError:
     112            # Guess not. Remove the reactor parameter and try again.
     113            del kwargs['reactor']
     114            xmlstream.IQ.__init__(self, *args, **kwargs)
     115
     116            # Patch the XmlStream instance so that it has a _callLater
     117            self._xmlstream._callLater = reactor.callLater
  • wokkel/disco.py

    r130 r133  
    1212
    1313from twisted.internet import defer
    14 from twisted.words.protocols.jabber import error, jid, xmlstream
     14from twisted.words.protocols.jabber import error, jid
    1515from twisted.words.xish import domish
    1616
    1717from wokkel import data_form
     18from wokkel.compat import IQ
    1819from wokkel.iwokkel import IDisco
    1920from wokkel.subprotocols import IQHandlerMixin, XMPPHandler
     
    346347
    347348
    348 class _DiscoRequest(xmlstream.IQ):
     349class _DiscoRequest(IQ):
    349350    """
    350351    Element representing an XMPP service discovery request.
     
    362363        @type nodeIdentifier: C{unicode}
    363364        """
    364         xmlstream.IQ.__init__(self, xs, "get")
     365        IQ.__init__(self, xs, "get")
    365366        query = self.addElement((namespace, 'query'))
    366367        if nodeIdentifier:
     
    374375    """
    375376
    376     def requestInfo(self, entity, nodeIdentifier=''):
     377    def requestInfo(self, entity, nodeIdentifier='', sender=None):
    377378        """
    378379        Request information discovery from a node.
     
    380381        @param entity: Entity to send the request to.
    381382        @type entity: L{jid.JID}
     383
    382384        @param nodeIdentifier: Optional node to request info from.
    383385        @type nodeIdentifier: C{unicode}
     386
     387        @param sender: Optional sender address.
     388        @type sender: L{jid.JID}
    384389        """
    385390
    386391        request = _DiscoRequest(self.xmlstream, NS_DISCO_INFO, nodeIdentifier)
     392        if sender is not None:
     393            request['from'] = unicode(sender)
    387394
    388395        d = request.send(entity.full())
     
    391398
    392399
    393     def requestItems(self, entity, nodeIdentifier=''):
     400    def requestItems(self, entity, nodeIdentifier='', sender=None):
    394401        """
    395402        Request items discovery from a node.
     
    397404        @param entity: Entity to send the request to.
    398405        @type entity: L{jid.JID}
     406
    399407        @param nodeIdentifier: Optional node to request info from.
    400408        @type nodeIdentifier: C{unicode}
     409
     410        @param sender: Optional sender address.
     411        @type sender: L{jid.JID}
    401412        """
    402413
    403414        request = _DiscoRequest(self.xmlstream, NS_DISCO_ITEMS, nodeIdentifier)
     415        if sender is not None:
     416            request['from'] = unicode(sender)
    404417
    405418        d = request.send(entity.full())
     
    526539        @rtype: L{defer.Deferred}
    527540        """
    528         dl = [handler.getDiscoInfo(requestor, target, nodeIdentifier)
     541        dl = [defer.maybeDeferred(handler.getDiscoInfo, requestor, target,
     542                                                        nodeIdentifier)
    529543              for handler in self.parent
    530544              if IDisco.providedBy(handler)]
     
    548562        @rtype: L{defer.Deferred}
    549563        """
    550         dl = [handler.getDiscoItems(requestor, target, nodeIdentifier)
     564        dl = [defer.maybeDeferred(handler.getDiscoItems, requestor, target,
     565                                                         nodeIdentifier)
    551566              for handler in self.parent
    552567              if IDisco.providedBy(handler)]
  • wokkel/generic.py

    r57 r68  
    11# -*- test-case-name: wokkel.test.test_generic -*-
    22#
    3 # Copyright (c) 2003-2008 Ralph Meijer
     3# Copyright (c) 2003-2009 Ralph Meijer
    44# See LICENSE for details.
    55
     
    1111
    1212from twisted.internet import defer, protocol
     13from twisted.python import reflect
    1314from twisted.words.protocols.jabber import error, jid, xmlstream
    1415from twisted.words.protocols.jabber.xmlstream import toResponse
     
    174175    """
    175176
    176     sender = None
    177     recipient = None
     177    stanzaKind = None
     178    stanzaID = None
    178179    stanzaType = None
     180
     181    def __init__(self, recipient=None, sender=None):
     182        self.recipient = recipient
     183        self.sender = sender
     184
    179185
    180186    @classmethod
     
    186192
    187193    def parseElement(self, element):
    188         self.sender = jid.internJID(element['from'])
    189194        if element.hasAttribute('from'):
    190195            self.sender = jid.internJID(element['from'])
     
    192197            self.recipient = jid.internJID(element['to'])
    193198        self.stanzaType = element.getAttribute('type')
     199        self.stanzaID = element.getAttribute('id')
     200
     201        # Save element
     202        stripNamespace(element)
     203        self.element = element
     204
     205        # accumulate all childHandlers in the class hierarchy of Class
     206        handlers = {}
     207        reflect.accumulateClassDict(self.__class__, 'childParsers', handlers)
     208
     209        for child in element.elements():
     210            try:
     211                handler = handlers[child.uri, child.name]
     212            except KeyError:
     213                pass
     214            else:
     215                getattr(self, handler)(child)
     216
     217
     218    def toElement(self):
     219        element = domish.Element((None, self.stanzaKind))
     220        if self.sender is not None:
     221            element['from'] = self.sender.full()
     222        if self.recipient is not None:
     223            element['to'] = self.recipient.full()
     224        if self.stanzaType:
     225            element['type'] = self.stanzaType
     226        if self.stanzaID:
     227            element['id'] = self.stanzaID
     228        return element
     229
     230
     231
     232class ErrorStanza(Stanza):
     233
     234    def parseElement(self, element):
     235        Stanza.parseElement(self, element)
     236        self.exception = error.exceptionFromStanza(element)
    194237
    195238
  • wokkel/pubsub.py

    r59 r63  
    11# -*- test-case-name: wokkel.test.test_pubsub -*-
    22#
    3 # Copyright (c) 2003-2008 Ralph Meijer
     3# Copyright (c) 2003-2009 Ralph Meijer
    44# See LICENSE for details.
    55
     
    1515from twisted.internet import defer
    1616from twisted.python import log
    17 from twisted.words.protocols.jabber import jid, error, xmlstream
     17from twisted.words.protocols.jabber import jid, error
    1818from twisted.words.xish import domish
    1919
    2020from wokkel import disco, data_form, generic, shim
     21from wokkel.compat import IQ
    2122from wokkel.subprotocols import IQHandlerMixin, XMPPHandler
    2223from wokkel.iwokkel import IPubSubClient, IPubSubService, IPubSubResource
     
    476477
    477478        This renders all of the relevant parameters for this specific
    478         requests into an L{xmlstream.IQ}, and invoke its C{send} method.
     479        requests into an L{IQ}, and invoke its C{send} method.
    479480        This returns a deferred that fires upon reception of a response. See
    480         L{xmlstream.IQ} for details.
     481        L{IQ} for details.
    481482
    482483        @param xs: The XML stream to send the request on.
     
    492493            raise NotImplementedError()
    493494
    494         iq = xmlstream.IQ(xs, self.stanzaType)
     495        iq = IQ(xs, self.stanzaType)
    495496        iq.addElement((childURI, 'pubsub'))
    496497        verbElement = iq.pubsub.addElement(childName)
  • wokkel/test/test_client.py

    r42 r73  
    1 # Copyright (c) 2003-2007 Ralph Meijer
     1# Copyright (c) 2003-2009 Ralph Meijer
    22# See LICENSE for details.
    33
     
    1414from twisted.words.protocols.jabber.xmlstream import INIT_FAILED_EVENT
    1515
     16try:
     17    from twisted.words.protocols.jabber.xmlstream import XMPPHandler
     18except ImportError:
     19    from wokkel.subprotocols import XMPPHandler
     20
    1621from wokkel import client
    1722from wokkel.test.test_compat import BootstrapMixinTest
     23
     24class XMPPClientTest(unittest.TestCase):
     25    """
     26    Tests for L{client.XMPPClient}.
     27    """
     28
     29    def setUp(self):
     30        self.client = client.XMPPClient(JID('user@example.org'), 'secret')
     31
     32
     33    def test_jid(self):
     34        """
     35        Make sure the JID we pass is stored on the client.
     36        """
     37        self.assertEquals(JID('user@example.org'), self.client.jid)
     38
     39
     40    def test_jidWhenInitialized(self):
     41        """
     42        Make sure that upon login, the JID is updated from the authenticator.
     43        """
     44        xs = self.client.factory.buildProtocol(None)
     45        self.client.factory.authenticator.jid = JID('user@example.org/test')
     46        xs.dispatch(xs, xmlstream.STREAM_AUTHD_EVENT)
     47        self.assertEquals(JID('user@example.org/test'), self.client.jid)
     48
     49
    1850
    1951class DeferredClientFactoryTest(BootstrapMixinTest):
     
    78110
    79111
     112    def test_addHandler(self):
     113        """
     114        Test the addition of a protocol handler.
     115        """
     116        handler = XMPPHandler()
     117        handler.setHandlerParent(self.factory.streamManager)
     118        self.assertIn(handler, self.factory.streamManager)
     119        self.assertIdentical(self.factory.streamManager, handler.parent)
     120
     121
     122    def test_removeHandler(self):
     123        """
     124        Test removal of a protocol handler.
     125        """
     126        handler = XMPPHandler()
     127        handler.setHandlerParent(self.factory.streamManager)
     128        handler.disownHandlerParent(self.factory.streamManager)
     129        self.assertNotIn(handler, self.factory.streamManager)
     130        self.assertIdentical(None, handler.parent)
     131
     132
    80133
    81134class ClientCreatorTest(unittest.TestCase):
  • wokkel/test/test_compat.py

    r53 r63  
    77"""
    88
     9from zope.interface import implements
    910from zope.interface.verify import verifyObject
    10 from twisted.internet import defer, protocol
    11 from twisted.internet.interfaces import IProtocolFactory
     11from twisted.internet import protocol, task
     12from twisted.internet.interfaces import IProtocolFactory, IReactorTime
    1213from twisted.trial import unittest
    13 from twisted.words.xish import domish, utility
     14from twisted.words.xish import utility
    1415from twisted.words.protocols.jabber import xmlstream
    15 from wokkel.compat import BootstrapMixin, XmlStreamServerFactory
     16from wokkel.compat import BootstrapMixin, IQ, XmlStreamServerFactory
    1617
    1718class DummyProtocol(protocol.Protocol, utility.EventDispatcher):
     
    166167        xs = self.factory.buildProtocol(None)
    167168        self.assertIdentical(self.factory, xs.factory)
     169
     170
     171
     172class FakeReactor(object):
     173
     174    implements(IReactorTime)
     175    def __init__(self):
     176        self.clock = task.Clock()
     177        self.callLater = self.clock.callLater
     178        self.getDelayedCalls = self.clock.getDelayedCalls
     179
     180
     181
     182class IQTest(unittest.TestCase):
     183    """
     184    Tests for L{IQ}.
     185    """
     186
     187    def setUp(self):
     188        self.reactor = FakeReactor()
     189        self.clock = self.reactor.clock
     190
     191
     192    def testRequestTimingOutEventDispatcher(self):
     193        """
     194        Test that an iq request with a defined timeout times out.
     195        """
     196        from twisted.words.xish import utility
     197        output = []
     198        xs = utility.EventDispatcher()
     199        xs.send = output.append
     200
     201        self.iq = IQ(xs, reactor=self.reactor)
     202        self.iq.timeout = 60
     203        d = self.iq.send()
     204        self.assertFailure(d, xmlstream.TimeoutError)
     205
     206        self.clock.pump([1, 60])
     207        self.assertFalse(self.reactor.getDelayedCalls())
     208        self.assertFalse(xs.iqDeferreds)
     209        return d
  • wokkel/test/test_disco.py

    r53 r67  
    515515
    516516
     517    def test_requestItemsFrom(self):
     518        """
     519        A disco items request can be sent with an explicit sender address.
     520        """
     521        d = self.protocol.requestItems(JID(u'example.org'),
     522                                       sender=JID(u'test.example.org'))
     523
     524        iq = self.stub.output[-1]
     525        self.assertEqual(u'test.example.org', iq.getAttribute(u'from'))
     526
     527        response = toResponse(iq, u'result')
     528        response.addElement((NS_DISCO_ITEMS, u'query'))
     529        self.stub.send(response)
     530
     531        return d
     532
     533
    517534    def test_requestInfo(self):
    518535        """
     
    550567
    551568        self.stub.send(response)
     569        return d
     570
     571
     572    def test_requestInfoFrom(self):
     573        """
     574        A disco info request can be sent with an explicit sender address.
     575        """
     576        d = self.protocol.requestInfo(JID(u'example.org'),
     577                                       sender=JID(u'test.example.org'))
     578
     579        iq = self.stub.output[-1]
     580        self.assertEqual(u'test.example.org', iq.getAttribute(u'from'))
     581
     582        response = toResponse(iq, u'result')
     583        response.addElement((NS_DISCO_INFO, u'query'))
     584        self.stub.send(response)
     585
    552586        return d
    553587
     
    717751
    718752
     753    def test_infoNotDeferred(self):
     754        """
     755        C{info} should gather disco info from sibling handlers.
     756        """
     757        discoItems = [disco.DiscoIdentity('dummy', 'generic',
     758                                          'Generic Dummy Entity'),
     759                      disco.DiscoFeature('jabber:iq:version')
     760        ]
     761
     762        class DiscoResponder(XMPPHandler):
     763            implements(disco.IDisco)
     764
     765            def getDiscoInfo(self, requestor, target, nodeIdentifier):
     766                if not nodeIdentifier:
     767                    return discoItems
     768                else:
     769                    return []
     770
     771        def cb(result):
     772            self.assertEquals(discoItems, result)
     773
     774        self.service.parent = [self.service, DiscoResponder()]
     775        d = self.service.info(JID('test@example.com'), JID('example.com'), '')
     776        d.addCallback(cb)
     777        return d
     778
     779
    719780    def test_items(self):
    720781        """
     
    739800        d.addCallback(cb)
    740801        return d
     802
     803
     804    def test_itemsNotDeferred(self):
     805        """
     806        C{info} should also collect results not returned via a deferred.
     807        """
     808        discoItems = [disco.DiscoItem(JID('example.com'), 'test', 'Test node')]
     809
     810        class DiscoResponder(XMPPHandler):
     811            implements(disco.IDisco)
     812
     813            def getDiscoItems(self, requestor, target, nodeIdentifier):
     814                if not nodeIdentifier:
     815                    return discoItems
     816                else:
     817                    return []
     818
     819        def cb(result):
     820            self.assertEquals(discoItems, result)
     821
     822        self.service.parent = [self.service, DiscoResponder()]
     823        d = self.service.items(JID('test@example.com'), JID('example.com'), '')
     824        d.addCallback(cb)
     825        return d
  • wokkel/test/test_xmppim.py

    r28 r68  
    1 # Copyright (c) 2003-2008 Ralph Meijer
     1# Copyright (c) 2003-2009 Ralph Meijer
    22# See LICENSE for details
    33
     
    66"""
    77
     8from twisted.internet import defer
    89from twisted.trial import unittest
    910from twisted.words.protocols.jabber.jid import JID
    1011from twisted.words.protocols.jabber.xmlstream import toResponse
    11 from twisted.words.xish import domish
     12from twisted.words.xish import domish, utility
    1213
    1314from wokkel import xmppim
     15from wokkel.generic import ErrorStanza, parseXml
    1416from wokkel.test.helpers import XmlStreamStub
    1517
     18NS_XML = 'http://www.w3.org/XML/1998/namespace'
    1619NS_ROSTER = 'jabber:iq:roster'
    1720
     
    7679
    7780
     81
     82class AvailabilityPresenceTest(unittest.TestCase):
     83
     84    def test_fromElement(self):
     85        xml = """<presence from='user@example.org' to='user@example.com'>
     86                   <show>chat</show>
     87                   <status>Let's chat!</status>
     88                   <priority>50</priority>
     89                 </presence>
     90              """
     91
     92        presence = xmppim.AvailabilityPresence.fromElement(parseXml(xml))
     93        self.assertEquals(JID('user@example.org'), presence.sender)
     94        self.assertEquals(JID('user@example.com'), presence.recipient)
     95        self.assertTrue(presence.available)
     96        self.assertEquals('chat', presence.show)
     97        self.assertEquals({None: "Let's chat!"}, presence.statuses)
     98        self.assertEquals(50, presence.priority)
     99
     100
     101class PresenceProtocolTest(unittest.TestCase):
     102    """
     103    Tests for L{xmppim.PresenceProtocol}
     104    """
     105
     106    def setUp(self):
     107        self.output = []
     108        self.protocol = xmppim.PresenceProtocol()
     109        self.protocol.parent = self
     110        self.protocol.xmlstream = utility.EventDispatcher()
     111        self.protocol.connectionInitialized()
     112
     113
     114    def send(self, obj):
     115        self.output.append(obj)
     116
     117
     118    def test_errorReceived(self):
     119        """
     120        Incoming presence stanzas are parsed and dispatched.
     121        """
     122        xml = """<presence type="error"/>"""
     123
     124        def errorReceived(error):
     125            xmppim.PresenceProtocol.errorReceived(self.protocol, error)
     126            try:
     127                self.assertIsInstance(error, ErrorStanza)
     128            except:
     129                d.errback()
     130            else:
     131                d.callback(None)
     132
     133        d = defer.Deferred()
     134        self.protocol.errorReceived = errorReceived
     135        self.protocol.xmlstream.dispatch(parseXml(xml))
     136        return d
     137
     138
     139    def test_availableReceived(self):
     140        """
     141        Incoming presence stanzas are parsed and dispatched.
     142        """
     143        xml = """<presence/>"""
     144
     145        def availableReceived(presence):
     146            xmppim.PresenceProtocol.availableReceived(self.protocol, presence)
     147            try:
     148                self.assertIsInstance(presence, xmppim.AvailabilityPresence)
     149            except:
     150                d.errback()
     151            else:
     152                d.callback(None)
     153
     154        d = defer.Deferred()
     155        self.protocol.availableReceived = availableReceived
     156        self.protocol.xmlstream.dispatch(parseXml(xml))
     157        return d
     158
     159
     160    def test_unavailableReceived(self):
     161        """
     162        Incoming presence stanzas are parsed and dispatched.
     163        """
     164        xml = """<presence type='unavailable'/>"""
     165
     166        def unavailableReceived(presence):
     167            xmppim.PresenceProtocol.unavailableReceived(self.protocol, presence)
     168            try:
     169                self.assertIsInstance(presence, xmppim.AvailabilityPresence)
     170            except:
     171                d.errback()
     172            else:
     173                d.callback(None)
     174
     175        d = defer.Deferred()
     176        self.protocol.unavailableReceived = unavailableReceived
     177        self.protocol.xmlstream.dispatch(parseXml(xml))
     178        return d
     179
     180
     181    def test_subscribeReceived(self):
     182        """
     183        Incoming presence stanzas are parsed and dispatched.
     184        """
     185        xml = """<presence type='subscribe'/>"""
     186
     187        def subscribeReceived(presence):
     188            xmppim.PresenceProtocol.subscribeReceived(self.protocol, presence)
     189            try:
     190                self.assertIsInstance(presence, xmppim.SubscriptionPresence)
     191            except:
     192                d.errback()
     193            else:
     194                d.callback(None)
     195
     196        d = defer.Deferred()
     197        self.protocol.subscribeReceived = subscribeReceived
     198        self.protocol.xmlstream.dispatch(parseXml(xml))
     199        return d
     200
     201
     202    def test_unsubscribeReceived(self):
     203        """
     204        Incoming presence stanzas are parsed and dispatched.
     205        """
     206        xml = """<presence type='unsubscribe'/>"""
     207
     208        def unsubscribeReceived(presence):
     209            xmppim.PresenceProtocol.unsubscribeReceived(self.protocol, presence)
     210            try:
     211                self.assertIsInstance(presence, xmppim.SubscriptionPresence)
     212            except:
     213                d.errback()
     214            else:
     215                d.callback(None)
     216
     217        d = defer.Deferred()
     218        self.protocol.unsubscribeReceived = unsubscribeReceived
     219        self.protocol.xmlstream.dispatch(parseXml(xml))
     220        return d
     221
     222
     223    def test_subscribedReceived(self):
     224        """
     225        Incoming presence stanzas are parsed and dispatched.
     226        """
     227        xml = """<presence type='subscribed'/>"""
     228
     229        def subscribedReceived(presence):
     230            xmppim.PresenceProtocol.subscribedReceived(self.protocol, presence)
     231            try:
     232                self.assertIsInstance(presence, xmppim.SubscriptionPresence)
     233            except:
     234                d.errback()
     235            else:
     236                d.callback(None)
     237
     238        d = defer.Deferred()
     239        self.protocol.subscribedReceived = subscribedReceived
     240        self.protocol.xmlstream.dispatch(parseXml(xml))
     241        return d
     242
     243
     244    def test_unsubscribedReceived(self):
     245        """
     246        Incoming presence stanzas are parsed and dispatched.
     247        """
     248        xml = """<presence type='unsubscribed'/>"""
     249
     250        def unsubscribedReceived(presence):
     251            xmppim.PresenceProtocol.unsubscribedReceived(self.protocol,
     252                                                         presence)
     253            try:
     254                self.assertIsInstance(presence, xmppim.SubscriptionPresence)
     255            except:
     256                d.errback()
     257            else:
     258                d.callback(None)
     259
     260        d = defer.Deferred()
     261        self.protocol.unsubscribedReceived = unsubscribedReceived
     262        self.protocol.xmlstream.dispatch(parseXml(xml))
     263        return d
     264
     265
     266    def test_probeReceived(self):
     267        """
     268        Incoming presence stanzas are parsed and dispatched.
     269        """
     270        xml = """<presence type='probe'/>"""
     271
     272        def probeReceived(presence):
     273            xmppim.PresenceProtocol.probeReceived(self.protocol, presence)
     274            try:
     275                self.assertIsInstance(presence, xmppim.ProbePresence)
     276            except:
     277                d.errback()
     278            else:
     279                d.callback(None)
     280
     281        d = defer.Deferred()
     282        self.protocol.probeReceived = probeReceived
     283        self.protocol.xmlstream.dispatch(parseXml(xml))
     284        return d
     285
     286    def test_available(self):
     287        """
     288        It should be possible to pass a sender address.
     289        """
     290        self.protocol.available(JID('user@example.com'),
     291                                show=u'chat',
     292                                status=u'Talk to me!',
     293                                priority=50)
     294        element = self.output[-1]
     295        self.assertEquals("user@example.com", element.getAttribute('to'))
     296        self.assertIdentical(None, element.getAttribute('type'))
     297        self.assertEquals(u'chat', unicode(element.show))
     298        self.assertEquals(u'Talk to me!', unicode(element.status))
     299        self.assertEquals(u'50', unicode(element.priority))
     300
     301    def test_availableLanguages(self):
     302        """
     303        It should be possible to pass a sender address.
     304        """
     305        self.protocol.available(JID('user@example.com'),
     306                                show=u'chat',
     307                                statuses={None: u'Talk to me!',
     308                                          'nl': u'Praat met me!'},
     309                                priority=50)
     310        element = self.output[-1]
     311        self.assertEquals("user@example.com", element.getAttribute('to'))
     312        self.assertIdentical(None, element.getAttribute('type'))
     313        self.assertEquals(u'chat', unicode(element.show))
     314
     315        statuses = {}
     316        for status in element.elements():
     317            if status.name == 'status':
     318                lang = status.getAttribute((NS_XML, 'lang'))
     319                statuses[lang] = unicode(status)
     320
     321        self.assertIn(None, statuses)
     322        self.assertEquals(u'Talk to me!', statuses[None])
     323        self.assertIn('nl', statuses)
     324        self.assertEquals(u'Praat met me!', statuses['nl'])
     325        self.assertEquals(u'50', unicode(element.priority))
     326
     327
     328    def test_availableSender(self):
     329        """
     330        It should be possible to pass a sender address.
     331        """
     332        self.protocol.available(JID('user@example.com'),
     333                                sender=JID('user@example.org'))
     334        element = self.output[-1]
     335        self.assertEquals("user@example.org", element.getAttribute('from'))
     336
     337
     338    def test_unavailableDirected(self):
     339        """
     340        Test sending of directed unavailable presence broadcast.
     341        """
     342
     343        self.protocol.unavailable(JID('user@example.com'))
     344        element = self.output[-1]
     345        self.assertEquals("presence", element.name)
     346        self.assertEquals(None, element.uri)
     347        self.assertEquals("user@example.com", element.getAttribute('to'))
     348        self.assertEquals("unavailable", element.getAttribute('type'))
     349
     350    def test_unavailableWithStatus(self):
     351        """
     352        Test sending of directed unavailable presence broadcast with status.
     353        """
     354
     355        self.protocol.unavailable(JID('user@example.com'),
     356                                  {None: 'Disconnected'})
     357        element = self.output[-1]
     358        self.assertEquals("presence", element.name)
     359        self.assertEquals(None, element.uri)
     360        self.assertEquals("user@example.com", element.getAttribute('to'))
     361        self.assertEquals("unavailable", element.getAttribute('type'))
     362        self.assertEquals("Disconnected", unicode(element.status))
     363
     364
     365    def test_unavailableBroadcast(self):
     366        """
     367        Test sending of unavailable presence broadcast.
     368        """
     369
     370        self.protocol.unavailable(None)
     371        element = self.output[-1]
     372        self.assertEquals("presence", element.name)
     373        self.assertEquals(None, element.uri)
     374        self.assertEquals(None, element.getAttribute('to'))
     375        self.assertEquals("unavailable", element.getAttribute('type'))
     376
     377
     378    def test_unavailableBroadcastNoRecipientParameter(self):
     379        """
     380        Test sending of unavailable presence broadcast by not passing entity.
     381        """
     382
     383        self.protocol.unavailable()
     384        element = self.output[-1]
     385        self.assertEquals("presence", element.name)
     386        self.assertEquals(None, element.uri)
     387        self.assertEquals(None, element.getAttribute('to'))
     388        self.assertEquals("unavailable", element.getAttribute('type'))
     389
     390
     391    def test_unavailableSender(self):
     392        """
     393        It should be possible to pass a sender address.
     394        """
     395        self.protocol.unavailable(JID('user@example.com'),
     396                                  sender=JID('user@example.org'))
     397        element = self.output[-1]
     398        self.assertEquals("user@example.org", element.getAttribute('from'))
     399
     400
     401    def test_subscribeSender(self):
     402        """
     403        It should be possible to pass a sender address.
     404        """
     405        self.protocol.subscribe(JID('user@example.com'),
     406                                sender=JID('user@example.org'))
     407        element = self.output[-1]
     408        self.assertEquals("user@example.org", element.getAttribute('from'))
     409
     410
     411    def test_unsubscribeSender(self):
     412        """
     413        It should be possible to pass a sender address.
     414        """
     415        self.protocol.unsubscribe(JID('user@example.com'),
     416                                  sender=JID('user@example.org'))
     417        element = self.output[-1]
     418        self.assertEquals("user@example.org", element.getAttribute('from'))
     419
     420
     421    def test_subscribedSender(self):
     422        """
     423        It should be possible to pass a sender address.
     424        """
     425        self.protocol.subscribed(JID('user@example.com'),
     426                                 sender=JID('user@example.org'))
     427        element = self.output[-1]
     428        self.assertEquals("user@example.org", element.getAttribute('from'))
     429
     430
     431    def test_unsubscribedSender(self):
     432        """
     433        It should be possible to pass a sender address.
     434        """
     435        self.protocol.unsubscribed(JID('user@example.com'),
     436                                   sender=JID('user@example.org'))
     437        element = self.output[-1]
     438        self.assertEquals("user@example.org", element.getAttribute('from'))
     439
     440
     441    def test_probeSender(self):
     442        """
     443        It should be possible to pass a sender address.
     444        """
     445        self.protocol.probe(JID('user@example.com'),
     446                            sender=JID('user@example.org'))
     447        element = self.output[-1]
     448        self.assertEquals("user@example.org", element.getAttribute('from'))
     449
     450
     451
    78452class RosterClientProtocolTest(unittest.TestCase):
    79453    """
  • wokkel/xmppim.py

    r28 r68  
    11# -*- test-case-name: wokkel.test.test_xmppim -*-
    22#
    3 # Copyright (c) 2003-2008 Ralph Meijer
     3# Copyright (c) 2003-2009 Ralph Meijer
    44# See LICENSE for details.
    55
     
    1414
    1515from twisted.words.protocols.jabber.jid import JID
    16 from twisted.words.protocols.jabber.xmlstream import IQ
    1716from twisted.words.xish import domish
    1817
     18from wokkel.compat import IQ
     19from wokkel.generic import ErrorStanza, Stanza
    1920from wokkel.subprotocols import XMPPHandler
    2021
     
    114115    def _onPresenceUnsubscribe(self, presence):
    115116        self.unsubscribeReceived(JID(presence["from"]))
     117
    116118
    117119    def availableReceived(self, entity, show=None, statuses=None, priority=0):
     
    246248        """
    247249        self.send(Presence(to=entity, type='unsubscribed'))
     250
     251
     252
     253class BasePresence(Stanza):
     254    """
     255    Stanza of kind presence.
     256    """
     257    stanzaKind = 'presence'
     258
     259
     260
     261class AvailabilityPresence(BasePresence):
     262    """
     263    Presence.
     264
     265    This represents availability presence (as opposed to
     266    L{SubscriptionPresence}).
     267
     268    @ivar available: The availability being communicated.
     269    @type available: C{bool}
     270    @ivar show: More specific availability. Can be one of C{'chat'}, C{'away'},
     271                C{'xa'}, C{'dnd'} or C{None}.
     272    @type show: C{str} or C{NoneType}
     273    @ivar statuses: Natural language texts to detail the (un)availability.
     274                    These are represented as a mapping from language code
     275                    (C{str} or C{None}) to the corresponding text (C{unicode}).
     276                    If the key is C{None}, the associated text is in the
     277                    default language.
     278    @type statuses: C{dict}
     279    @ivar priority: Priority level for this resource. Must be between -128 and
     280                    127. Defaults to 0.
     281    @type priority: C{int}
     282    """
     283
     284    childParsers = {(None, 'show'): '_childParser_show',
     285                     (None, 'status'): '_childParser_status',
     286                     (None, 'priority'): '_childParser_priority'}
     287
     288    def __init__(self, recipient=None, sender=None, available=True,
     289                       show=None, status=None, statuses=None, priority=0):
     290        BasePresence.__init__(self, recipient=recipient, sender=sender)
     291        self.available = available
     292        self.show = show
     293        self.statuses = statuses or {}
     294        if status:
     295            self.statuses[None] = status
     296        self.priority = priority
     297
     298
     299    def _childParser_show(self, element):
     300        show = unicode(element)
     301        if show in ('chat', 'away', 'xa', 'dnd'):
     302            self.show = show
     303
     304
     305    def _childParser_status(self, element):
     306        lang = element.getAttribute((NS_XML, 'lang'), None)
     307        text = unicode(element)
     308        self.statuses[lang] = text
     309
     310
     311    def _childParser_priority(self, element):
     312        try:
     313            self.priority = int(unicode(element))
     314        except ValueError:
     315            pass
     316
     317
     318    def parseElement(self, element):
     319        BasePresence.parseElement(self, element)
     320
     321        if self.stanzaType == 'unavailable':
     322            self.available = False
     323
     324
     325    def toElement(self):
     326        if not self.available:
     327            self.stanzaType = 'unavailable'
     328
     329        presence = BasePresence.toElement(self)
     330
     331        if self.available:
     332            if self.show in ('chat', 'away', 'xa', 'dnd'):
     333                presence.addElement('show', content=self.show)
     334            if self.priority != 0:
     335                presence.addElement('priority', content=unicode(self.priority))
     336
     337        for lang, text in self.statuses.iteritems():
     338            status = presence.addElement('status', content=text)
     339            if lang:
     340                status[(NS_XML, 'lang')] = lang
     341
     342        return presence
     343
     344
     345
     346class SubscriptionPresence(BasePresence):
     347    """
     348    Presence subscription request or response.
     349
     350    This kind of presence is used to represent requests for presence
     351    subscription and their replies.
     352
     353    Based on L{BasePresence} and {Stanza}, it just uses the L{stanzaType}
     354    attribute to represent the type of subscription presence. This can be
     355    one of C{'subscribe'}, C{'unsubscribe'}, C{'subscribed'} and
     356    C{'unsubscribed'}.
     357    """
     358
     359
     360
     361class ProbePresence(BasePresence):
     362    """
     363    Presence probe request.
     364    """
     365
     366    stanzaType = 'probe'
     367
     368
     369
     370class PresenceProtocol(XMPPHandler):
     371    """
     372    XMPP Presence protocol.
     373
     374    @cvar presenceTypeParserMap: Maps presence stanza types to their respective
     375        stanza parser classes (derived from L{Stanza}).
     376    @type presenceTypeParserMap: C{dict}
     377    """
     378
     379    presenceTypeParserMap = {
     380                'error': ErrorStanza,
     381                'available': AvailabilityPresence,
     382                'unavailable': AvailabilityPresence,
     383                'subscribe': SubscriptionPresence,
     384                'unsubscribe': SubscriptionPresence,
     385                'subscribed': SubscriptionPresence,
     386                'unsubscribed': SubscriptionPresence,
     387                'probe': ProbePresence,
     388        }
     389
     390    def connectionInitialized(self):
     391        self.xmlstream.addObserver("/presence", self._onPresence)
     392
     393
     394    def _onPresence(self, element):
     395        stanza = Stanza.fromElement(element)
     396
     397        presenceType = stanza.stanzaType or 'available'
     398
     399        try:
     400            parser = self.presenceTypeParserMap[presenceType]
     401        except KeyError:
     402            return
     403
     404        presence = parser.fromElement(element)
     405
     406        try:
     407            handler = getattr(self, '%sReceived' % presenceType)
     408        except AttributeError:
     409            return
     410        else:
     411            handler(presence)
     412
     413
     414    def errorReceived(self, presence):
     415        """
     416        Error presence was received.
     417        """
     418        pass
     419
     420
     421    def availableReceived(self, presence):
     422        """
     423        Available presence was received.
     424        """
     425        pass
     426
     427
     428    def unavailableReceived(self, presence):
     429        """
     430        Unavailable presence was received.
     431        """
     432        pass
     433
     434
     435    def subscribedReceived(self, presence):
     436        """
     437        Subscription approval confirmation was received.
     438        """
     439        pass
     440
     441
     442    def unsubscribedReceived(self, presence):
     443        """
     444        Unsubscription confirmation was received.
     445        """
     446        pass
     447
     448
     449    def subscribeReceived(self, presence):
     450        """
     451        Subscription request was received.
     452        """
     453        pass
     454
     455
     456    def unsubscribeReceived(self, presence):
     457        """
     458        Unsubscription request was received.
     459        """
     460        pass
     461
     462
     463    def probeReceived(self, presence):
     464        """
     465        Probe presence was received.
     466        """
     467        pass
     468
     469
     470    def available(self, recipient=None, show=None, statuses=None, priority=0,
     471                        status=None, sender=None):
     472        """
     473        Send available presence.
     474
     475        @param recipient: Optional Recipient to which the presence should be
     476            sent.
     477        @type recipient: {JID}
     478
     479        @param show: Optional detailed presence information. One of C{'away'},
     480            C{'xa'}, C{'chat'}, C{'dnd'}.
     481        @type show: C{str}
     482
     483        @param statuses: Mapping of natural language descriptions of the
     484           availability status, keyed by the language descriptor. A status
     485           without a language specified, is keyed with C{None}.
     486        @type statuses: C{dict}
     487
     488        @param priority: priority level of the resource.
     489        @type priority: C{int}
     490        """
     491        presence = AvailabilityPresence(recipient=recipient, sender=sender,
     492                                        show=show, statuses=statuses,
     493                                        status=status, priority=priority)
     494        self.send(presence.toElement())
     495
     496
     497    def unavailable(self, recipient=None, statuses=None, sender=None):
     498        """
     499        Send unavailable presence.
     500
     501        @param recipient: Optional entity to which the presence should be sent.
     502        @type recipient: {JID}
     503
     504        @param statuses: dictionary of natural language descriptions of the
     505            availability status, keyed by the language descriptor. A status
     506            without a language specified, is keyed with C{None}.
     507        @type statuses: C{dict}
     508        """
     509        presence = AvailabilityPresence(recipient=recipient, sender=sender,
     510                                        available=False, statuses=statuses)
     511        self.send(presence.toElement())
     512
     513
     514    def subscribe(self, recipient, sender=None):
     515        """
     516        Send subscription request
     517
     518        @param recipient: Entity to subscribe to.
     519        @type recipient: {JID}
     520        """
     521        presence = SubscriptionPresence(recipient=recipient, sender=sender)
     522        presence.stanzaType = 'subscribe'
     523        self.send(presence.toElement())
     524
     525
     526    def unsubscribe(self, recipient, sender=None):
     527        """
     528        Send unsubscription request
     529
     530        @param recipient: Entity to unsubscribe from.
     531        @type recipient: {JID}
     532        """
     533        presence = SubscriptionPresence(recipient=recipient, sender=sender)
     534        presence.stanzaType = 'unsubscribe'
     535        self.send(presence.toElement())
     536
     537
     538    def subscribed(self, recipient, sender=None):
     539        """
     540        Send subscription confirmation.
     541
     542        @param recipient: Entity that subscribed.
     543        @type recipient: {JID}
     544        """
     545        presence = SubscriptionPresence(recipient=recipient, sender=sender)
     546        presence.stanzaType = 'subscribed'
     547        self.send(presence.toElement())
     548
     549
     550    def unsubscribed(self, recipient, sender=None):
     551        """
     552        Send unsubscription confirmation.
     553
     554        @param recipient: Entity that unsubscribed.
     555        @type recipient: {JID}
     556        """
     557        presence = SubscriptionPresence(recipient=recipient, sender=sender)
     558        presence.stanzaType = 'unsubscribed'
     559        self.send(presence.toElement())
     560
     561
     562    def probe(self, recipient, sender=None):
     563        """
     564        Send presence probe.
     565
     566        @param recipient: Entity to be probed.
     567        @type recipient: {JID}
     568        """
     569        presence = ProbePresence(recipient=recipient, sender=sender)
     570        self.send(presence.toElement())
     571
    248572
    249573
Note: See TracChangeset for help on using the changeset viewer.