source: ralphm-patches/session_manager.patch @ 72:727b4d29c48e

Last change on this file since 72:727b4d29c48e was 72:727b4d29c48e, checked in by Ralph Meijer <ralphm@…>, 8 years ago

Major reworking of avatars, session manager and stanza handlers.

File size: 34.4 KB
RevLine 
[54]1# HG changeset patch
[72]2# Parent fdef0cff7a57368fa21984593ef05e616039e2e2
[54]3
[72]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 """
[66]47 
[72]48+import copy
49+
50 from zope.interface import implements
[54]51 
[72]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.
[66]60+
[72]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
[57]657+from twisted.python import log, randbytes
[72]658 from twisted.words.protocols.jabber import error
659 from twisted.words.protocols.jabber.jid import JID
660 from twisted.words.xish import domish
[54]661 
[72]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 @@
[54]672 
673 
[72]674 
675+class InMemoryRoster(object):
[54]676+
[72]677+    def __init__(self, items):
678+        self.roster = Roster(((item.entity, item) for item in items))
[54]679+
[66]680+
[72]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)
[66]747+class UserSession(object):
[72]748+    """
749+    An XMPP user session.
[66]750+
[72]751+    This represents the session for a connected client after authenticating
752+    as L{user}.
[66]753+
[72]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
[66]776+    mind = None
[72]777+    entity = None
[66]778+    connected = False
779+    interested = False
780+    presence = None
781+
[72]782+    def __init__(self, user):
783+        self.user = user
[66]784+
785+
786+    def loggedIn(self, realm, mind):
[72]787+        self.mind = mind
[66]788+        self.realm = realm
789+
790+
791+    def bindResource(self, resource):
792+        def cb(entity):
793+            self.entity = entity
794+            self.connected = True
795+            return entity
796+
[72]797+        d = self.user.bindResource(self, resource)
[66]798+        d.addCallback(cb)
799+        return d
800+
801+
802+    def logout(self):
[54]803+        self.connected = False
[72]804+
805+        if self.entity:
806+            self.user.unbindResource(self.entity.resource)
[66]807+
808+
809+    def send(self, element):
[72]810+        """
811+        Called when the client sends a stanza.
812+        """
813+        self.realm.server.routeOrDeliver(element)
[66]814+
815+
[72]816+    def receive(self, element, recipient=None):
817+        """
818+        Deliver a stanza to the client.
819+        """
[66]820+        self.mind.send(element)
[54]821+
822+
[72]823+    def probePresence(self):
824+        self.user.probePresence(self)
[54]825+
[72]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):
[54]1128+    """
1129+    Session Manager.
1130+
1131+    @ivar xmlstream: XML Stream to inject incoming stanzas from client
1132+        connections into. Stanzas where the C{'to'} attribute is not set
1133+        or is directed at the local domain are injected as if received on
1134+        the XML Stream (using C{dispatch}), other stanzas are injected as if
1135+        they were sent from the XML Stream (using C{send}).
1136+    """
1137+
[72]1138+    def startService(self):
1139+        InternalComponent.startService(self)
1140+        self.xmlstream.send = self.routeOrDeliver
[54]1141+
1142+
1143+    def routeOrDeliver(self, element):
[66]1144+        """
1145+        Deliver a stanza locally or pass on for routing.
1146+        """
[72]1147+        if (JID(element['to']).host in self.domains):
[54]1148+            # This stanza is for local delivery
1149+            log.msg("Delivering locally: %r" % element.toXml())
[72]1150+            self._pipe.source.dispatch(element)
[54]1151+        else:
1152+            # This stanza is for remote routing
1153+            log.msg("Routing remotely: %r" % element.toXml())
[72]1154+            self._pipe.sink.dispatch(element)
[54]1155+
1156+
[72]1157+    def multicast(self, stanza, recipients):
1158+        """
[66]1159+
[72]1160+        @param stanza: The stanza to send. Its C{element} attribute should
1161+            already be set.
1162+        @type stanza: L{wokkel.generic.Stanza}.
[66]1163+
[72]1164+        @type recipients: iterable of L{JID}.
1165+        """
1166+        if not stanza.element:
1167+            stanza.toElement()
[66]1168+
[72]1169+        for recipient in recipients:
1170+            clone = cloneElement(stanza.element)
1171+            clone['to'] = recipient.full()
1172+            clone.handled = False
1173+            self.routeOrDeliver(clone)
[66]1174+
1175+
[72]1176+    @defer.inlineCallbacks
1177+    def probePresence(self, user):
1178+        """
1179+        Request the presences of all contacts the user has a subscription to.
[66]1180+
[72]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 TracBrowser for help on using the repository browser.