Changeset 72:727b4d29c48e in ralphm-patches for c2s_stanza_handlers.patch


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

Major reworking of avatars, session manager and stanza handlers.

File:
1 edited

Legend:

Unmodified
Added
Removed
  • 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
Note: See TracChangeset for help on using the changeset viewer.