Changeset 72:727b4d29c48e in ralphm-patches


Ignore:
Timestamp:
Jan 27, 2013, 10:40:32 PM (7 years ago)
Author:
Ralph Meijer <ralphm@…>
Branch:
default
Message:

Major reworking of avatars, session manager and stanza handlers.

Files:
5 added
8 edited

Legend:

Unmodified
Added
Removed
  • c2s_server_factory.patch

    r71 r72  
    11# HG changeset patch
    2 # Parent 8cb185c88211ac69faa924f6e93425d102daa2bc
     2# Parent 49294b2cf829414b42141731b5130d91474c0443
    33Add factory for accepting client connections.
    44
    5 The new XMPPC2SServerFactory is a server factory for accepting client
     5The new `XMPPC2SServerFactory` is a server factory for accepting client
    66connections. It uses `XMPPClientListenAuthenticator` to perform the
    7 steps for authentication and binding of a resource, and keeps a list
    8 of all streams.
    9 
    10 Upon loss of the connection, the service is called with `unbindResource`.
    11 Received stanzas cause the service's `onElement` to be called.
    12 
    13 The factory has a `deliverStanza` method to deliver stanzas to a particular
    14 recipient. This is used for stanzas that have different recipient addressing
    15 than the actual recipient (for presence and messages from a different (or no)
    16 resource).
     7steps for authentication and binding of a resource.
     8
     9For each connection, the factory also sets up subprotocol handlers by
     10calling `setupHandlers`. By default these are `RecipientAddressStamper`
     11and `StanzaForwarder`.
     12
     13The former makes sure that all XML stanzas received from the client
     14are stamped with a proper recipient address. The latter
     15passes stanzas on to the stream's avatar.
    1716
    1817TODO:
    1918
    20  * Add docstrings.
    2119 * Add tests.
    2220
     
    2422--- a/wokkel/client.py
    2523+++ b/wokkel/client.py
    26 @@ -21,7 +21,9 @@
    27  from twisted.words.xish import domish
     24@@ -22,7 +22,9 @@
    2825 
    2926 from wokkel import generic
    30 +from wokkel.compat import XmlStreamServerFactory
    3127 from wokkel.iwokkel import IUserSession
    3228+from wokkel.subprotocols import ServerStreamManager
    3329 from wokkel.subprotocols import StreamManager
     30+from wokkel.subprotocols import XMPPHandler
    3431 
    3532 NS_CLIENT = 'jabber:client'
    36 @@ -442,3 +444,41 @@
     33 
     34@@ -480,3 +482,70 @@
    3735             self.portal = self.portals[self.xmlstream.thisEntity]
    3836         except KeyError:
     
    4139+
    4240+
    43 +class XMPPC2SServerFactory(XmlStreamServerFactory):
     41+class RecipientAddressStamper(XMPPHandler):
     42+    """
     43+    Protocol handler to ensure client stanzas have a sender address.
     44+    """
     45+
     46+    def connectionInitialized(self):
     47+        self.xmlstream.addObserver('/*', self.onStanza, priority=1)
     48+
     49+
     50+    def onStanza(self, element):
     51+        """
     52+        Make sure each stanza has a sender address.
     53+        """
     54+        if element.uri:
     55+            return
     56+
     57+        if (element.name == 'presence' and
     58+            element.getAttribute('type') in ('subscribe', 'subscribed',
     59+                                             'unsubscribe', 'unsubscribed')):
     60+            element['from'] = self.xmlstream.avatar.entity.userhost()
     61+        elif element.name in ('message', 'presence', 'iq'):
     62+            element['from'] = self.xmlstream.avatar.entity.full()
     63+
     64+
     65+
     66+class XMPPC2SServerFactory(xmlstream.XmlStreamServerFactory):
     67+    """
     68+    Server factory for XMPP client-server connections.
     69+    """
    4470+
    4571+    def __init__(self, portals):
     
    4773+            return XMPPClientListenAuthenticator(portals)
    4874+
    49 +        XmlStreamServerFactory.__init__(self, authenticatorFactory)
     75+        xmlstream.XmlStreamServerFactory.__init__(self, authenticatorFactory)
    5076+        self.addBootstrap(xmlstream.STREAM_CONNECTED_EVENT,
    5177+                          self.onConnectionMade)
     
    74100+        """
    75101+        return [
    76 +            generic.StanzaForwarder()
    77 +        ]
     102+            generic.StanzaForwarder(),
     103+            RecipientAddressStamper(),
     104+            ]
    78105diff --git a/wokkel/generic.py b/wokkel/generic.py
    79106--- a/wokkel/generic.py
    80107+++ b/wokkel/generic.py
    81 @@ -615,3 +615,55 @@
    82  
    83          self._initializers = self.getInitializers()
    84          self._initializeStream()
     108@@ -628,3 +628,65 @@
     109     standard full stop.
     110     """
     111     return name.encode('idna')
    85112+
    86113+
     
    108135+        Called when the stream has been initialized.
    109136+        """
    110 +        self.xmlstream.addObserver(
    111 +            '/*[@xmlns="%s"]' % self.xmlstream.namespace,
    112 +            self.onStanza, priority=-1)
     137+        self.xmlstream.addObserver('/*[@xmlns="%s"]' %
     138+                                       self.xmlstream.namespace,
     139+                                   stripNamespace, priority=1)
     140+        self.xmlstream.addObserver('/*', self.onStanza, priority=-1)
     141+
    113142+
    114143+
     
    117146+        Called when a stanza element was received.
    118147+
    119 +        Unless a stanza has already been handled, or the name of the element is
    120 +        not one of C{'iq'}, C{'message'}, C{'presence'}, the stanza is passed
    121 +        on to the avatar's C{send} method.
     148+        If this is an XML stanza, and it has not been handled by another
     149+        subprotocol handler, the stanza is passed on to the avatar's C{send}
     150+        method.
     151+
     152+        If there is no recipient address on the stanza, a service-unavailable
     153+        is returned instead.
    122154+        """
    123155+        if element.handled:
    124156+            return
    125157+
    126 +        if element.name not in ('iq', 'message', 'presence'):
     158+        if (element.name not in ('iq', 'message', 'presence') or
     159+            element.uri is not None):
    127160+            return
    128161+
    129 +        self.xmlstream.avatar.send(element)
    130 +
    131 +
    132 +    def onError(reason):
     162+        if not element.getAttribute('to'):
     163+            exc = error.StanzaError('service-unavailable')
     164+            self.send(exc.toResponse(element))
     165+        else:
     166+            self.xmlstream.avatar.send(element)
     167+
     168+
     169+    def onError(self, reason):
    133170+        """
    134171+        Log a stream error.
    135172+        """
    136173+        log.err(reason, "Stream error")
    137 diff --git a/wokkel/subprotocols.py b/wokkel/subprotocols.py
    138 --- a/wokkel/subprotocols.py
    139 +++ b/wokkel/subprotocols.py
    140 @@ -142,10 +142,13 @@
    141      @type timeout: C{int}
    142  
    143      @ivar _reactor: A provider of L{IReactorTime} to track timeouts.
    144 +
    145 +    @cvar __streamCount: Global stream count for distinguishing streams.
     174diff --git a/wokkel/test/test_client.py b/wokkel/test/test_client.py
     175--- a/wokkel/test/test_client.py
     176+++ b/wokkel/test/test_client.py
     177@@ -7,7 +7,7 @@
     178 
     179 from base64 import b64encode
     180 
     181-from zope.interface import implements
     182+from zope.interface import implementer
     183 
     184 from twisted.cred.portal import IRealm, Portal
     185 from twisted.cred.checkers import InMemoryUsernamePasswordDatabaseDontUse
     186@@ -28,6 +28,8 @@
     187 
     188 from wokkel import client, iwokkel
     189 from wokkel.generic import TestableXmlStream, FeatureListenAuthenticator
     190+from wokkel.generic import parseXml
     191+from wokkel.test.helpers import XmlStreamStub
     192 
     193 class XMPPClientTest(unittest.TestCase):
    146194     """
    147  
    148      timeout = None
    149      _reactor = None
    150 +    __streamCount = 0
    151  
    152      logTraffic = False
    153  
    154 @@ -195,20 +198,24 @@
    155          and call each handler's C{makeConnection} method with the L{XmlStream}
    156          instance.
    157          """
    158 -        def logDataIn(buf):
    159 -            log.msg("RECV: %r" % buf)
    160 +        xs.serial = self.__streamCount
    161 +        BaseStreamManager.__streamCount += 1
    162  
    163 -        def logDataOut(buf):
    164 -            log.msg("SEND: %r" % buf)
    165  
    166          if self.logTraffic:
    167 -            xs.rawDataInFn = logDataIn
    168 -            xs.rawDataOutFn = logDataOut
    169 +            def logData(direction, data):
    170 +                log.msg(format="%(direction)s (%(streamID)s): %(data)r",
    171 +                        direction=direction, streamID=xs.serial, data=data)
    172 +
    173 +            log.msg(format="Connection %(streamID)s made", streamID=xs.serial)
    174 +            xs.rawDataInFn = lambda data: logData("RECV", data)
    175 +            xs.rawDataOutFn = lambda data: logData("SEND", data)
    176  
    177          xs.addObserver(xmlstream.STREAM_AUTHD_EVENT,
    178                         self.connectionInitialized)
    179          xs.addObserver(xmlstream.STREAM_END_EVENT,
    180                         self.connectionLost)
    181 +
    182          self.xmlstream = xs
    183  
    184          for e in list(self):
    185 @@ -222,6 +229,9 @@
    186          Send out cached stanzas and call each handler's
    187          C{connectionInitialized} method.
    188          """
    189 +        if self.logTraffic:
    190 +            log.msg(format="Connection %(streamID)s initialized",
    191 +                    streamID=xs.serial)
    192  
    193          xs.addObserver('/iq[@type="result"]', self._onIQResponse)
    194          xs.addObserver('/iq[@type="error"]', self._onIQResponse)
    195 @@ -247,6 +257,10 @@
    196          L{XmlStream} anymore and notifies each handler that the connection
    197          was lost by calling its C{connectionLost} method.
    198          """
    199 +        if self.logTraffic:
    200 +            log.msg(format="Connection %(streamID)s lost",
    201 +                    streamID=self.xmlstream.serial)
    202 +
    203          self.xmlstream = None
    204          self._initialized = False
    205  
     195@@ -180,8 +182,8 @@
     196 
     197 
     198 
     199+@implementer(iwokkel.IUserSession)
     200 class TestSession(object):
     201-    implements(iwokkel.IUserSession)
     202 
     203     def __init__(self, domain, user):
     204         self.domain = domain
     205@@ -189,14 +191,14 @@
     206 
     207 
     208     def bindResource(self, resource):
     209-        return defer.succeed(JID(tuple=(self.user, self.domain, resource)))
     210+        self.entity = JID(tuple=(self.user, self.domain, resource))
     211+        return defer.succeed(self.entity)
     212 
     213 
     214 
     215+@implementer(IRealm)
     216 class TestRealm(object):
     217 
     218-    implements(IRealm)
     219-
     220     logoutCalled = False
     221 
     222     def __init__(self, domain):
     223@@ -679,3 +681,91 @@
     224                          "to='example.com' "
     225                          "version='1.0'>")
     226         self.xmlstream.assertStreamError(self, condition='host-unknown')
     227+
     228+
     229+
     230+class RecipientAddressStamperTest(unittest.TestCase):
     231+    """
     232+    Tests for L{client.RecipientAddressStamper}.
     233+    """
     234+
     235+
     236+    def setUp(self):
     237+        self.stub = XmlStreamStub()
     238+        self.stub.xmlstream.namespace = ''
     239+        avatar = TestSession(u'example.org', u'test')
     240+        avatar.bindResource(u'Home')
     241+        self.stub.xmlstream.avatar = avatar
     242+
     243+        self.protocol = client.RecipientAddressStamper()
     244+        self.protocol.makeConnection(self.stub.xmlstream)
     245+        self.protocol.connectionInitialized()
     246+
     247+
     248+    def test_presence(self):
     249+        """
     250+        The from address is set to the full JID on presence stanzas.
     251+        """
     252+        xml = """<presence/>"""
     253+        element = parseXml(xml)
     254+        self.stub.xmlstream.dispatch(element)
     255+        self.assertEqual(u'test@example.org/Home',
     256+                         element.getAttribute('from'))
     257+
     258+
     259+    def test_presenceSubscribe(self):
     260+        """
     261+        The from address is set to the bare JID on presence subscribe.
     262+        """
     263+        xml = """<presence type='subscribe'/>"""
     264+        element = parseXml(xml)
     265+        self.stub.xmlstream.dispatch(element)
     266+        self.assertEqual(u'test@example.org',
     267+                         element.getAttribute('from'))
     268+
     269+
     270+    def test_fromAlreadySet(self):
     271+        """
     272+        The from address is overridden if already present.
     273+        """
     274+        xml = """<presence from='test@example.org/Work'/>"""
     275+        element = parseXml(xml)
     276+        self.stub.xmlstream.dispatch(element)
     277+        self.assertEqual(u'test@example.org/Home',
     278+                         element.getAttribute('from'))
     279+
     280+
     281+    def test_notHandled(self):
     282+        """
     283+        The stanza will not have its 'handled' attribute set to True.
     284+        """
     285+        xml = """<presence/>"""
     286+        element = parseXml(xml)
     287+        self.stub.xmlstream.dispatch(element)
     288+        self.assertFalse(element.handled)
     289+
     290+
     291+    def test_message(self):
     292+        """
     293+        The from address is set to the full JID on message stanzas.
     294+        """
     295+        xml = """<message to='other@example.org'>
     296+                   <body>Hi!</body>
     297+                 </message>"""
     298+        element = parseXml(xml)
     299+        self.stub.xmlstream.dispatch(element)
     300+        self.assertEqual(u'test@example.org/Home',
     301+                         element.getAttribute('from'))
     302+
     303+
     304+    def test_iq(self):
     305+        """
     306+        The from address is set to the full JID on iq stanzas.
     307+        """
     308+        xml = """<iq type='get' id='g_1'>
     309+                   <query xmlns='jabber:iq:version'/>
     310+                 </iq>"""
     311+        element = parseXml(xml)
     312+        self.stub.xmlstream.dispatch(element)
     313+        self.assertEqual(u'test@example.org/Home',
     314+                         element.getAttribute('from'))
     315diff --git a/wokkel/test/test_generic.py b/wokkel/test/test_generic.py
     316--- a/wokkel/test/test_generic.py
     317+++ b/wokkel/test/test_generic.py
     318@@ -681,3 +681,105 @@
     319         name = u"example.com."
     320         result = generic.prepareIDNName(name)
     321         self.assertEqual(b"example.com.", result)
     322+
     323+
     324+
     325+class StanzaForwarderTest(unittest.TestCase):
     326+    """
     327+    Tests for L{generic.StanzaForwarder}.
     328+    """
     329+
     330+    def setUp(self):
     331+        class Avatar(object):
     332+            def __init__(self):
     333+                self.sent = []
     334+
     335+            def send(self, element):
     336+                self.sent.append(element)
     337+
     338+        self.stub = XmlStreamStub()
     339+        self.avatar = Avatar()
     340+        self.protocol = generic.StanzaForwarder()
     341+        self.protocol.makeConnection(self.stub.xmlstream)
     342+        self.protocol.xmlstream.avatar = self.avatar
     343+        self.protocol.xmlstream.namespace = u'jabber:client'
     344+        self.protocol.send = self.protocol.xmlstream.send
     345+
     346+
     347+    def test_onStanza(self):
     348+        """
     349+        An XML stanza is delivered at the stream avatar.
     350+        """
     351+        self.protocol.connectionInitialized()
     352+
     353+        element = domish.Element((None, u'message'))
     354+        element[u'to'] = u'other@example.org'
     355+        self.stub.send(element)
     356+
     357+        self.assertEqual(1, len(self.avatar.sent))
     358+        self.assertEqual(0, len(self.stub.output))
     359+
     360+
     361+    def test_onStanzaNoRecipient(self):
     362+        """
     363+        Stanzas without recipient are rejected.
     364+        """
     365+        self.protocol.connectionInitialized()
     366+
     367+        element = domish.Element((None, u'message'))
     368+        self.stub.send(element)
     369+
     370+        self.assertEqual(0, len(self.avatar.sent))
     371+        self.assertEqual(1, len(self.stub.output))
     372+
     373+
     374+    def test_onStanzaClientNamespace(self):
     375+        """
     376+        Stanzas with an explicit namespace are delivered.
     377+        """
     378+        self.protocol.connectionInitialized()
     379+
     380+        element = domish.Element(('jabber:client', u'message'))
     381+        element[u'to'] = u'other@example.org'
     382+        self.stub.send(element)
     383+
     384+        self.assertEqual(1, len(self.avatar.sent))
     385+        self.assertEqual(0, len(self.stub.output))
     386+
     387+
     388+    def test_onStanzaWrongNamespace(self):
     389+        """
     390+        If there is no xmlns on the stanza, it should still be delivered.
     391+        """
     392+        self.protocol.connectionInitialized()
     393+
     394+        element = domish.Element((u'testns', u'message'))
     395+        element[u'to'] = u'other@example.org'
     396+        self.stub.send(element)
     397+
     398+        self.assertEqual(0, len(self.avatar.sent))
     399+        self.assertEqual(0, len(self.stub.output))
     400+
     401+
     402+    def test_onStanzaAlreadyHandled(self):
     403+        """
     404+        If the stanza is marked as handled, ignore it.
     405+        """
     406+        self.protocol.connectionInitialized()
     407+
     408+        element = domish.Element((None, u'message'))
     409+        element[u'to'] = u'other@example.org'
     410+        element.handled = True
     411+        self.stub.send(element)
     412+
     413+        self.assertEqual(0, len(self.avatar.sent))
     414+        self.assertEqual(0, len(self.stub.output))
     415+
     416+
     417+    def test_onError(self):
     418+        """
     419+        A stream error is logged.
     420+        """
     421+        exc = error.StreamError('host-unknown')
     422+        self.stub.xmlstream.dispatch(exc, xmlstream.STREAM_ERROR_EVENT)
     423+        self.assertEqual(1, len(self.flushLoggedErrors(error.StreamError)))
  • c2s_stanza_handlers.patch

    r70 r72  
    11# HG changeset patch
    2 # Parent d387959177865c2f964eb013df21986a2f9020e9
     2# Parent c104dd0a9d3fb840b53661822cf06728a5b23d8f
    33Add c2s protocol handlers for iq, message and presence stanzas.
    44
     
    88 * Save last unavailable presence for future probes.
    99
    10 diff --git a/doc/examples/client_service.tac b/doc/examples/client_service.tac
    11 new file mode 100644
    12 --- /dev/null
    13 +++ b/doc/examples/client_service.tac
    14 @@ -0,0 +1,82 @@
    15 +from twisted.application import service, strports
    16 +from twisted.cred.portal import Portal
    17 +from twisted.cred.checkers import InMemoryUsernamePasswordDatabaseDontUse
    18 +from twisted.internet import defer
    19 +
    20 +from wokkel import client, xmppim
    21 +from wokkel.component import InternalComponent, Router
    22 +from wokkel.generic import FallbackHandler
    23 +from wokkel.ping import PingHandler
    24 +from wokkel.xmppim import RosterItem
    25 +
    26 +from twisted.words.protocols.jabber.jid import internJID as JID
    27 +
    28 +import socket
    29 +domain = socket.gethostname()
    30 +
    31 +ALICE = JID('alice@'+domain)
    32 +BOB = JID('bob@'+domain)
    33 +CHARLIE = JID('charlie@'+domain)
    34 +
    35 +roster = {
    36 +    'alice': {
    37 +        BOB: RosterItem(BOB,
    38 +                           subscriptionTo=True,
    39 +                           subscriptionFrom=True,
    40 +                           name='Bob'),
    41 +        CHARLIE: RosterItem(CHARLIE,
    42 +                           subscriptionTo=True,
    43 +                           subscriptionFrom=True,
    44 +                           name='Charlie',
    45 +                           groups=set(['Friends'])),
    46 +        },
    47 +    'bob': {
    48 +        ALICE: RosterItem(ALICE,
    49 +                           subscriptionTo=True,
    50 +                           subscriptionFrom=True,
    51 +                           name='Alice'),
    52 +        }
    53 +    }
    54 +
    55 +accounts = set(roster.keys())
    56 +
    57 +
    58 +class StaticRoster(xmppim.RosterServerProtocol):
    59 +
    60 +    def __init__(self, roster):
    61 +        xmppim.RosterServerProtocol.__init__(self)
    62 +        self.roster = roster
    63 +
    64 +    def getRoster(self, request):
    65 +        user = request.sender.user
    66 +        return defer.succeed(self.roster[user].values())
    67 +
    68 +
    69 +
    70 +application = service.Application("Jabber server")
    71 +
    72 +router = Router()
    73 +component = InternalComponent(router, domain)
    74 +component.setServiceParent(application)
    75 +
    76 +sessionManager = client.SessionManager(domain, accounts)
    77 +sessionManager.setHandlerParent(component)
    78 +
    79 +checker = InMemoryUsernamePasswordDatabaseDontUse(alice='secret',
    80 +                                                  bob='secret')
    81 +portal = Portal(sessionManager, (checker,))
    82 +portals = {JID(domain): portal}
    83 +
    84 +xmppim.AccountIQHandler(sessionManager).setHandlerParent(component)
    85 +xmppim.AccountMessageHandler(sessionManager).setHandlerParent(component)
    86 +xmppim.PresenceServerHandler(sessionManager, domain, roster).setHandlerParent(component)
    87 +FallbackHandler().setHandlerParent(component)
    88 +StaticRoster(roster).setHandlerParent(component)
    89 +PingHandler().setHandlerParent(component)
    90 +
    91 +c2sFactory = client.XMPPC2SServerFactory(portals)
    92 +c2sFactory.logTraffic = True
    93 +c2sService = strports.service('5224', c2sFactory)
    94 +c2sService.setServiceParent(application)
    95 +
    96 +sessionManager.connectionManager = c2sFactory
     10diff --git a/wokkel/client.py b/wokkel/client.py
     11--- a/wokkel/client.py
     12+++ b/wokkel/client.py
     13@@ -20,7 +20,7 @@
     14 from twisted.words.protocols.jabber import client, error, sasl, xmlstream
     15 from twisted.words.xish import domish
     16 
     17-from wokkel import generic
     18+from wokkel import generic, xmppim
     19 from wokkel.iwokkel import IUserSession
     20 from wokkel.subprotocols import ServerStreamManager
     21 from wokkel.subprotocols import StreamManager
     22@@ -548,4 +548,6 @@
     23         return [
     24             generic.StanzaForwarder(),
     25             RecipientAddressStamper(),
     26+            xmppim.UserSessionPresenceProtocol(),
     27+            xmppim.UserRosterProtocol(),
     28             ]
    9729diff --git a/wokkel/test/test_xmppim.py b/wokkel/test/test_xmppim.py
    9830--- a/wokkel/test/test_xmppim.py
    9931+++ b/wokkel/test/test_xmppim.py
    100 @@ -13,7 +13,7 @@
     32@@ -5,7 +5,7 @@
     33 Tests for L{wokkel.xmppim}.
     34 """
     35 
     36-from zope.interface import verify
     37+from zope.interface import implementer, verify
     38 
     39 from twisted.cred import checkers, error as ecred
     40 from twisted.cred.portal import IRealm
     41@@ -17,7 +17,7 @@
    10142 from twisted.words.xish import domish, utility
    10243 
    103  from wokkel import xmppim
     44 from wokkel import ewokkel, component, xmppim
    10445-from wokkel.generic import ErrorStanza, parseXml
    10546+from wokkel.generic import ErrorStanza, Stanza, parseXml
    10647 from wokkel.test.helpers import TestableRequestHandlerMixin, XmlStreamStub
    107  
    108  NS_XML = 'http://www.w3.org/XML/1998/namespace'
    109 @@ -1333,6 +1333,82 @@
    110  
    111  
    112  
     48 from wokkel.subprotocols import IQHandlerMixin
     49 
     50@@ -2004,3 +2004,419 @@
     51         d = self.sessionManager.probePresence(user)
     52         d.addCallback(cb)
     53         return d
     54+
     55+
     56+
    11357+class AccountIQHandlerTest(unittest.TestCase):
    11458+    """
     
    11761+
    11862+    def setUp(self):
     63+        self.user = xmppim.User(JID(u'user@example.org'))
     64+        users = {self.user.entity: self.user}
     65+        realm = xmppim.StaticRealm(u'example.org', users)
     66+
    11967+        self.stub = XmlStreamStub()
    120 +        self.protocol = xmppim.AccountIQHandler(None)
     68+        self.protocol = xmppim.AccountIQHandler(realm)
    12169+        self.protocol.makeConnection(self.stub.xmlstream)
    12270+        self.protocol.connectionInitialized()
    12371+
    12472+
    125 +    def test_onIQNotUser(self):
    126 +        """
    127 +        IQs to JIDs without local part are ignored.
    128 +        """
     73+    def test_onIQ(self):
     74+        """
     75+        IQ stanzas are observed.
     76+        """
     77+        received = []
     78+        self.patch(self.protocol, 'iqReceived', received.append)
     79+
    12980+        xml = """
    130 +          <iq to='example.org'>
     81+          <iq to='user@example.org/Home' from='contact@example.com/Work'>
    13182+            <query xmlns='jabber:iq:version'/>
    13283+          </iq>
     
    13687+        self.stub.send(iq)
    13788+
     89+        self.assertEqual(1, len(received))
     90+        stanza = received[-1]
     91+        self.assertEqual(JID(u'user@example.org/Home'), stanza.recipient)
     92+
     93+
     94+    def test_onIQNotUser(self):
     95+        """
     96+        IQs to JIDs without local part are ignored.
     97+        """
     98+        xml = """
     99+          <iq to='example.org' from='contact@example.com/Work'>
     100+            <query xmlns='jabber:iq:version'/>
     101+          </iq>
     102+        """
     103+
     104+        iq = parseXml(xml)
     105+        self.stub.send(iq)
     106+
    138107+        self.assertFalse(getattr(iq, 'handled'))
    139108+
    140109+
     110+    def test_onIQNoResource(self):
     111+        """
     112+        IQs to JIDs without resource are ignored.
     113+        """
     114+        xml = """
     115+          <iq to='user@example.org' from='contact@example.com/Work'>
     116+            <query xmlns='jabber:iq:version'/>
     117+          </iq>
     118+        """
     119+
     120+        iq = parseXml(xml)
     121+        self.stub.send(iq)
     122+
     123+        self.assertFalse(getattr(iq, 'handled'))
     124+
     125+
     126+    def test_iqReceivedDelivered(self):
     127+        """
     128+        IQs are delivered to the user's deliverIQ method.
     129+        """
     130+        received = []
     131+        self.patch(self.user, 'deliverIQ', received.append)
     132+
     133+        stanza = Stanza(recipient=JID(u'user@example.org/Home'),
     134+                        sender=JID(u'contact@example.com/Work'))
     135+        stanza.stanzaKind = u'iq'
     136+        stanza.stanzaType = u'get'
     137+        self.protocol.iqReceived(stanza)
     138+        self.assertEqual([stanza], received)
     139+
     140+
     141+    def test_iqReceivedNoSuchUser(self):
     142+        """
     143+        IQs are delivered to the user's deliverIQ method.
     144+        """
     145+        def deliverIQ(stanza):
     146+            raise ewokkel.NoSuchUser()
     147+
     148+        def cb(error):
     149+            self.assertEqual('service-unavailable', error.condition)
     150+
     151+        self.patch(self.user, 'deliverIQ', deliverIQ)
     152+
     153+        stanza = Stanza(recipient=JID(u'other@example.org/Home'),
     154+                        sender=JID(u'contact@example.com/Work'))
     155+        stanza.stanzaKind = u'iq'
     156+        stanza.stanzaType = u'get'
     157+        d = self.protocol.iqReceived(stanza)
     158+        self.assertFailure(d, error.StanzaError)
     159+        d.addCallback(cb)
     160+        return d
     161+
     162+
    141163+
    142164+class AccountMessageHandlerTest(unittest.TestCase):
     
    146168+
    147169+    def setUp(self):
     170+        self.user = xmppim.User(JID(u'test@example.org'))
     171+        users = {self.user.entity: self.user}
     172+        realm = xmppim.StaticRealm(u'example.org', users)
     173+
    148174+        self.stub = XmlStreamStub()
    149 +        self.protocol = xmppim.AccountMessageHandler(None)
     175+        self.protocol = xmppim.AccountMessageHandler(realm)
    150176+        self.protocol.makeConnection(self.stub.xmlstream)
    151177+        self.protocol.connectionInitialized()
    152178+
    153179+
    154 +    def test_onMessageNotUser(self):
    155 +        """
    156 +        Messages to JIDs without local part are ignored.
    157 +        """
     180+    def test_messageReceived(self):
     181+        """
     182+        Message stanzas are observed.
     183+        """
     184+        received = []
     185+        self.patch(self.protocol, 'messageReceived', received.append)
     186+
    158187+        xml = """
    159188+          <message to='example.org'>
     
    165194+        self.stub.send(message)
    166195+
     196+        self.assertEqual(1, len(received))
     197+        stanza = received[-1]
     198+        self.assertEqual(u'Hello', stanza.body)
     199+
     200+
     201+
     202+    def test_messageReceivedNotUser(self):
     203+        """
     204+        Messages to JIDs without local part are ignored.
     205+
     206+        This also tests the observer that is set up to handle message stanzas.
     207+        """
     208+        xml = """
     209+          <message to='example.org'>
     210+            <body>Hello</body>
     211+          </message>
     212+        """
     213+
     214+        message = parseXml(xml)
     215+        self.stub.send(message)
     216+
    167217+        self.assertFalse(getattr(message, 'handled'))
    168218+
    169219+
    170 +
    171 +class ClonePresenceTest(unittest.TestCase):
    172 +    """
    173 +    Tests for L{xmppim.clonePresence}.
    174 +    """
    175 +
    176 +    def test_rootElement(self):
    177 +        """
    178 +        The copied presence stanza is not identical, but renders identically.
    179 +        """
    180 +        originalElement = domish.Element((None, 'presence'))
    181 +        stanza = Stanza.fromElement(originalElement)
    182 +        copyElement = xmppim.clonePresence(stanza)
    183 +
    184 +        self.assertNotIdentical(copyElement, originalElement)
    185 +        self.assertEquals(copyElement.toXml(), originalElement.toXml())
    186 +
    187 +
    188 +
    189  class RosterServerProtocolTest(unittest.TestCase, TestableRequestHandlerMixin):
    190      """
    191      Tests for L{xmppim.RosterServerProtocol}.
     220+    def test_messageReceivedDelivered(self):
     221+        """
     222+        Messages are delivered to the user's deliverMessage method.
     223+        """
     224+        received = []
     225+        self.patch(self.user, 'deliverMessage', received.append)
     226+
     227+        stanza = xmppim.Message(recipient=JID(u'test@example.org'),
     228+                                sender=JID(u'other@example.com'))
     229+        self.protocol.messageReceived(stanza)
     230+        self.assertEqual([stanza], received)
     231+
     232+
     233+    def test_messageReceivedNotImplemented(self):
     234+        """
     235+        If NotImplementedError is raised, service-available is returned.
     236+        """
     237+        def deliverMessage(stanza):
     238+            raise NotImplementedError()
     239+
     240+        def cb(error):
     241+            self.assertEqual('service-unavailable', error.condition)
     242+
     243+        self.patch(self.user, 'deliverMessage', deliverMessage)
     244+
     245+        stanza = xmppim.Message(recipient=JID(u'test@example.org'),
     246+                                sender=JID(u'other@example.com'))
     247+        d = self.protocol.messageReceived(stanza)
     248+        self.assertFailure(d, error.StanzaError)
     249+        d.addCallback(cb)
     250+        return d
     251+
     252+
     253+@implementer(xmppim.IUserSession)
     254+class FakeUserSession(xmppim.UserSession):
     255+
     256+    def __init__(self, *args, **kwargs):
     257+        super(FakeUserSession, self).__init__(*args, **kwargs)
     258+        self.probePresenceCalls = 0
     259+        self.broadcastPresenceCalls = []
     260+
     261+
     262+    def probePresence(self):
     263+        self.probePresenceCalls += 1
     264+
     265+
     266+    def broadcastPresence(self, presence, available):
     267+        self.broadcastPresenceCalls.append((presence, available))
     268+
     269+
     270+
     271+class FakeUserSessionTest(unittest.TestCase):
     272+    """
     273+    Tests for L{FakeUserSessionTest}.
     274+    """
     275+
     276+    def test_interface(self):
     277+        verify.verifyObject(xmppim.IUserSession, FakeUserSession(None))
     278+
     279+
     280+
     281+class UserSessionPresenceProtocolTest(unittest.TestCase):
     282+    """
     283+    Tests for L{xmppim.UserSessionPresenceProtocol}.
     284+    """
     285+
     286+    def setUp(self):
     287+        self.stub = XmlStreamStub()
     288+        self.protocol = xmppim.UserSessionPresenceProtocol()
     289+        self.protocol.makeConnection(self.stub.xmlstream)
     290+
     291+        entity = JID(u'user@example.org')
     292+        user = xmppim.User(entity)
     293+        user.roster = xmppim.InMemoryRoster([])
     294+
     295+        self.session = FakeUserSession(user)
     296+        self.stub.xmlstream.avatar = self.session
     297+
     298+
     299+    def test_availableReceivedDirected(self):
     300+        """
     301+        The directed available stanza is passed for delivery.
     302+        """
     303+        stanza = xmppim.AvailabilityPresence(
     304+                recipient=JID(u'contact@example.org'),
     305+                sender=JID(u'user@example.org'),
     306+                available=True)
     307+
     308+        self.assertFalse(self.protocol.availableReceived(stanza))
     309+
     310+
     311+    def test_availableReceivedBroadcast(self):
     312+        """
     313+        Availabilty is broadcast to contacts and presence is probed.
     314+        """
     315+        stanza = xmppim.AvailabilityPresence(
     316+                sender=JID(u'user@example.org'),
     317+                available=True)
     318+
     319+        self.assertTrue(self.protocol.availableReceived(stanza))
     320+        self.assertEqual(1, self.session.probePresenceCalls)
     321+        self.assertEqual(1, len(self.session.broadcastPresenceCalls))
     322+        presence, available = self.session.broadcastPresenceCalls[-1]
     323+        self.assertIdentical(stanza, presence)
     324+        self.assertTrue(available)
     325+
     326+
     327+    def test_unavailableReceivedDirected(self):
     328+        """
     329+        The directed available stanza is passed for delivery.
     330+        """
     331+        stanza = xmppim.AvailabilityPresence(
     332+                recipient=JID(u'contact@example.org'),
     333+                sender=JID(u'user@example.org'),
     334+                available=False)
     335+
     336+        self.assertFalse(self.protocol.unavailableReceived(stanza))
     337+
     338+
     339+    def test_unavailableReceivedBroadcast(self):
     340+        """
     341+        Unavailabilty is broadcast to contacts.
     342+        """
     343+        stanza = xmppim.AvailabilityPresence(
     344+                sender=JID(u'user@example.org'),
     345+                available=False)
     346+
     347+        self.assertTrue(self.protocol.unavailableReceived(stanza))
     348+        self.assertEqual(0, self.session.probePresenceCalls)
     349+        self.assertEqual(1, len(self.session.broadcastPresenceCalls))
     350+        presence, available = self.session.broadcastPresenceCalls[-1]
     351+        self.assertIdentical(stanza, presence)
     352+        self.assertFalse(available)
     353+
     354+
     355+    def test_subscribedReceived(self):
     356+        """
     357+        The subscription approval stanza is passed for delivery.
     358+        """
     359+        stanza = xmppim.SubscriptionPresence(
     360+                recipient=JID(u'contact@example.org'),
     361+                sender=JID(u'user@example.org'))
     362+        stanza.stanzaType = u'subscribed'
     363+
     364+        self.assertFalse(self.protocol.unsubscribedReceived(stanza))
     365+
     366+
     367+    def test_subscribedReceivedFullJID(self):
     368+        """
     369+        The resource is stripped from full JIDs for subscription approval.
     370+        """
     371+        stanza = xmppim.SubscriptionPresence(
     372+                recipient=JID(u'contact@example.org/Home'),
     373+                sender=JID(u'user@example.org'))
     374+        stanza.stanzaType = u'subscribed'
     375+        stanza.toElement()
     376+
     377+        self.protocol.subscribedReceived(stanza)
     378+        self.assertEqual(u'contact@example.org', stanza.element['to'])
     379+
     380+
     381+    def test_unsubscribedReceived(self):
     382+        """
     383+        The subscription cancellation stanza is passed for delivery.
     384+        """
     385+        stanza = xmppim.SubscriptionPresence(
     386+                recipient=JID(u'contact@example.org'),
     387+                sender=JID(u'user@example.org'))
     388+        stanza.stanzaType = u'unsubscribed'
     389+
     390+        self.assertFalse(self.protocol.unsubscribedReceived(stanza))
     391+
     392+
     393+    def test_unsubscribedReceivedFullJID(self):
     394+        """
     395+        The resource is stripped from full JIDs for subscription cancellation.
     396+        """
     397+        stanza = xmppim.SubscriptionPresence(
     398+                recipient=JID(u'contact@example.org/Home'),
     399+                sender=JID(u'user@example.org'))
     400+        stanza.stanzaType = u'unsubscribed'
     401+        stanza.toElement()
     402+
     403+        self.protocol.unsubscribedReceived(stanza)
     404+        self.assertEqual(u'contact@example.org', stanza.element['to'])
     405+
     406+
     407+    def test_subscribeReceived(self):
     408+        """
     409+        The subscribe request stanza is passed for delivery.
     410+        """
     411+        stanza = xmppim.SubscriptionPresence(
     412+                recipient=JID(u'contact@example.org'),
     413+                sender=JID(u'user@example.org'))
     414+        stanza.stanzaType = u'subscribe'
     415+
     416+        self.assertFalse(self.protocol.subscribedReceived(stanza))
     417+
     418+
     419+    def test_subscribeReceivedFullJID(self):
     420+        """
     421+        The resource is stripped from full JIDs for subscribe requests.
     422+        """
     423+        stanza = xmppim.SubscriptionPresence(
     424+                recipient=JID(u'contact@example.org/Home'),
     425+                sender=JID(u'user@example.org'))
     426+        stanza.stanzaType = u'subscribe'
     427+        stanza.toElement()
     428+
     429+        self.protocol.subscribeReceived(stanza)
     430+        self.assertEqual(u'contact@example.org', stanza.element['to'])
     431+
     432+
     433+    def test_unsubscribeReceived(self):
     434+        """
     435+        The unsubscribe request stanza is passed for delivery.
     436+        """
     437+        stanza = xmppim.SubscriptionPresence(
     438+                recipient=JID(u'contact@example.org'),
     439+                sender=JID(u'user@example.org'))
     440+        stanza.stanzaType = u'unsubscribe'
     441+
     442+        self.assertFalse(self.protocol.unsubscribeReceived(stanza))
     443+
     444+
     445+    def test_unsubscribeReceivedFullJID(self):
     446+        """
     447+        The resource is stripped from full JIDs for unsubscribe requests.
     448+        """
     449+        stanza = xmppim.SubscriptionPresence(
     450+                recipient=JID(u'contact@example.org/Home'),
     451+                sender=JID(u'user@example.org'))
     452+        stanza.stanzaType = u'unsubscribe'
     453+        stanza.toElement()
     454+
     455+        self.protocol.unsubscribeReceived(stanza)
     456+        self.assertEqual(u'contact@example.org', stanza.element['to'])
     457+
     458+
     459+    def test_probeReceived(self):
     460+        """
     461+        The probe stanza is passed for delivery.
     462+        """
     463+        stanza = xmppim.ProbePresence(
     464+                recipient=JID(u'contact@example.org'),
     465+                sender=JID(u'user@example.org'))
     466+
     467+        self.assertFalse(self.protocol.probeReceived(stanza))
     468+
     469+
    192470diff --git a/wokkel/xmppim.py b/wokkel/xmppim.py
    193471--- a/wokkel/xmppim.py
    194472+++ b/wokkel/xmppim.py
    195 @@ -12,7 +12,10 @@
    196  
    197  import warnings
    198  
    199 +import copy
    200 +
    201  from twisted.internet import defer
    202 +from twisted.python import log
    203  from twisted.words.protocols.jabber import error
    204  from twisted.words.protocols.jabber.jid import JID
    205  from twisted.words.xish import domish
    206 @@ -408,10 +411,7 @@
    207  
    208  
    209  
    210 -    def _onPresence(self, element):
    211 -        """
    212 -        Called when a presence stanza has been received.
    213 -        """
     473@@ -417,27 +417,33 @@
     474 
     475 
     476 
    214477+    def parsePresence(self, element):
    215          stanza = Stanza.fromElement(element)
    216  
    217          presenceType = stanza.stanzaType or 'available'
    218 @@ -421,14 +421,22 @@
    219          except KeyError:
    220              return
    221  
     478+        stanza = Stanza.fromElement(element)
     479+
     480+        presenceType = stanza.stanzaType or 'available'
     481+
     482+        try:
     483+            parser = self.presenceTypeParserMap[presenceType]
     484+        except KeyError:
     485+            return
     486+
     487+        return parser.fromElement(element)
     488+
     489+
     490+    @asyncObserver
     491     def _onPresence(self, element):
     492         """
     493         Called when a presence stanza has been received.
     494         """
     495-        stanza = Stanza.fromElement(element)
     496-
     497-        presenceType = stanza.stanzaType or 'available'
     498-
     499-        try:
     500-            parser = self.presenceTypeParserMap[presenceType]
     501-        except KeyError:
     502-            return
     503-
    222504-        presence = parser.fromElement(element)
    223 +        return parser.fromElement(element)
    224 +
    225 +
    226 +    def _onPresence(self, element):
    227 +        """
    228 +        Called when a presence stanza has been received.
    229 +        """
    230505+        presence = self.parsePresence(element)
    231506+        presenceType = presence.stanzaType or 'available'
     
    234509             handler = getattr(self, '%sReceived' % presenceType)
    235510         except AttributeError:
    236              return
     511-            return
     512+            return False
    237513         else:
    238514-            handler(presence)
    239 +            element.handled = handler(presence)
    240  
    241  
    242  
    243 @@ -1035,6 +1043,13 @@
    244          return element
    245  
    246  
    247 +    @classmethod
    248 +    def fromElement(cls, element):
    249 +        stanza = super(Message, cls).fromElement(element)
    250 +        stanza.stanzaType = stanza.stanzaType or 'normal'
    251 +        return stanza
    252 +
    253 +
    254  
    255  class MessageProtocol(XMPPHandler):
     515+            return handler(presence)
     516 
     517 
     518 
     519@@ -1188,10 +1194,12 @@
     520         L{Message}.
    256521     """
    257 @@ -1067,6 +1082,441 @@
    258  
    259  
    260  
     522 
     523+    observerPriority = 0
     524     stanzaFactory = Message
     525 
     526     def connectionInitialized(self):
     527-        self.xmlstream.addObserver("/message", self._onMessage)
     528+        self.xmlstream.addObserver("/message", self._onMessage,
     529+                                   self.observerPriority)
     530 
     531 
     532     @asyncObserver
     533@@ -1673,3 +1681,246 @@
     534             presence = ProbePresence(recipient=entity,
     535                                      sender=user.entity)
     536             self.routeOrDeliver(presence.toElement())
     537+
     538+
     539+
    261540+class AccountIQHandler(XMPPHandler):
    262541+
    263 +    def __init__(self, sessionManager):
     542+    def __init__(self, realm):
    264543+        XMPPHandler.__init__(self)
    265 +        self.sessionManager = sessionManager
     544+        self.realm = realm
    266545+
    267546+
    268547+    def connectionMade(self):
    269 +        self.xmlstream.addObserver('/iq', self.onIQ, 1)
    270 +
    271 +
    272 +    def onIQ(self, iq):
    273 +        """
    274 +        Handler for iq stanzas to user accounts' connected resources.
     548+        self.xmlstream.addObserver('/iq', self._onIQ, 1)
     549+
     550+
     551+    @asyncObserver
     552+    def _onIQ(self, element):
     553+        stanza = Stanza.fromElement(element)
     554+        return self.iqReceived(stanza)
     555+
     556+
     557+    def iqReceived(self, iq):
     558+        """
     559+        Handler for iq stazas to user accounts' connected resources.
    275560+
    276561+        If the recipient is a bare JID or there is no associated user, this
     
    280565+        the iq.
    281566+        """
    282 +
    283 +        if iq.handled:
     567+        if not iq.recipient.user:
     568+            # This is not for a user.
     569+            return False
     570+        elif not iq.recipient.resource:
     571+            # This might be picked up by another handler.
     572+            return False
     573+
     574+        def gotUser(user):
     575+            user.deliverIQ(iq)
     576+
     577+        def notFound(failure):
     578+            failure.trap(NoSuchResource, NoSuchUser)
     579+            raise error.StanzaError('service-unavailable')
     580+
     581+        d = self.realm.lookupUser(iq.recipient.userhostJID())
     582+        d.addCallback(gotUser)
     583+        d.addErrback(notFound)
     584+        return d
     585+
     586+
     587+
     588+class AccountMessageHandler(MessageProtocol):
     589+
     590+    observerPriority = 1
     591+
     592+    def __init__(self, realm):
     593+        MessageProtocol.__init__(self)
     594+        self.realm = realm
     595+
     596+
     597+    def messageReceived(self, message):
     598+        """
     599+        Handler for message stanzas to user accounts.
     600+        """
     601+        if not message.recipient.user:
     602+            # This is not for a user.
     603+            return False
     604+
     605+        def gotUser(user):
     606+            user.deliverMessage(message)
     607+
     608+        def eb(failure):
     609+            failure.trap(NoSuchUser, NoSuchResource, NotImplementedError)
     610+            raise error.StanzaError('service-unavailable')
     611+
     612+        d = self.realm.lookupUser(message.recipient.userhostJID())
     613+        d.addCallback(gotUser)
     614+        d.addErrback(eb)
     615+        return d
     616+
     617+
     618+
     619+class AccountPresenceHandler(PresenceProtocol):
     620+
     621+    def __init__(self, realm):
     622+        PresenceProtocol.__init__(self)
     623+        self.realm = realm
     624+
     625+
     626+    def _deliverPresence(self, presence):
     627+        if not presence.recipient.user:
     628+            # This is not for a user.
     629+            return False
     630+
     631+        def gotUser(user):
     632+            user.deliverPresence(presence)
     633+
     634+        def notFound(failure):
     635+            failure.trap(NoSuchResource, NoSuchUser)
    284636+            return
    285637+
    286 +        stanza = Stanza.fromElement(iq)
    287 +        recipient = stanza.recipient
    288 +
    289 +        if not recipient:
    290 +            # This stanza doesn't have a recipient, ignore it.
    291 +            return
    292 +        elif not recipient.user:
    293 +            # This is not for an account, ignore it
    294 +            return
    295 +        elif recipient.user not in self.sessionManager.accounts:
    296 +            # This is not a user, ignore it
    297 +            return
    298 +        elif not recipient.resource:
    299 +            # Bare JID at local domain, ignore it
    300 +            return
    301 +
    302 +        userSessions = self.sessionManager.lookupSessions(recipient)
    303 +        if recipient.resource in userSessions:
    304 +            self.sessionManager.deliverStanza(iq, recipient)
    305 +        else:
    306 +            # Full JID without connected resource, return error
    307 +            exc = error.StanzaError('service-unavailable')
    308 +            if stanza.stanzaType in ('result', 'error'):
    309 +                log.err(exc, 'Could not deliver IQ response')
    310 +            else:
    311 +                self.send(exc.toResponse(iq))
    312 +
    313 +        iq.handled = True
    314 +
    315 +
    316 +
    317 +class AccountMessageHandler(XMPPHandler):
    318 +
    319 +    def __init__(self, sessionManager):
    320 +        XMPPHandler.__init__(self)
    321 +        self.sessionManager = sessionManager
    322 +
    323 +
    324 +    def connectionMade(self):
    325 +        self.xmlstream.addObserver('/message', self.onMessage, 1)
    326 +
    327 +
    328 +    def onMessage(self, element):
    329 +        """
    330 +        Handler for message stanzas to user accounts.
    331 +        """
    332 +
    333 +        if element.handled:
    334 +            return
    335 +
    336 +        message = Message.fromElement(element)
    337 +        recipient = message.recipient
    338 +        stanzaType = message.stanzaType or 'normal'
    339 +
    340 +        try:
    341 +            if not recipient:
    342 +                # This stanza doesn't have a recipient, ignore it.
    343 +                return
    344 +            if not recipient.user:
    345 +                # This is not for an account, ignore it
    346 +                return
    347 +            elif recipient.user not in self.sessionManager.accounts:
    348 +                # This is not a user, ignore it
    349 +                return
    350 +            elif recipient.resource:
    351 +                userSessions = self.sessionManager.lookupSessions(recipient)
    352 +                if recipient.resource in userSessions:
    353 +                    self.sessionManager.deliverStanza(element, recipient)
    354 +                else:
    355 +                    if stanzaType in ('normal', 'chat', 'headline'):
    356 +                        self.onMessageBareJID(message)
    357 +                    elif stanzaType == 'error':
    358 +                        log.msg("Dropping message to unconnected resource %r" %
    359 +                                recipient.full())
    360 +                    elif stanzaType == 'groupchat':
    361 +                        raise error.StanzaError('service-unavailable')
    362 +            else:
    363 +                self.onMessageBareJID(message)
    364 +        except error.StanzaError, exc:
    365 +            if stanzaType == 'error':
    366 +                log.err(exc, "Undeliverable error")
    367 +            else:
    368 +                self.send(exc.toResponse(element))
    369 +
    370 +        element.handled = True
    371 +
    372 +
    373 +    def onMessageBareJID(self, message):
    374 +        userSessions = self.sessionManager.lookupSessions(message.recipient)
    375 +
    376 +        recipients = set()
    377 +
    378 +        if message.stanzaType == 'headline':
    379 +            for session in userSessions.itervalues():
    380 +                if session.presence.priority >= 0:
    381 +                    recipients.add(session.entity)
    382 +        elif message.stanzaType in ('chat', 'normal'):
    383 +            priorities = {}
    384 +            for session in userSessions.itervalues():
    385 +                if not session.presence or not session.presence.available:
    386 +                    continue
    387 +                priority = session.presence.priority
    388 +                if priority >= 0:
    389 +                    priorities.setdefault(priority, set()).add(session.entity)
    390 +            if priorities:
    391 +                maxPriority = max(priorities.keys())
    392 +                recipients.update(priorities[maxPriority])
    393 +        elif message.stanzaType == 'groupchat':
    394 +            raise error.StanzaError('service-unavailable')
    395 +
    396 +        if recipients:
    397 +            for recipient in recipients:
    398 +                self.sessionManager.deliverStanza(message.element, recipient)
    399 +        elif message.stanzaType in ('chat', 'normal'):
    400 +            raise error.StanzaError('service-unavailable')
    401 +        else:
    402 +            # silently discard
    403 +            log.msg("Discarding message to %r" % message.recipient)
    404 +
    405 +
    406 +
    407 +
    408 +def clonePresence(presence):
    409 +    """
    410 +    Make a deep copy of a presence stanza.
    411 +
    412 +    The returned presence stanza is an orphaned deep copy of the given
    413 +    original.
    414 +
    415 +    @note: Since the reference to the original parent, if any, is gone,
    416 +    inherited attributes like C{xml:lang} are not preserved.
    417 +    """
    418 +    element = presence.element
    419 +
    420 +    parent = element.parent
    421 +    element.parent = None
    422 +    newElement = copy.deepcopy(element)
    423 +    element.parent = parent
    424 +    return newElement
    425 +
    426 +
    427 +
    428 +class PresenceServerHandler(PresenceProtocol):
    429 +
    430 +    def __init__(self, sessionManager, domain, roster):
    431 +        PresenceProtocol.__init__(self)
    432 +        self.sessionManager = sessionManager
    433 +        self.domain = domain
    434 +        self.roster = roster
    435 +        self.presences = {} # user -> resource -> presence
    436 +        self.offlinePresences = {} # user -> presence
    437 +        self.remotePresences = {} # user -> remote entity -> presence
    438 +
    439 +        self.sessionManager.clientStream.addObserver('/presence',
    440 +                                                     self._onPresenceOutbound)
    441 +
    442 +
    443 +    def _onPresenceOutbound(self, element):
    444 +        log.msg("Got outbound presence: %r" % element.toXml())
    445 +        presence = self.parsePresence(element)
    446 +
    447 +        presenceType = presence.stanzaType or 'available'
    448 +        method = '%sReceivedOutbound' % presenceType
    449 +        print method
    450 +
    451 +        try:
    452 +            handler = getattr(self, method)
    453 +        except AttributeError:
    454 +            return
    455 +        else:
    456 +            element.handled = handler(presence)
    457 +
    458 +
    459 +    def _broadcastToOtherResources(self, presence):
    460 +        """
    461 +        Broadcast presence to other available resources.
    462 +        """
    463 +        fromJID = presence.sender
    464 +        for otherResource in self.presences[fromJID.user]:
    465 +            if otherResource == fromJID.resource:
    466 +                continue
    467 +
    468 +            resourceJID = JID(tuple=(fromJID.user,
    469 +                                     fromJID.host,
    470 +                                     otherResource))
    471 +            outPresence = clonePresence(presence)
    472 +            outPresence['to'] = resourceJID.full()
    473 +            self.sessionManager.deliverStanza(outPresence, resourceJID)
    474 +
    475 +
    476 +    def _broadcastToContacts(self, presence):
    477 +        """
    478 +        Broadcast presence to subscribed entities.
    479 +        """
    480 +        fromJID = presence.sender
    481 +        roster = self.roster[fromJID.user]
    482 +
    483 +        for item in roster.itervalues():
    484 +            if not item.subscriptionFrom:
    485 +                continue
    486 +
    487 +            outPresence = clonePresence(presence)
    488 +            outPresence['to'] = item.entity.full()
    489 +
    490 +            if item.entity.host == self.domain:
    491 +                # local contact
    492 +                if item.entity.user in self.presences:
    493 +                    # broadcast to contact's available resources
    494 +                    for itemResource in self.presences[item.entity.user]:
    495 +                        resourceJID = JID(tuple=(item.entity.user,
    496 +                                                 item.entity.host,
    497 +                                                 itemResource))
    498 +                        self.sessionManager.deliverStanza(outPresence,
    499 +                                                          resourceJID)
    500 +            else:
    501 +                # remote contact
    502 +                self.send(outPresence)
    503 +
    504 +
    505 +    def _on_availableBroadcast(self, presence):
    506 +        fromJID = presence.sender
    507 +        user, resource = fromJID.user, fromJID.resource
    508 +        roster = self.roster[user]
    509 +
    510 +        if user not in self.presences:
    511 +            # initial presence
    512 +            self.presences[user] = {}
    513 +            self.remotePresences[user] = {}
    514 +
    515 +            # send out probes
    516 +            for item in roster.itervalues():
    517 +                if item.subscriptionTo and item.entity.host != self.domain:
    518 +                    self.probe(item.entity, fromJID)
    519 +        else:
    520 +            if resource not in self.presences[user]:
    521 +                # initial presence with another available resource
    522 +
    523 +                # send last known presences from remote contacts
    524 +                remotePresences = self.remotePresences[user]
    525 +                for entity, remotePresence in remotePresences.iteritems():
    526 +                    self.sessionManager.deliverStanza(remotePresence.element,
    527 +                                                      fromJID)
    528 +
    529 +            # send presence to other resources
    530 +            self._broadcastToOtherResources(presence)
    531 +
    532 +        # Send last known local presences
    533 +        if user not in self.presences or resource not in self.presences[user]:
    534 +            for item in roster.itervalues():
    535 +                if item.subscriptionTo and \
    536 +                   item.entity.host == self.domain and \
    537 +                   item.entity.user in self.presences:
    538 +                    for contactPresence in \
    539 +                            self.presences[item.entity.user].itervalues():
    540 +                        outPresence = clonePresence(contactPresence)
    541 +                        outPresence['to'] = fromJID.userhost()
    542 +                        self.sessionManager.deliverStanza(outPresence, fromJID)
    543 +
    544 +        # broadcast presence
    545 +        self._broadcastToContacts(presence)
    546 +
    547 +        # save presence
    548 +        self.presences[user][resource] = presence
    549 +        session = self.sessionManager.lookupSession(fromJID)
    550 +        session.presence = presence
    551 +
    552 +        return True
    553 +
    554 +
    555 +    def _on_availableDirected(self, presence):
    556 +        self.send(presence.element)
    557 +        return True
    558 +
    559 +
    560 +    def availableReceivedOutbound(self, presence):
    561 +        if presence.recipient:
    562 +            return self._on_availableDirected(presence)
    563 +        else:
    564 +            return self._on_availableBroadcast(presence)
     638+        d = self.realm.lookupUser(presence.recipient.userhostJID())
     639+        d.addCallback(gotUser)
     640+        d.addErrback(notFound)
     641+        return d
    565642+
    566643+
    567644+    def availableReceived(self, presence):
    568 +        fromJID = presence.sender
    569 +        toJID = presence.recipient
    570 +
    571 +        if not toJID.user:
    572 +            # This is not for an account, ignore it.
    573 +            return False
    574 +        elif toJID.user not in self.roster:
    575 +            # This is not for a known account, ignore it.
    576 +            return False
    577 +        elif toJID.user not in self.presences:
    578 +            # No available resource, drop it.
    579 +            return True
    580 +        else:
    581 +            for resource in self.presences[toJID.user]:
    582 +                resourceJID = JID(tuple=(toJID.user,
    583 +                                         toJID.host,
    584 +                                         resource))
    585 +                self.sessionManager.deliverStanza(presence.element, resourceJID)
    586 +            self.remotePresences[toJID.user][fromJID] = presence
    587 +            return True
    588 +
    589 +
    590 +    def _on_unavailableBroadcast(self, presence):
    591 +        fromJID = presence.sender
    592 +        user, resource = fromJID.user, fromJID.resource
    593 +
    594 +        # broadcast presence
    595 +        self._broadcastToContacts(presence)
    596 +
    597 +        if user in self.presences:
    598 +            # send presence to other resources
    599 +            self._broadcastToOtherResources(presence)
    600 +
    601 +            # update stored presences
    602 +            if resource in self.presences[user]:
    603 +                del self.presences[user][resource]
    604 +
    605 +            if not self.presences[user]:
    606 +                # last resource to become unavailable
    607 +                del self.presences[user]
    608 +
    609 +                # TODO: save last unavailable presence
    610 +
    611 +        return True
    612 +
    613 +
    614 +    def _on_unavailableDirected(self, presence):
    615 +        self.send(presence.element)
    616 +        return True
    617 +
    618 +
    619 +    def unavailableReceivedOutbound(self, presence):
    620 +        if presence.recipient:
    621 +            return self._on_unavailableDirected(presence)
    622 +        else:
    623 +            return self._on_unavailableBroadcast(presence)
    624 +
    625 +#    def unavailableReceived(self, presence):
    626 +
    627 +
    628 +    def subscribedReceivedOutbound(self, presence):
    629 +        log.msg("%r subscribed %s to its presence" % (presence.sender,
    630 +                                                      presence.recipient))
    631 +        self.send(presence.element)
    632 +        return True
     645+        return self._deliverPresence(presence)
     646+
     647+
     648+    def unavailableReceived(self, presence):
     649+        return self._deliverPresence(presence)
    633650+
    634651+
     
    636653+        log.msg("%r subscribed %s to its presence" % (presence.sender,
    637654+                                                      presence.recipient))
    638 +
    639 +
    640 +    def unsubscribedReceivedOutbound(self, presence):
    641 +        log.msg("%r unsubscribed %s from its presence" % (presence.sender,
    642 +                                                          presence.recipient))
    643 +        self.send(presence.element)
    644 +        return True
     655+        return self._deliverPresence(presence)
    645656+
    646657+
     
    648659+        log.msg("%r unsubscribed %s from its presence" % (presence.sender,
    649660+                                                          presence.recipient))
    650 +
    651 +
    652 +    def subscribeReceivedOutbound(self, presence):
    653 +        log.msg("%r requests subscription to %s" % (presence.sender,
    654 +                                                    presence.recipient))
    655 +        self.send(presence.element)
    656 +        return True
     661+        return self._deliverPresence(presence)
    657662+
    658663+
     
    660665+        log.msg("%r requests subscription to %s" % (presence.sender,
    661666+                                                    presence.recipient))
    662 +
    663 +
    664 +    def unsubscribeReceivedOutbound(self, presence):
    665 +        log.msg("%r requests unsubscription from %s" % (presence.sender,
    666 +                                                        presence.recipient))
    667 +        self.send(presence.element)
    668 +        return True
     667+        return self._deliverPresence(presence)
    669668+
    670669+
     
    672671+        log.msg("%r requests unsubscription from %s" % (presence.sender,
    673672+                                                        presence.recipient))
     673+        return self._deliverPresence(presence)
    674674+
    675675+
    676676+    def probeReceived(self, presence):
    677 +        fromJID = presence.sender
    678 +        toJID = presence.recipient
    679 +
    680 +        if toJID.user not in self.roster or \
    681 +           fromJID.userhost() not in self.roster[toJID.user] or \
    682 +           not self.roster[toJID.user][fromJID.userhost()].subscriptionFrom:
    683 +            # send unsubscribed
    684 +            pass
    685 +        elif toJID.user not in self.presences:
    686 +            # send last unavailable or nothing
    687 +            pass
     677+        if not presence.recipient.user:
     678+            # This is not for a user.
     679+            return False
     680+
     681+        def gotUser(user):
     682+            return user.getPresences(presence.sender.userhostJID())
     683+
     684+        def notSubscribed(failure):
     685+            failure.trap(NoSuchUser, NotSubscribed)
     686+            response = SubscriptionPresence(recipient=presence.sender,
     687+                                            sender=presence.recipient)
     688+            response.stanzaType = 'unsubscribed'
     689+            return [response]
     690+
     691+        def sendPresences(userPresences):
     692+            for userPresence in userPresences:
     693+                self.parent.multicast(userPresence, [presence.sender])
     694+
     695+        d = self.realm.lookupUser(presence.recipient.userhostJID())
     696+        d.addCallback(gotUser)
     697+        d.addErrback(notSubscribed)
     698+        d.addCallback(sendPresences)
     699+        return d
     700+
     701+
     702+
     703+class UserSessionPresenceProtocol(PresenceProtocol):
     704+    """
     705+    Presence protocol for user client sessions.
     706+    """
     707+
     708+    def _stampContactJID(self, presence):
     709+        """
     710+        Stamp the contact JID to be a bare JID.
     711+        """
     712+        if presence.recipient.resource:
     713+            contactJID = presence.recipient.userhostJID()
     714+            presence.element['to'] = contactJID.userhost()
     715+
     716+
     717+    def availableReceived(self, presence):
     718+        if presence.recipient:
     719+            # Pass through directed presence.
     720+            return False
    688721+        else:
    689 +            for resourcePresence in self.presences[toJID.user].itervalues():
    690 +                outPresence = clonePresence(resourcePresence)
    691 +                outPresence['to'] = fromJID.userhost()
    692 +                self.send(outPresence)
    693 +
    694 +
    695 +
    696  class RosterServerProtocol(XMPPHandler, IQHandlerMixin):
    697      """
    698      XMPP subprotocol handler for the roster, server side.
     722+            session = self.xmlstream.avatar
     723+            sendProbe = not session.presence
     724+
     725+            session.broadcastPresence(presence, available=True)
     726+
     727+            if sendProbe:
     728+                session.probePresence()
     729+
     730+            return True
     731+
     732+
     733+    def unavailableReceived(self, presence):
     734+        if presence.recipient:
     735+            # Pass through directed presence.
     736+            return False
     737+        else:
     738+            session = self.xmlstream.avatar
     739+            session.broadcastPresence(presence, available=False)
     740+            return True
     741+
     742+
     743+    def subscribedReceived(self, presence):
     744+        """
     745+        Subscription approval confirmation was received.
     746+        """
     747+        self._stampContactJID(presence)
     748+        return False
     749+
     750+
     751+    def unsubscribedReceived(self, presence):
     752+        """
     753+        Unsubscription confirmation was received.
     754+        """
     755+        self._stampContactJID(presence)
     756+        return False
     757+
     758+
     759+    def subscribeReceived(self, presence):
     760+        """
     761+        Subscription request was received.
     762+        """
     763+        self._stampContactJID(presence)
     764+        return False
     765+
     766+
     767+    def unsubscribeReceived(self, presence):
     768+        """
     769+        Unsubscription request was received.
     770+        """
     771+        self._stampContactJID(presence)
     772+        return False
     773+
     774+
     775+    def probeReceived(self, presence):
     776+        """
     777+        Probe presence was received.
     778+        """
     779+        return False
  • client_listen_authenticator.patch

    r66 r72  
    11# HG changeset patch
    2 # Parent a1648553ea06b7f0b38fec775db71ac03782e3d6
     2# Parent c22caa54600c4f85db2a400c7fbea5497f943aa1
    33Add authenticator for accepting XMPP client connections.
    44
    5 The new authenticator XMPPClientListenAuthenticator is to be used together
    6 with an `XmlStream` created for an incoming XMPP stream. It uses the
    7 new initializers for SASL (PLAIN only), resource binding and session
    8 establishement.
     5The new authenticator XMPPClientListenAuthenticator is to be used
     6together with an `XmlStream` created for an incoming XMPP stream. It
     7uses the new initializers for SASL (PLAIN only), resource binding and
     8session establishement.
    99
    1010This authenticator needs at least one Twisted Cred portal to hold the
     
    5656     def __init__(self, jid, password):
    5757         xmlstream.ConnectAuthenticator.__init__(self, jid.host)
    58 @@ -186,3 +199,246 @@
     58@@ -186,3 +199,284 @@
    5959     c = XMPPClientConnector(reactor, domain, factory)
    6060     c.connect()
     
    8080+    """
    8181+    Stream initializer for SASL authentication, receiving side.
     82+
     83+    This authenticator uses L{Twisted Cred<twisted.cred>}, the pluggable
     84+    authentication system. As such it takes a
     85+    L{Portal<twisted.cred.portal.Portal>} to select authentication mechanisms,
     86+    creates a credential object for the selected authentication mechanism and
     87+    passes it to the portal to login and acquire an avatar.
     88+
     89+    The avatar will be set on the C{avatar} attribute of the
     90+    L{xmlstream.XmlStream}.
     91+
     92+    Currently, only the C{PLAIN} SASL mechanism is supported.
    8293+    """
    8394+
    8495+    required = True
     96+    _mechanisms = None
     97+    __credentialsMap = {
     98+        credentials.IAnonymous: 'ANONYMOUS',
     99+        credentials.IUsernamePassword: 'PLAIN',
     100+        }
    85101+
    86102+    def __init__(self, name, xs, portal):
     
    92108+    def getFeatures(self):
    93109+        feature = domish.Element((sasl.NS_XMPP_SASL, 'mechanisms'))
    94 +        feature.addElement('mechanism', content='PLAIN')
     110+
     111+        # Advertise supported SASL mechanisms that have corresponding
     112+        # checkers in the Portal.
     113+        self._mechanisms = set()
     114+        for interface in self.portal.listCredentialsInterfaces():
     115+            try:
     116+                mechanism = self.__credentialsMap[interface]
     117+            except KeyError:
     118+                pass
     119+            else:
     120+                self._mechanisms.add(mechanism)
     121+                feature.addElement('mechanism', content=mechanism)
     122+
    95123+        return [feature]
    96124+
     
    142170+
    143171+
    144 +    def _credentialsFromPlain(self, auth):
     172+    def _credentialsFrom_PLAIN(self, auth):
    145173+        """
    146174+        Create credentials from the initial response for PLAIN.
     
    157185+
    158186+
     187+    def _credentialsFrom_ANONYMOUS(self, auth):
     188+        """
     189+        Create credentials from the initial response for ANONYMOUS.
     190+        """
     191+        return credentials.Anonymous()
     192+
     193+
    159194+    def _doAuth(self, auth):
    160195+        """
    161196+        Start authentication.
    162197+        """
    163 +        if auth.getAttribute('mechanism') != 'PLAIN':
     198+        mechanism = auth.getAttribute('mechanism')
     199+
     200+        if mechanism not in self._mechanisms:
    164201+            raise InvalidMechanism()
    165202+
    166 +        creds = self._credentialsFromPlain(auth)
     203+        creds = getattr(self, '_credentialsFrom_' + mechanism)(auth)
    167204+
    168205+        def cb((iface, avatar, logout)):
     
    180217+    """
    181218+    Stream initializer for resource binding, receiving side.
     219+
     220+    Upon a request for resource binding, this will call C{bindResource} on
     221+    the stream's avatar.
    182222+    """
    183223+
     
    282322+            return [BindReceivingInitializer('bind', self.xmlstream),
    283323+                    SessionReceivingInitializer('session', self.xmlstream)]
    284 +        else:
    285 +            return []
    286324+
    287325+
     
    306344--- a/wokkel/generic.py
    307345+++ b/wokkel/generic.py
    308 @@ -465,6 +465,7 @@
     346@@ -467,6 +467,7 @@
    309347 
    310348     def __init__(self):
     
    314352 
    315353     def _onElementFallback(self, element):
    316 @@ -556,11 +557,12 @@
     354@@ -558,11 +559,12 @@
    317355 
    318356         self.xmlstream.send(features)
     
    331369--- a/wokkel/iwokkel.py
    332370+++ b/wokkel/iwokkel.py
    333 @@ -985,6 +985,45 @@
     371@@ -985,6 +985,55 @@
    334372 
    335373 
    336374 
    337375+class IUserSession(Interface):
     376+    """
     377+    Interface for a XMPP user client session avatar.
     378+    """
     379+
     380+    entity = Attribute(
     381+        """
     382+        The JID for this session.
     383+        """)
     384+
     385+
    338386+    def loggedIn(realm, mind):
    339387+        """
     
    413461 class XMPPClientTest(unittest.TestCase):
    414462     """
    415 @@ -155,3 +168,505 @@
     463@@ -164,3 +177,505 @@
    416464         self.assertEqual(factory.deferred, d2)
    417465 
  • listening-authenticator-stream-features.patch

    r66 r72  
    11# HG changeset patch
    2 # Parent 9393ad83138bbe6ab1c5249a6a115b8e56144622
     2# Parent 840b96390047670c5209195300f902689c18b12f
    33Add FeatureListeningAuthenticator.
    44
     
    99--- a/wokkel/generic.py
    1010+++ b/wokkel/generic.py
    11 @@ -10,13 +10,13 @@
     11@@ -10,7 +10,7 @@
    1212 from zope.interface import implements
    1313 
     
    1515-from twisted.python import reflect
    1616+from twisted.python import log, reflect
     17 from twisted.python.deprecate import deprecated
     18 from twisted.python.versions import Version
    1719 from twisted.words.protocols.jabber import error, jid, xmlstream
    18  from twisted.words.protocols.jabber.xmlstream import toResponse
     20@@ -18,7 +18,7 @@
    1921 from twisted.words.xish import domish, utility
    2022 from twisted.words.xish.xmlstream import BootstrapMixin
     
    2527 
    2628 IQ_GET = '/iq[@type="get"]'
    27 @@ -25,6 +25,8 @@
     29@@ -27,6 +27,8 @@
    2830 NS_VERSION = 'jabber:iq:version'
    2931 VERSION = IQ_GET + '/query[@xmlns="' + NS_VERSION + '"]'
     
    3436     """
    3537     Parse serialized XML into a DOM structure.
    36 @@ -327,3 +329,287 @@
    37  
    38      def clientConnectionFailed(self, connector, reason):
    39          self.deferred.errback(reason)
    40 +
    41 +
    42 +
     38@@ -332,6 +334,290 @@
     39 
     40 
     41 
    4342+class TestableXmlStream(xmlstream.XmlStream):
    4443+    """
     
    322321+        self._initializers = self.getInitializers()
    323322+        self._initializeStream()
     323+
     324+
     325+
     326 @deprecated(Version("Wokkel", 0, 8, 0), "unicode.encode('idna')")
     327 def prepareIDNName(name):
     328     """
    324329diff --git a/wokkel/iwokkel.py b/wokkel/iwokkel.py
    325330--- a/wokkel/iwokkel.py
     
    388393--- a/wokkel/test/test_generic.py
    389394+++ b/wokkel/test/test_generic.py
    390 @@ -5,11 +5,16 @@
    391  Tests for L{wokkel.generic}.
    392  """
     395@@ -7,14 +7,19 @@
     396 
     397 import re
    393398 
    394399+from zope.interface import verify
    395400+
    396401+from twisted.internet import defer
     402 from twisted.python import deprecate
     403 from twisted.python.versions import Version
    397404+from twisted.test import proto_helpers
    398405 from twisted.trial import unittest
     406 from twisted.trial.util import suppress as SUPPRESS
    399407 from twisted.words.xish import domish
    400408 from twisted.words.protocols.jabber.jid import JID
     
    406414 
    407415 NS_VERSION = 'jabber:iq:version'
    408 @@ -268,3 +273,331 @@
    409          The default is no timeout.
    410          """
    411          self.assertIdentical(None, self.request.timeout)
    412 +
    413 +
    414 +
     416@@ -276,6 +281,334 @@
     417 
     418 
     419 
    415420+class BaseReceivingInitializerTest(unittest.TestCase):
    416421+    """
     
    738743+
    739744+        self.xmlstream.assertStreamError(self, condition='invalid-namespace')
     745+
     746+
     747+
     748 class PrepareIDNNameTests(unittest.TestCase):
     749     """
     750     Tests for L{wokkel.generic.prepareIDNName}.
  • roster_server.patch

    r68 r72  
    11Add server side support for the roster protocol.
    22
    3  * Implements roster get by calling `getRoster` and using the returned `list`
    4    of `RosterItem`s to send back the roster.
     3 * Implements roster get by calling `getRoster` and using the returned
     4   `Roster` to send back the roster.
    55
    66TODO:
    7  * Add more docstrings.
    8  * Use `dict` instead of `list` as return value of `getRoster`.
    97 * Add support for roster sets?
    108
     
    1210--- a/wokkel/test/test_xmppim.py
    1311+++ b/wokkel/test/test_xmppim.py
    14 @@ -1330,3 +1330,81 @@
    15          d = self.handleRequest(xml)
    16          d.addCallback(cb)
    17          return d
    18 +
    19 +
    20 +
     12@@ -1339,6 +1339,85 @@
     13 
     14 
     15 
    2116+class RosterServerProtocolTest(unittest.TestCase, TestableRequestHandlerMixin):
    2217+    """
     
    4540+            item = xmppim.RosterItem(JID('other@example.org'), True, False,
    4641+                                 'The User')
    47 +            return defer.succeed([item])
     42+            roster = xmppim.Roster({item.entity: item})
     43+            return defer.succeed(roster)
    4844+
    4945+        def cb(element):
     
    9490+        self.assertFailure(d, NotImplementedError)
    9591+        return d
     92+
     93+
     94+
     95 class MessageTest(unittest.TestCase):
     96     """
     97     Tests for L{xmppim.Message}.
    9698diff --git a/wokkel/xmppim.py b/wokkel/xmppim.py
    9799--- a/wokkel/xmppim.py
    98100+++ b/wokkel/xmppim.py
    99 @@ -25,6 +25,7 @@
     101@@ -26,6 +26,7 @@
    100102 NS_ROSTER = 'jabber:iq:roster'
    101103 
     
    105107 
    106108 
    107 @@ -1063,3 +1064,40 @@
    108          """
    109          Called when a message stanza was received.
    110          """
    111 +
    112 +
    113 +
     109@@ -739,9 +740,9 @@
     110 
     111 
     112     @classmethod
     113-    def fromElement(Class, element):
     114+    def fromElement(cls, element):
     115         entity = JID(element['jid'])
     116-        item = Class(entity)
     117+        item = cls(entity)
     118         subscription = element.getAttribute('subscription')
     119         if subscription == 'remove':
     120             item.remove = True
     121@@ -780,21 +781,26 @@
     122     version = None
     123     rosterSet = False
     124 
     125+
     126     def parseRequest(self, element):
     127-        self.version = element.getAttribute('ver')
     128-
     129-        for child in element.elements(NS_ROSTER, 'item'):
     130-            self.item = RosterItem.fromElement(child)
     131+        roster = Roster.fromElement(element)
     132+        self.version = roster.version
     133+        for item in roster.itervalues():
     134+            self.item = item
     135             break
     136+        return roster
     137 
     138 
     139     def toElement(self):
     140         element = Request.toElement(self)
     141-        query = element.addElement((NS_ROSTER, 'query'))
     142-        if self.version is not None:
     143-            query['ver'] = self.version
     144+
     145+        roster = Roster()
     146+        roster.version = self.version
     147         if self.item:
     148-            query.addChild(self.item.toElement(rosterSet=self.rosterSet))
     149+            roster[self.item.entity] = self.item
     150+
     151+        element.addChild(roster.toElement(rosterSet=self.rosterSet))
     152+
     153         return element
     154 
     155 
     156@@ -812,7 +818,7 @@
     157 
     158 class Roster(dict):
     159     """
     160-    In-memory roster container.
     161+    Roster container.
     162 
     163     This provides a roster as a mapping from L{JID} to L{RosterItem}. If
     164     roster versioning is used, the C{version} attribute holds the version
     165@@ -825,6 +831,28 @@
     166     version = None
     167 
     168 
     169+    @classmethod
     170+    def fromElement(cls, element):
     171+        roster = cls()
     172+        roster.version = element.getAttribute('ver')
     173+        for element in element.elements(NS_ROSTER, 'item'):
     174+            item = RosterItem.fromElement(element)
     175+            roster[item.entity] = item
     176+        return roster
     177+
     178+
     179+    def toElement(self, rosterSet=False):
     180+        element = domish.Element((NS_ROSTER, 'query'))
     181+
     182+        if self.version:
     183+            element['ver'] = self.version
     184+
     185+        for item in self.itervalues():
     186+            element.addChild(item.toElement(rosterSet))
     187+
     188+        return element
     189+
     190+
     191 
     192 class RosterClientProtocol(XMPPHandler, IQHandlerMixin):
     193     """
     194@@ -898,12 +926,7 @@
     195 
     196         def processRoster(result):
     197             if result.query is not None:
     198-                roster = Roster()
     199-                roster.version = result.query.getAttribute('ver')
     200-                for element in result.query.elements(NS_ROSTER, 'item'):
     201-                    item = RosterItem.fromElement(element)
     202-                    roster[item.entity] = item
     203-                return roster
     204+                return Roster.fromElement(result.query)
     205             else:
     206                 return None
     207 
     208@@ -1002,6 +1025,43 @@
     209 
     210 
     211 
    114212+class RosterServerProtocol(XMPPHandler, IQHandlerMixin):
    115213+    """
     
    127225+
    128226+
    129 +    def _toRosterReply(self, roster, request):
    130 +        response = domish.Element((NS_ROSTER, 'query'))
    131 +
    132 +        for item in roster:
    133 +            response.addChild(item.toElement())
    134 +
    135 +        return response
    136 +
    137 +
    138227+    def _onRosterGet(self, iq):
    139 +        request = Stanza.fromElement(iq)
     228+        request = RosterRequest.fromElement(iq)
     229+
     230+        def toResponse(roster):
     231+            return roster.toElement()
    140232+
    141233+        d = self.getRoster(request)
    142 +        d.addCallback(self._toRosterReply, request)
     234+        d.addCallback(toResponse)
    143235+        return d
    144236+
    145237+
    146238+    def getRoster(self, request):
     239+        """
     240+        Called when the roster is requested.
     241+
     242+        @returns: Deferred that fires with a L{Roster}.
     243+        @rtype: L{defer.Deferred}
     244+        """
    147245+        raise NotImplementedError()
     246+
     247+
     248+
     249 class Message(Stanza):
     250     """
     251     A message stanza.
  • series

    r68 r72  
     1deprecate_prepareIDNName.patch
     2async-observer.patch
     3message-stanza.patch
    14server-stream-manager.patch
    2 roster_server.patch #+c2s
     5stream-manager-logging.patch
    36
    47listening-authenticator-stream-features.patch #+c2s
    58client_listen_authenticator.patch #+c2s
     9c2s_server_factory.patch #+c2s
    610
    7 c2s_server_factory.patch #+c2s
     11roster_server.patch #+c2s
    812session_manager.patch #+c2s
     13
     14
    915c2s_stanza_handlers.patch #+c2s
     16c2s_example.patch
    1017
    1118version.patch
  • server-stream-manager.patch

    r68 r72  
    11# HG changeset patch
    2 # Parent f98da30301ea95ae9fe688df4128f6fbdf25df7d
     2# Parent 3f3fe954b1975c2d9115e0fa8177ae7b28a708a8
     3Generalize StreamManager and add ServerStreamManager.
     4
     5This generalizes `StreamManager` to `BaseStreamManager` to take the
     6common functionality and reuse it for `ServerStreamManager`. Where
     7`StreamManager` is used for initiating connections, with a factory as
     8a parameter, `ServerStreamManager` works for receiving connections. It
     9must be created in a protocol factory when a connection is established,
     10and then its `makeConnection` should be called to hook it up to the
     11`XmlStream` instance.
    312
    413diff --git a/wokkel/client.py b/wokkel/client.py
  • session_manager.patch

    r70 r72  
    11# HG changeset patch
    2 # Parent bc450d2e7ed710c5605545e39bb6a054c368571f
     2# Parent fdef0cff7a57368fa21984593ef05e616039e2e2
    33
    4 diff --git a/wokkel/client.py b/wokkel/client.py
    5 --- a/wokkel/client.py
    6 +++ b/wokkel/client.py
    7 @@ -12,19 +12,23 @@
    8  
    9  import base64
    10  
    11 +from zope.interface import implements
    12 +
    13  from twisted.application import service
    14 -from twisted.cred import credentials, error as ecred
    15 +from twisted.cred import credentials, error as ecred, portal
    16  from twisted.internet import defer, reactor
    17 -from twisted.python import log
     4diff --git a/wokkel/ewokkel.py b/wokkel/ewokkel.py
     5new file mode 100644
     6--- /dev/null
     7+++ b/wokkel/ewokkel.py
     8@@ -0,0 +1,32 @@
     9+# Copyright (c) Ralph Meijer.
     10+# See LICENSE for details.
     11+
     12+"""
     13+Exceptions for Wokkel.
     14+"""
     15+
     16+
     17+class WokkelError(Exception):
     18+    """
     19+    Base exception for Wokkel.
     20+    """
     21+
     22+class NoSuchContact(WokkelError):
     23+    """
     24+    Raised when the given contact is not present in the user's roster.
     25+    """
     26+
     27+class NotSubscribed(WokkelError):
     28+    """
     29+    Raised when the contact does not have a presence subscription to the user.
     30+    """
     31+
     32+class NoSuchResource(WokkelError):
     33+    """
     34+    Raised when the given resource is currently not connected.
     35+    """
     36+
     37+class NoSuchUser(WokkelError):
     38+    """
     39+    Raised when there is no user with the given name or JID.
     40+    """
     41diff --git a/wokkel/generic.py b/wokkel/generic.py
     42--- a/wokkel/generic.py
     43+++ b/wokkel/generic.py
     44@@ -7,6 +7,8 @@
     45 Generic XMPP protocol helpers.
     46 """
     47 
     48+import copy
     49+
     50 from zope.interface import implements
     51 
     52 from twisted.internet import defer, protocol
     53@@ -66,6 +68,24 @@
     54     return rootElement
     55 
     56 
     57+def cloneElement(element):
     58+    """
     59+    Make a deep copy of a serialized element.
     60+
     61+    The returned element is an orphaned deep copy of the given original.
     62+
     63+    @note: Since the reference to the original parent, if any, is gone,
     64+    inherited attributes like C{xml:lang} are not preserved.
     65+
     66+    @type element: L{domish.Element}.
     67+    """
     68+    parent = element.parent
     69+    element.parent = None
     70+    clone = copy.deepcopy(element)
     71+    element.parent = parent
     72+    return clone
     73+
     74+
     75 
     76 class FallbackHandler(XMPPHandler):
     77     """
     78@@ -168,10 +188,23 @@
     79     """
     80     Abstract representation of a stanza.
     81 
     82+    @ivar recipient: The receiving entity.
     83+    @type recipient: L{jid.JID}
     84+
     85     @ivar sender: The sending entity.
     86     @type sender: L{jid.JID}
     87-    @ivar recipient: The receiving entity.
     88-    @type recipient: L{jid.JID}
     89+
     90+    @ivar stanzaKind: One of C{'message'}, C{'presence'}, C{'iq'}.
     91+    @type stanzaKind: L{unicode}.
     92+
     93+    @ivar stanzaID: The optional stanza identifier.
     94+    @type stanzaID: L{unicode}.
     95+
     96+    @ivar stanzaType: The optional stanza type.
     97+    @type stanzaType: L{unicode}.
     98+
     99+    @ivar element: The serialized XML of this stanza.
     100+    @type element: L{domish.Element}.
     101     """
     102 
     103     recipient = None
     104@@ -179,6 +212,8 @@
     105     stanzaKind = None
     106     stanzaID = None
     107     stanzaType = None
     108+    element = None
     109+
     110 
     111     def __init__(self, recipient=None, sender=None):
     112         self.recipient = recipient
     113@@ -217,6 +252,7 @@
     114             self.sender = jid.internJID(element['from'])
     115         if element.hasAttribute('to'):
     116             self.recipient = jid.internJID(element['to'])
     117+        self.stanzaKind = element.name
     118         self.stanzaType = element.getAttribute('type')
     119         self.stanzaID = element.getAttribute('id')
     120 
     121@@ -242,6 +278,7 @@
     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:
     129diff --git a/wokkel/iwokkel.py b/wokkel/iwokkel.py
     130--- a/wokkel/iwokkel.py
     131+++ b/wokkel/iwokkel.py
     132@@ -996,6 +996,14 @@
     133         """)
     134 
     135 
     136+    interested = Attribute(
     137+        """
     138+        This session represents a I{interested resource}, i.e.  the user's
     139+        roster has been requested and roster pushes will be sent out to
     140+        this session.
     141+        """)
     142+
     143+
     144     def loggedIn(realm, mind):
     145         """
     146         Called by the realm when login occurs.
     147diff --git a/wokkel/test/test_generic.py b/wokkel/test/test_generic.py
     148--- a/wokkel/test/test_generic.py
     149+++ b/wokkel/test/test_generic.py
     150@@ -24,6 +24,27 @@
     151 
     152 NS_VERSION = 'jabber:iq:version'
     153 
     154+class CloneElementTest(unittest.TestCase):
     155+    """
     156+    Tests for L{xmppim.clonePresence}.
     157+    """
     158+
     159+    def test_rootElement(self):
     160+        """
     161+        The copied presence stanza is not identical, but renders identically.
     162+        """
     163+        parent = object()
     164+        originalElement = domish.Element((None, 'presence'))
     165+        originalElement.parent = parent
     166+        copyElement = generic.cloneElement(originalElement)
     167+
     168+        self.assertNotIdentical(copyElement, originalElement)
     169+        self.assertEqual(copyElement.toXml(), originalElement.toXml())
     170+        self.assertIdentical(None, copyElement.parent)
     171+        self.assertIdentical(parent, originalElement.parent)
     172+
     173+
     174+
     175 class VersionHandlerTest(unittest.TestCase):
     176     """
     177     Tests for L{wokkel.generic.VersionHandler}.
     178@@ -110,10 +131,21 @@
     179         <message type='chat' from='other@example.org' to='user@example.org'/>
     180         """
     181 
     182-        stanza = generic.Stanza.fromElement(generic.parseXml(xml))
     183+        element = generic.parseXml(xml)
     184+        stanza = generic.Stanza.fromElement(element)
     185         self.assertEqual('chat', stanza.stanzaType)
     186         self.assertEqual(JID('other@example.org'), stanza.sender)
     187         self.assertEqual(JID('user@example.org'), stanza.recipient)
     188+        self.assertIdentical(element, stanza.element)
     189+
     190+
     191+    def test_fromElementStanzaKind(self):
     192+        """
     193+        The stanza kind is also recorded in the stanza.
     194+        """
     195+        xml = """<presence/>"""
     196+        stanza = generic.Stanza.fromElement(generic.parseXml(xml))
     197+        self.assertEqual(u'presence', stanza.stanzaKind)
     198 
     199 
     200     def test_fromElementChildParser(self):
     201diff --git a/wokkel/test/test_xmppim.py b/wokkel/test/test_xmppim.py
     202--- a/wokkel/test/test_xmppim.py
     203+++ b/wokkel/test/test_xmppim.py
     204@@ -5,6 +5,10 @@
     205 Tests for L{wokkel.xmppim}.
     206 """
     207 
     208+from zope.interface import verify
     209+
     210+from twisted.cred import checkers, error as ecred
     211+from twisted.cred.portal import IRealm
     212 from twisted.internet import defer
     213 from twisted.trial import unittest
     214 from twisted.words.protocols.jabber import error
     215@@ -12,9 +16,10 @@
     216 from twisted.words.protocols.jabber.xmlstream import toResponse
     217 from twisted.words.xish import domish, utility
     218 
     219-from wokkel import xmppim
     220+from wokkel import ewokkel, component, xmppim
     221 from wokkel.generic import ErrorStanza, parseXml
     222 from wokkel.test.helpers import TestableRequestHandlerMixin, XmlStreamStub
     223+from wokkel.subprotocols import IQHandlerMixin
     224 
     225 NS_XML = 'http://www.w3.org/XML/1998/namespace'
     226 NS_ROSTER = 'jabber:iq:roster'
     227@@ -99,6 +104,7 @@
     228         self.assertEquals(50, presence.priority)
     229 
     230 
     231+
     232 class PresenceProtocolTest(unittest.TestCase):
     233     """
     234     Tests for L{xmppim.PresenceProtocol}
     235@@ -1418,6 +1424,112 @@
     236 
     237 
     238 
     239+class InMemoryRosterTest(unittest.TestCase):
     240+    """
     241+    Tests for L{xmppim.InMemoryRoster}.
     242+    """
     243+
     244+    def setUp(self):
     245+        contacts = [
     246+            xmppim.RosterItem(JID('contact1@example.org'),
     247+                              subscriptionFrom=True,
     248+                              subscriptionTo=False),
     249+            xmppim.RosterItem(JID('contact2@example.org'),
     250+                              subscriptionFrom=False,
     251+                              subscriptionTo=True),
     252+            ]
     253+        self.roster = xmppim.InMemoryRoster(contacts)
     254+
     255+
     256+    def test_getSubscribers(self):
     257+        def gotSubscribers(subscribers):
     258+            subscribers = list(subscribers)
     259+            self.assertIn(JID('contact1@example.org'), subscribers)
     260+            self.assertNotIn(JID('contact2@example.org'), subscribers)
     261+
     262+
     263+        d = self.roster.getSubscribers()
     264+        d.addCallback(gotSubscribers)
     265+        return d
     266+
     267+
     268+    def test_getSubscriptions(self):
     269+        def gotSubscriptions(subscriptions):
     270+            subscriptions = list(subscriptions)
     271+            self.assertNotIn(JID('contact1@example.org'), subscriptions)
     272+            self.assertIn(JID('contact2@example.org'), subscriptions)
     273+
     274+        d = self.roster.getSubscriptions()
     275+        d.addCallback(gotSubscriptions)
     276+        return d
     277+
     278+
     279+class UserRosterProtocolTest(unittest.TestCase):
     280+    """
     281+    Tests for L{xmppim.UserRosterProtocol}.
     282+    """
     283+
     284+    def setUp(self):
     285+        self.stub = XmlStreamStub()
     286+        self.service = xmppim.UserRosterProtocol()
     287+        self.service.makeConnection(self.stub.xmlstream)
     288+
     289+        entity = JID(u'user@example.org')
     290+        user = xmppim.User(entity)
     291+
     292+        contact = xmppim.RosterItem(JID(u'contact@example.org'))
     293+        user.roster = xmppim.InMemoryRoster([contact])
     294+
     295+        self.session = xmppim.UserSession(user)
     296+        self.stub.xmlstream.avatar = self.session
     297+
     298+
     299+    def test_getRoster(self):
     300+        """
     301+        The returned roster is gotten from the session user.
     302+        """
     303+        def gotRoster(result):
     304+            self.assertIn(JID(u'contact@example.org'), result)
     305+
     306+        request = xmppim.RosterRequest()
     307+        d = self.service.getRoster(request)
     308+        d.addCallback(gotRoster)
     309+        return d
     310+
     311+
     312+    def test_getRosterInterested(self):
     313+        """
     314+        Requesting the roster marks the session as interested in roster pushes.
     315+        """
     316+        def gotRoster(result):
     317+            self.assertTrue(self.session.interested)
     318+
     319+        request = xmppim.RosterRequest()
     320+        d = self.service.getRoster(request)
     321+        d.addCallback(gotRoster)
     322+        return d
     323+
     324+
     325+    def test_handleRequestLocal(self):
     326+        """
     327+        Handle requests without recipient (= local server).
     328+        """
     329+        called = []
     330+        request = xmppim.RosterRequest(recipient=None)
     331+        self.patch(IQHandlerMixin, 'handleRequest', called.append)
     332+        self.service.handleRequest(request.toElement())
     333+        self.assertTrue(called)
     334+
     335+
     336+    def test_handleRequestOther(self):
     337+        """
     338+        If the request has a non-empty recipient, ignore this request.
     339+        """
     340+        request = xmppim.RosterRequest(recipient=JID('other.example.org'))
     341+        self.assertFalse(self.service.handleRequest(request.toElement()))
     342+
     343+
     344+
     345 class MessageTest(unittest.TestCase):
     346     """
     347     Tests for L{xmppim.Message}.
     348@@ -1598,3 +1710,297 @@
     349                          "was deprecated in Wokkel 0.8.0; "
     350                          "please use MessageProtocol.messageReceived instead.",
     351                          warnings[0]['message'])
     352+
     353+
     354+
     355+class UserSessionTest(unittest.TestCase):
     356+    """
     357+    Tests for L{xmppim.UserSession}.
     358+    """
     359+
     360+    def setUp(self):
     361+        self.session = xmppim.UserSession(None)
     362+
     363+
     364+    def test_interface(self):
     365+        """
     366+        UserSession implements IUserSession.
     367+        """
     368+        verify.verifyObject(xmppim.IUserSession, self.session)
     369+
     370+
     371+class UserTest(unittest.TestCase):
     372+    """
     373+    Tests for L{xmppim.User}.
     374+    """
     375+
     376+    def setUp(self):
     377+        self.user = xmppim.User(JID('user@example.org'),
     378+                                xmppim.InMemoryRoster([]))
     379+        self.session = xmppim.UserSession(self.user)
     380+        self.session.bindResource('Home')
     381+
     382+
     383+    @defer.inlineCallbacks
     384+    def test_getPresences(self):
     385+        """
     386+        A contact with a subscription to the user gets its presence.
     387+        """
     388+        contact = xmppim.RosterItem(JID('contact@example.org'),
     389+                                     subscriptionFrom=True)
     390+        self.user.roster.roster[contact.entity] = contact
     391+
     392+        presence = xmppim.AvailabilityPresence()
     393+        self.session.presence = presence
     394+
     395+        presences = yield self.user.getPresences(JID('contact@example.org'))
     396+        self.assertEqual([presence], list(presences))
     397+
     398+        defer.returnValue(None)
     399+
     400+
     401+    def test_getPresencesNoSubscription(self):
     402+        """
     403+        A contact without a subscription raises NotSubscribed.
     404+        """
     405+        contact = xmppim.RosterItem(JID('contact@example.org'),
     406+                                     subscriptionFrom=False)
     407+        self.user.roster.roster[contact.entity] = contact
     408+
     409+        presence = xmppim.AvailabilityPresence()
     410+        self.session.presence = presence
     411+
     412+        d = self.user.getPresences(JID('contact@example.org'))
     413+        self.assertFailure(d, ewokkel.NotSubscribed)
     414+        return d
     415+
     416+
     417+    def test_getPresencesUnknown(self):
     418+        """
     419+        A contact with a subscription to the user gets its presence.
     420+        """
     421+        presence = xmppim.AvailabilityPresence()
     422+        self.session.presence = presence
     423+        d = self.user.getPresences(JID('unknown@example.org'))
     424+        self.assertFailure(d, ewokkel.NoSuchContact)
     425+        return d
     426+
     427+
     428+class AnonymousRealmTest(unittest.TestCase):
     429+    """
     430+    Tests for L{xmppim.AnonymousRealm}.
     431+    """
     432+
     433+    def setUp(self):
     434+        self.realm = xmppim.AnonymousRealm('example.org')
     435+
     436+
     437+    def test_interface(self):
     438+        """
     439+        AnonymousRealm implements IRealm.
     440+        """
     441+        verify.verifyObject(IRealm, self.realm)
     442+
     443+
     444+    def test_requestAvatarAnonymous(self):
     445+        """
     446+        An anonymous avatar ID yields a generated JID.
     447+        """
     448+        def gotAvatar(result):
     449+            def gotUser(user):
     450+                self.assertIdentical(user, avatar.user)
     451+
     452+            iface, avatar, logout = result
     453+            self.assertNotIdentical(None, avatar.user)
     454+            self.assertNotIdentical(None, avatar.user.entity)
     455+            self.assertNotIdentical(None, avatar.user.entity.host)
     456+            self.assertEqual(u'example.org', avatar.user.entity.host)
     457+
     458+            d = self.realm.lookupUser(avatar.user.entity)
     459+            d.addCallback(gotUser)
     460+            return d
     461+
     462+        avatarID = checkers.ANONYMOUS
     463+        d = self.realm.requestAvatar(avatarID, None, xmppim.IUserSession)
     464+        d.addCallback(gotAvatar)
     465+        return d
     466+
     467+
     468+    def test_requestAvatarAnonymousDifferent(self):
     469+        """
     470+        Requesting two anonymous avatar IDs yields different JIDs.
     471+        """
     472+        def gotAvatar1(result):
     473+            iface, avatar1, logout = result
     474+
     475+            def gotAvatar2(result):
     476+                iface, avatar2, logout = result
     477+                self.assertNotIdentical(avatar1, avatar2)
     478+                self.assertNotIdentical(avatar1.user, avatar2.user)
     479+                self.assertNotEqual(avatar1.user.entity,
     480+                                    avatar2.user.entity)
     481+
     482+            d = self.realm.requestAvatar(avatarID, None, xmppim.IUserSession)
     483+            d.addCallback(gotAvatar2)
     484+            return d
     485+
     486+        avatarID = checkers.ANONYMOUS
     487+        d = self.realm.requestAvatar(avatarID, None, xmppim.IUserSession)
     488+        d.addCallback(gotAvatar1)
     489+        return d
     490+
     491+
     492+    def test_logout(self):
     493+        """
     494+        When the logout function is called, the user is removed from the realm.
     495+        """
     496+        def gotAvatar(result):
     497+            iface, avatar, logout = result
     498+
     499+            logout()
     500+
     501+            entity = avatar.user.entity
     502+            d = self.realm.lookupUser(entity)
     503+            self.assertFailure(d, ewokkel.NoSuchUser)
     504+            return d
     505+
     506+        avatarID = checkers.ANONYMOUS
     507+        d = self.realm.requestAvatar(avatarID, None, xmppim.IUserSession)
     508+        d.addCallback(gotAvatar)
     509+        return d
     510+
     511+
     512+
     513+class StaticRealmTest(unittest.TestCase):
     514+    """
     515+    Tests for L{xmppim.StaticRealmTest}.
     516+    """
     517+
     518+    def setUp(self):
     519+        entity = JID(u'\u00e9lise@example.org')
     520+        self.user = xmppim.User(entity)
     521+        users = {entity: self.user}
     522+        self.realm = xmppim.StaticRealm('example.org', users)
     523+
     524+
     525+    def test_interface(self):
     526+        """
     527+        StaticRealm implements IRealm.
     528+        """
     529+        verify.verifyObject(IRealm, self.realm)
     530+
     531+
     532+    def test_requestAvatar(self):
     533+        """
     534+        A UserSession is initialized and returned from requestAvatar.
     535+        """
     536+        def gotAvatar(result):
     537+            iface, avatar, logout = result
     538+            self.assertIdentical(self.user, avatar.user)
     539+
     540+        avatarID = u'\u00e9lise'.encode('utf-8')
     541+        d = self.realm.requestAvatar(avatarID, None, xmppim.IUserSession)
     542+        d.addCallback(gotAvatar)
     543+        return d
     544+
     545+
     546+    def test_requestAvatarUnknown(self):
     547+        """
     548+        A UserSession is initialized and returned from requestAvatar.
     549+        """
     550+        avatarID = u'nobody'.encode('utf-8')
     551+        d = self.realm.requestAvatar(avatarID, None, xmppim.IUserSession)
     552+        self.assertFailure(d, ecred.LoginDenied)
     553+        return d
     554+
     555+
     556+
     557+class TestRouter(component.Router):
     558+    """
     559+    Router that only records incoming traffic for testing.
     560+    """
     561+
     562+    def __init__(self):
     563+        super(TestRouter, self).__init__()
     564+        self.output = []
     565+
     566+
     567+    def route(self, stanza):
     568+        """
     569+        All routed stanzas are recorded.
     570+        """
     571+        self.output.append(stanza)
     572+
     573+
     574+
     575+class SessionManagerTest(unittest.TestCase):
     576+    """
     577+    Tests for L{xmppim.SessionManager}.
     578+    """
     579+
     580+    def setUp(self):
     581+        self.router = TestRouter()
     582+        self.sessionManager = xmppim.SessionManager(self.router,
     583+                                                    u'example.org')
     584+        self.sessionManager.startService()
     585+
     586+        self.input = []
     587+        def onElement(element):
     588+            self.input.append(element)
     589+
     590+        self.sessionManager.xmlstream.addObserver('/*', onElement)
     591+
     592+
     593+    def test_routeOrDeliverLocal(self):
     594+        """
     595+        Stanzas for local domains are reinjected in to the XML stream.
     596+        """
     597+        element = parseXml("""<presence from='test@example.org'
     598+                                        to='other@example.org'/>""")
     599+        self.sessionManager.xmlstream.send(element)
     600+
     601+        self.assertEqual(1, len(self.input))
     602+        self.assertEqual(0, len(self.router.output))
     603+
     604+
     605+    def test_routeOrDeliverRemote(self):
     606+        """
     607+        Stanzas for other domains are sent to the router.
     608+        """
     609+        element = parseXml("""<presence from='test@example.org'
     610+                                        to='other@example.com'/>""")
     611+        self.sessionManager.xmlstream.send(element)
     612+
     613+        self.assertEqual(0, len(self.input))
     614+        self.assertEqual(1, len(self.router.output))
     615+
     616+
     617+    def test_probePresence(self):
     618+        """
     619+        Presence probes are sent to contacts with a subscription.
     620+        """
     621+        def cb(result):
     622+            self.assertEqual(1, len(self.router.output))
     623+            element = self.router.output[-1]
     624+            self.assertEqual(u'presence',
     625+                             element.name)
     626+            self.assertEqual(u'probe',
     627+                             element.getAttribute(u'type'))
     628+            self.assertEqual(u'contact2@example.com',
     629+                             element.getAttribute(u'to'))
     630+
     631+        contacts = [
     632+            xmppim.RosterItem(JID(u'contact1@example.com'),
     633+                              subscriptionFrom=True,
     634+                              subscriptionTo=False),
     635+            xmppim.RosterItem(JID(u'contact2@example.com'),
     636+                              subscriptionFrom=False,
     637+                              subscriptionTo=True),
     638+            ]
     639+        roster = xmppim.InMemoryRoster(contacts)
     640+        entity = JID(u'user@example.org')
     641+        user = xmppim.User(entity, roster)
     642+
     643+        d = self.sessionManager.probePresence(user)
     644+        d.addCallback(cb)
     645+        return d
     646diff --git a/wokkel/xmppim.py b/wokkel/xmppim.py
     647--- a/wokkel/xmppim.py
     648+++ b/wokkel/xmppim.py
     649@@ -12,12 +12,20 @@
     650 
     651 import warnings
     652 
     653+from zope.interface import implementer
     654+
     655+from twisted.cred import error as ecred, portal
     656 from twisted.internet import defer
    18657+from twisted.python import log, randbytes
    19  from twisted.names.srvconnect import SRVConnector
    20  from twisted.words.protocols.jabber import client, error, sasl, xmlstream
    21 -from twisted.words.xish import domish
    22 +from twisted.words.protocols.jabber.jid import JID, internJID
    23 +from twisted.words.xish import domish, utility
    24  
    25  from wokkel import generic
    26  from wokkel.compat import XmlStreamServerFactory
    27  from wokkel.iwokkel import IUserSession
    28  from wokkel.subprotocols import ServerStreamManager
    29  from wokkel.subprotocols import StreamManager
    30 +from wokkel.subprotocols import XMPPHandler
    31  
    32  NS_CLIENT = 'jabber:client'
    33  
    34 @@ -482,3 +486,186 @@
    35          return [
    36              generic.StanzaForwarder()
    37          ]
    38 +
    39 +
    40 +
     658 from twisted.words.protocols.jabber import error
     659 from twisted.words.protocols.jabber.jid import JID
     660 from twisted.words.xish import domish
     661 
     662-from wokkel.generic import ErrorStanza, Stanza, Request
     663+from wokkel.component import InternalComponent
     664+from wokkel.ewokkel import NoSuchContact
     665+from wokkel.ewokkel import NotSubscribed, NoSuchResource, NoSuchUser
     666+from wokkel.iwokkel import IUserSession
     667+from wokkel.generic import ErrorStanza, Stanza, Request, cloneElement
     668 from wokkel.subprotocols import IQHandlerMixin
     669 from wokkel.subprotocols import XMPPHandler
     670 from wokkel.subprotocols import asyncObserver
     671@@ -1062,6 +1070,67 @@
     672 
     673 
     674 
     675+class InMemoryRoster(object):
     676+
     677+    def __init__(self, items):
     678+        self.roster = Roster(((item.entity, item) for item in items))
     679+
     680+
     681+    def getRoster(self, version=None):
     682+        return defer.succeed(self.roster)
     683+
     684+
     685+    def getSubscribers(self):
     686+        subscribers = (entity for entity, item in self.roster.iteritems()
     687+                              if item.subscriptionFrom)
     688+        return defer.succeed(subscribers)
     689+
     690+
     691+    def getSubscriptions(self):
     692+        subscriptions = (entity for entity, item in self.roster.iteritems()
     693+                                if item.subscriptionTo)
     694+        return defer.succeed(subscriptions)
     695+
     696+
     697+    def getContact(self, entity):
     698+        try:
     699+            return defer.succeed(self.roster[entity])
     700+        except KeyError:
     701+            return defer.fail(NoSuchContact())
     702+
     703+
     704+
     705+
     706+
     707+class UserRosterProtocol(RosterServerProtocol):
     708+    """
     709+    Roster protocol handler for client connections.
     710+
     711+    This protocol is meant to be used with
     712+    L{wokkel.client.XMPPC2SServerFactory} to interact with L{UserSession} and
     713+    L{User}.
     714+    """
     715+
     716+    def handleRequest(self, iq):
     717+        """
     718+        Ignore roster requests for non-empty recipients.
     719+        """
     720+        if iq.getAttribute('to'):
     721+            return False
     722+        else:
     723+            return super(UserRosterProtocol, self).handleRequest(iq)
     724+
     725+
     726+    def getRoster(self, request):
     727+        """
     728+        Return the roster of the user associated with this session.
     729+        """
     730+        session = self.xmlstream.avatar
     731+        session.interested = True
     732+        return session.user.roster.getRoster()
     733+
     734+
     735+
     736 class Message(Stanza):
     737     """
     738     A message stanza.
     739@@ -1159,3 +1228,448 @@
     740 
     741             self.onMessage(message.element)
     742             return True
     743+
     744+
     745+
     746+@implementer(IUserSession)
    41747+class UserSession(object):
    42 +
    43 +    implements(IUserSession)
    44 +
    45 +    realm = None
     748+    """
     749+    An XMPP user session.
     750+
     751+    This represents the session for a connected client after authenticating
     752+    as L{user}.
     753+
     754+    @ivar user: The authenticated user for this session.
     755+    @type user: L{User}
     756+
     757+    @ivar mind: The protocol instance for this session.
     758+    @type mind: L{xmlstream.Xmlstream}
     759+
     760+    @ivar entity: The full JID of the entity after a resource has been
     761+       bound.
     762+    @type entity: L{JID}
     763+
     764+    @ivar connected: Flag that is C{True} while a resource is bound.
     765+    @type connected: L{boolean}
     766+
     767+    @ivar interested: Flag to record that the roster has been requested. When
     768+        L{True}, this session will receive roster pushes.
     769+    @type interested: L{boolean}
     770+
     771+    @ivar presence: Last broadcast presence from the client for this session.
     772+    @type presence: L{AvailabilityPresence}
     773+    """
     774+
     775+    user = None
    46776+    mind = None
    47 +
     777+    entity = None
    48778+    connected = False
    49779+    interested = False
    50780+    presence = None
    51781+
    52 +    clientStream = None
    53 +
    54 +    def __init__(self, entity):
    55 +        self.entity = entity
     782+    def __init__(self, user):
     783+        self.user = user
    56784+
    57785+
    58786+    def loggedIn(self, realm, mind):
     787+        self.mind = mind
    59788+        self.realm = realm
    60 +        self.mind = mind
    61789+
    62790+
     
    67795+            return entity
    68796+
    69 +        d = self.realm.bindResource(self, resource)
     797+        d = self.user.bindResource(self, resource)
    70798+        d.addCallback(cb)
    71799+        return d
     
    74802+    def logout(self):
    75803+        self.connected = False
    76 +        self.realm.unbindResource(self)
     804+
     805+        if self.entity:
     806+            self.user.unbindResource(self.entity.resource)
    77807+
    78808+
    79809+    def send(self, element):
    80 +        self.realm.onElement(element, self)
    81 +
    82 +
    83 +    def receive(self, element):
     810+        """
     811+        Called when the client sends a stanza.
     812+        """
     813+        self.realm.server.routeOrDeliver(element)
     814+
     815+
     816+    def receive(self, element, recipient=None):
     817+        """
     818+        Deliver a stanza to the client.
     819+        """
    84820+        self.mind.send(element)
    85821+
    86822+
    87 +
    88 +class SessionManager(XMPPHandler):
     823+    def probePresence(self):
     824+        self.user.probePresence(self)
     825+
     826+
     827+    def broadcastPresence(self, presence, available):
     828+        if available:
     829+            self.presence = presence
     830+        else:
     831+            self.presence = None
     832+            # TODO: unset probeSent on user?
     833+            # TODO: save last unavailable presence?
     834+
     835+        self.user.broadcastPresence(presence)
     836+
     837+
     838+
     839+class User(object):
     840+    """
     841+    An XMPP user account.
     842+
     843+    @ivar entity: The JID of the user.
     844+    @type entity: L{JID}
     845+
     846+    @ivar roster: The user's roster.
     847+
     848+    @ivar sessions: The currently connected sessions for this user, indexed by
     849+        resource.
     850+    @type sessions: L{dict}
     851+
     852+    @ivar probeSent: Flag that is C{True} if presence probes have been sent
     853+        out for this user. This is only done on the initial presence broadcast
     854+        of the first available resource. Subsequent resources will get the
     855+        presences stored in L{contactPresences}.
     856+    @type probeSent: L{boolean}
     857+
     858+    @ivar contactPresences: Cached presences of contacts as a mapping of L{JID}
     859+        to L{AvailabilityPresence}.
     860+    @type contactPresences: L{dict}
     861+
     862+    @ivar realm: The realm that provided this user object.
     863+    @type realm: L{IRealm} provider
     864+    """
     865+
     866+    realm = None
     867+
     868+    def __init__(self, entity, roster=None):
     869+        self.entity = entity
     870+        self.roster = roster
     871+        self.sessions = {}
     872+        self.probeSent = False
     873+        self.contactPresences = {}
     874+
     875+
     876+    def bindResource(self, session, resource):
     877+        if resource is None:
     878+            resource = randbytes.secureRandom(8).encode('hex')
     879+        elif resource in self.sessions:
     880+            resource = resource + ' ' + randbytes.secureRandom(8).encode('hex')
     881+
     882+        entity = JID(tuple=(self.entity.user, self.entity.host, resource))
     883+        self.sessions[resource] = session
     884+
     885+        return defer.succeed(entity)
     886+
     887+
     888+    def unbindResource(self, resource):
     889+        del self.sessions[resource]
     890+        return defer.succeed(None)
     891+
     892+
     893+    def deliverIQ(self, stanza):
     894+        try:
     895+            session = self.sessions[stanza.recipient.resource]
     896+        except KeyError:
     897+            raise NoSuchResource()
     898+
     899+        session.receive(stanza.element)
     900+
     901+
     902+    def deliverMessage(self, stanza):
     903+        if stanza.recipient.resource:
     904+            try:
     905+                session = self.sessions[stanza.recipient.resource]
     906+            except KeyError:
     907+                if stanza.stanzaType in ('normal', 'chat', 'headline'):
     908+                    self.deliverMessageAnyResource(stanza)
     909+                else:
     910+                    raise NoSuchResource()
     911+            else:
     912+                session.receive(stanza.element)
     913+        else:
     914+            if stanza.stanzaType == 'groupchat':
     915+                raise NotImplementedError("Groupchat message to the bare JID")
     916+            else:
     917+                self.deliverMessageAnyResource(stanza)
     918+
     919+
     920+    def deliverMessageAnyResource(self, stanza):
     921+        if stanza.stanzaType == 'headline':
     922+            recipients = set()
     923+            for resource, session in self.sessions.iteritems():
     924+                if session.presence.priority >= 0:
     925+                    recipients.add(resource)
     926+        elif stanza.stanzaType in ('chat', 'normal'):
     927+            priorities = {}
     928+            for resource, session in self.sessions.iteritems():
     929+                if not session.presence or not session.presence.available:
     930+                    continue
     931+                priority = session.presence.priority
     932+                if priority >= 0:
     933+                    priorities.setdefault(priority, set()).add(resource)
     934+            if priorities:
     935+                maxPriority = max(priorities.keys())
     936+                recipients = priorities[maxPriority]
     937+            else:
     938+                # No available resource, offline storage not supported
     939+                raise NotImplementedError("Offline storage is not supported")
     940+        else:
     941+            recipients = set()
     942+
     943+        if recipients:
     944+            for resource in recipients:
     945+                session = self.sessions[resource]
     946+                session.receive(stanza.element)
     947+        else:
     948+            # silently discard
     949+            log.msg("Discarding message to %r" % stanza.recipient)
     950+
     951+
     952+    def deliverPresence(self, stanza):
     953+        if not stanza.recipient.resource:
     954+            # record
     955+
     956+            for session in self.sessions.itervalues():
     957+                if session.presence:
     958+                    session.receive(stanza.element)
     959+
     960+
     961+    def probePresence(self, session):
     962+        """
     963+        Probe presences for this user.
     964+
     965+        If this is the first session requesting presence probes, they are
     966+        sent out to the contacts via the realm. After that, the last received
     967+        presences are sent back to the session directly.
     968+        """
     969+        if not self.probeSent:
     970+            # send out probes
     971+            self.contactPresences = {}
     972+            self.realm.server.probePresence(self)
     973+            self.probeSent = True
     974+        else:
     975+            # deliver known contact presences
     976+            for presence in self.contactPresences.itervalues():
     977+                session.receive(presence.element)
     978+
     979+
     980+    @defer.inlineCallbacks
     981+    def broadcastPresence(self, presence):
     982+        """
     983+        Broadcast presence to all subscribed contacts and myself.
     984+        """
     985+        subscribers = yield self.roster.getSubscribers()
     986+        self.realm.server.multicast(presence, subscribers)
     987+
     988+
     989+    @defer.inlineCallbacks
     990+    def getPresences(self, entity):
     991+        """
     992+        Get presences on behalf of a contact.
     993+
     994+        @param entity: The contact requesting that initiated a presence probe.
     995+        @type entity: L{JID}
     996+
     997+        @return: Deferred that fires with an iterable of
     998+            L{AvailabilityPresence}.
     999+        @rtype: L{defer.Deferred}
     1000+
     1001+        @raise NotSubscribed: If the contact does not have a presence
     1002+            subscription from this user.
     1003+        @raise NoSuchContact: If the requestor is not a contact.
     1004+        """
     1005+        bareEntity = entity.userhostJID()
     1006+        item = yield self.roster.getContact(bareEntity)
     1007+
     1008+        if not item.subscriptionFrom:
     1009+            raise NotSubscribed()
     1010+
     1011+        presences = (session.presence for session in self.sessions.itervalues()
     1012+                                      if session.presence)
     1013+        defer.returnValue(presences)
     1014+        # TODO: send last unavailable or unavailable presence?
     1015+
     1016+
     1017+
     1018+@implementer(portal.IRealm)
     1019+class BaseRealm(object):
     1020+    server = None
     1021+
     1022+    def __init__(self, domain):
     1023+        self.domain = domain
     1024+
     1025+
     1026+    def lookupUser(self, entity):
     1027+        raise NotImplementedError()
     1028+
     1029+
     1030+    def createUser(self, entity):
     1031+        raise NotImplementedError()
     1032+
     1033+
     1034+    def getUser(self, entity):
     1035+        def trapNoSuchUser(failure):
     1036+            failure.trap(NoSuchUser)
     1037+            return self.createUser(entity)
     1038+
     1039+        d = self.lookupUser(entity)
     1040+        d.addErrback(trapNoSuchUser)
     1041+        return d
     1042+
     1043+
     1044+    def logoutFactory(self, session):
     1045+        return session.logout
     1046+
     1047+
     1048+    def entityFromAvatarID(self, avatarId):
     1049+        localpart = avatarId.decode('utf-8')
     1050+        return JID(tuple=(localpart, self.domain, None))
     1051+
     1052+
     1053+    def requestAvatar(self, avatarId, mind, *interfaces):
     1054+        if IUserSession not in interfaces:
     1055+            raise NotImplementedError(self, interfaces)
     1056+
     1057+        entity = self.entityFromAvatarID(avatarId)
     1058+
     1059+        def gotUser(user):
     1060+            session = UserSession(user)
     1061+            session.loggedIn(self, mind)
     1062+            return IUserSession, session, self.logoutFactory(session)
     1063+
     1064+        d = self.getUser(entity)
     1065+        d.addCallback(gotUser)
     1066+        return d
     1067+
     1068+
     1069+
     1070+class AnonymousRealm(BaseRealm):
     1071+
     1072+    def __init__(self, domain):
     1073+        BaseRealm.__init__(self, domain)
     1074+        self.users = {}
     1075+
     1076+
     1077+    def entityFromAvatarID(self, avatarId):
     1078+        localpart = randbytes.secureRandom(8).encode('hex')
     1079+        return JID(tuple=(localpart, self.domain, None))
     1080+
     1081+
     1082+    def lookupUser(self, entity):
     1083+        try:
     1084+            user = self.users[entity]
     1085+        except KeyError:
     1086+            return defer.fail(NoSuchUser(entity))
     1087+        return defer.succeed(user)
     1088+
     1089+
     1090+    def createUser(self, entity):
     1091+        user = User(entity, InMemoryRoster([]))
     1092+        user.realm = self
     1093+        self.users[entity] = user
     1094+        return defer.succeed(user)
     1095+
     1096+
     1097+    def logoutFactory(self, session):
     1098+        def logout():
     1099+            session.logout()
     1100+            del self.users[session.user.entity]
     1101+        return logout
     1102+
     1103+
     1104+
     1105+class StaticRealm(BaseRealm):
     1106+
     1107+    def __init__(self, domain, users):
     1108+        BaseRealm.__init__(self, domain)
     1109+        for user in users.itervalues():
     1110+            user.realm = self
     1111+        self.users = users
     1112+
     1113+
     1114+    def lookupUser(self, entity):
     1115+        try:
     1116+            user = self.users[entity]
     1117+        except KeyError:
     1118+            return defer.fail(NoSuchUser(entity))
     1119+        return defer.succeed(user)
     1120+
     1121+
     1122+    def createUser(self, entity):
     1123+        return defer.fail(ecred.LoginDenied("Can't create a new user"))
     1124+
     1125+
     1126+
     1127+class SessionManager(InternalComponent):
    891128+    """
    901129+    Session Manager.
     
    971136+    """
    981137+
    99 +    implements(portal.IRealm)
    100 +
    101 +    def __init__(self, domain, accounts):
    102 +        XMPPHandler.__init__(self)
    103 +        self.domain = domain
    104 +        self.accounts = accounts
    105 +
    106 +        self.sessions = {}
    107 +        self.clientStream = utility.EventDispatcher()
    108 +        self.clientStream.addObserver('/*', self.routeOrDeliver, -1)
    109 +
    110 +
    111 +    def requestAvatar(self, avatarId, mind, *interfaces):
    112 +        if IUserSession not in interfaces:
    113 +            raise NotImplementedError(self, interfaces)
    114 +
    115 +        localpart = avatarId.decode('utf-8')
    116 +        entity = JID(tuple=(localpart, self.domain, None))
    117 +        session = UserSession(entity)
    118 +        session.loggedIn(self, mind)
    119 +        return IUserSession, session, session.logout
    120 +
    121 +
    122 +    def bindResource(self, session, resource):
    123 +        localpart = session.entity.user
    124 +
    125 +        try:
    126 +            userSessions = self.sessions[localpart]
    127 +        except KeyError:
    128 +            userSessions = self.sessions[localpart] = {}
    129 +
    130 +        if resource is None:
    131 +            resource = randbytes.secureRandom(8).encode('hex')
    132 +        elif resource in self.userSessions:
    133 +            resource = resource + ' ' + randbytes.secureRandom(8).encode('hex')
    134 +
    135 +        entity = JID(tuple=(session.entity.user, session.entity.host, resource))
    136 +        userSessions[resource] = session
    137 +
    138 +        return defer.succeed(entity)
    139 +
    140 +
    141 +    def lookupSessions(self, entity):
    142 +        """
    143 +        Return all sessions for a user.
    144 +
    145 +        @param entity: Entity to retrieve sessions for. This the resource part
    146 +            will be ignored.
    147 +        @type entity: L{JID<twisted.words.protocols.jabber.jid.JID>}
    148 +
    149 +        @return: Mapping of sessions keyed by resource.
    150 +        @rtype: C{dict}
    151 +        """
    152 +        localpart = entity.user
    153 +
    154 +        try:
    155 +            return self.sessions[localpart]
    156 +        except:
    157 +            return {}
    158 +
    159 +
    160 +    def lookupSession(self, entity):
    161 +        """
    162 +        Return the session for a particular resource of an entity.
    163 +
    164 +        @param entity: Entity to retrieve sessions for.
    165 +        @type entity: L{JID<twisted.words.protocols.jabber.jid.JID>}
    166 +
    167 +        @return: C{UserSession}.
    168 +        """
    169 +
    170 +        userSessions = self.lookupSessions(entity)
    171 +        return userSessions[entity.resource]
    172 +
    173 +
    174 +
    175 +    def unbindResource(self, session, reason=None):
    176 +        session.connected = False
    177 +
    178 +        localpart = session.entity.user
    179 +        resource = session.entity.resource
    180 +
    181 +        del self.sessions[localpart][resource]
    182 +        if not self.sessions[localpart]:
    183 +            del self.sessions[localpart]
    184 +
    185 +        return defer.succeed(None)
    186 +
    187 +
    188 +    def onElement(self, element, session):
    189 +        # Make sure each stanza has a sender address
    190 +        if (element.name == 'presence' and
    191 +            element.getAttribute('type') in ('subscribe', 'subscribed',
    192 +                                             'unsubscribe', 'unsubscribed')):
    193 +            element['from'] = session.entity.userhost()
    194 +        else:
    195 +            element['from'] = session.entity.full()
    196 +
    197 +        self.clientStream.dispatch(element)
     1138+    def startService(self):
     1139+        InternalComponent.startService(self)
     1140+        self.xmlstream.send = self.routeOrDeliver
    1981141+
    1991142+
     
    2021145+        Deliver a stanza locally or pass on for routing.
    2031146+        """
    204 +        if element.handled:
    205 +            return
    206 +
    207 +        if (not element.hasAttribute('to') or
    208 +            internJID(element['to']).host == self.domain):
     1147+        if (JID(element['to']).host in self.domains):
    2091148+            # This stanza is for local delivery
    2101149+            log.msg("Delivering locally: %r" % element.toXml())
    211 +            self.xmlstream.dispatch(element)
     1150+            self._pipe.source.dispatch(element)
    2121151+        else:
    2131152+            # This stanza is for remote routing
    2141153+            log.msg("Routing remotely: %r" % element.toXml())
    215 +            XMPPHandler.send(self, element)
    216 +
    217 +
    218 +    def deliverStanza(self, element, recipient):
    219 +        session = self.lookupSession(recipient)
    220 +        session.receive(element)
    221 diff --git a/wokkel/test/test_client.py b/wokkel/test/test_client.py
    222 --- a/wokkel/test/test_client.py
    223 +++ b/wokkel/test/test_client.py
    224 @@ -7,7 +7,7 @@
    225  
    226  from base64 import b64encode
    227  
    228 -from zope.interface import implements
    229 +from zope.interface import implements, verify
    230  
    231  from twisted.cred.portal import IRealm, Portal
    232  from twisted.cred.checkers import InMemoryUsernamePasswordDatabaseDontUse
    233 @@ -601,6 +601,7 @@
    234  
    235  
    236  
    237 +
    238  class XMPPClientListenAuthenticatorTest(unittest.TestCase):
    239      """
    240      Tests for L{client.XMPPClientListenAuthenticator}.
    241 @@ -670,3 +671,26 @@
    242                           "to='example.com' "
    243                           "version='1.0'>")
    244          self.xmlstream.assertStreamError(self, condition='host-unknown')
    245 +
    246 +
    247 +
    248 +class UserSessionTest(unittest.TestCase):
    249 +
    250 +    def setUp(self):
    251 +        self.session = client.UserSession(JID('user@example.org'))
    252 +
    253 +
    254 +    def test_interface(self):
    255 +        verify.verifyObject(client.IUserSession, self.session)
    256 +
    257 +
    258 +
    259 +class SessionManagerTest(unittest.TestCase):
    260 +
    261 +    def setUp(self):
    262 +        accounts = {'user': None}
    263 +        self.sessionManager = client.SessionManager('example.org', accounts)
    264 +
    265 +
    266 +    def test_interface(self):
    267 +        verify.verifyObject(IRealm, self.sessionManager)
     1154+            self._pipe.sink.dispatch(element)
     1155+
     1156+
     1157+    def multicast(self, stanza, recipients):
     1158+        """
     1159+
     1160+        @param stanza: The stanza to send. Its C{element} attribute should
     1161+            already be set.
     1162+        @type stanza: L{wokkel.generic.Stanza}.
     1163+
     1164+        @type recipients: iterable of L{JID}.
     1165+        """
     1166+        if not stanza.element:
     1167+            stanza.toElement()
     1168+
     1169+        for recipient in recipients:
     1170+            clone = cloneElement(stanza.element)
     1171+            clone['to'] = recipient.full()
     1172+            clone.handled = False
     1173+            self.routeOrDeliver(clone)
     1174+
     1175+
     1176+    @defer.inlineCallbacks
     1177+    def probePresence(self, user):
     1178+        """
     1179+        Request the presences of all contacts the user has a subscription to.
     1180+
     1181+        This will send out presence probe stanzas, even to local contacts.
     1182+        """
     1183+        subscriptions = yield user.roster.getSubscriptions()
     1184+        for entity in subscriptions:
     1185+            presence = ProbePresence(recipient=entity,
     1186+                                     sender=user.entity)
     1187+            self.routeOrDeliver(presence.toElement())
Note: See TracChangeset for help on using the changeset viewer.