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
  • new file wokkel/ewokkel.py

    # HG changeset patch
    # Parent fdef0cff7a57368fa21984593ef05e616039e2e2
    
    diff --git a/wokkel/ewokkel.py b/wokkel/ewokkel.py
    new file mode 100644
    - +  
     1# Copyright (c) Ralph Meijer.
     2# See LICENSE for details.
     3
     4"""
     5Exceptions for Wokkel.
     6"""
     7
     8
     9class WokkelError(Exception):
     10    """
     11    Base exception for Wokkel.
     12    """
     13
     14class NoSuchContact(WokkelError):
     15    """
     16    Raised when the given contact is not present in the user's roster.
     17    """
     18
     19class NotSubscribed(WokkelError):
     20    """
     21    Raised when the contact does not have a presence subscription to the user.
     22    """
     23
     24class NoSuchResource(WokkelError):
     25    """
     26    Raised when the given resource is currently not connected.
     27    """
     28
     29class NoSuchUser(WokkelError):
     30    """
     31    Raised when there is no user with the given name or JID.
     32    """
  • wokkel/generic.py

    diff --git a/wokkel/generic.py b/wokkel/generic.py
    a b  
    77Generic XMPP protocol helpers.
    88"""
    99
     10import copy
     11
    1012from zope.interface import implements
    1113
    1214from twisted.internet import defer, protocol
     
    6668    return rootElement
    6769
    6870
     71def cloneElement(element):
     72    """
     73    Make a deep copy of a serialized element.
     74
     75    The returned element is an orphaned deep copy of the given original.
     76
     77    @note: Since the reference to the original parent, if any, is gone,
     78    inherited attributes like C{xml:lang} are not preserved.
     79
     80    @type element: L{domish.Element}.
     81    """
     82    parent = element.parent
     83    element.parent = None
     84    clone = copy.deepcopy(element)
     85    element.parent = parent
     86    return clone
     87
     88
    6989
    7090class FallbackHandler(XMPPHandler):
    7191    """
     
    168188    """
    169189    Abstract representation of a stanza.
    170190
     191    @ivar recipient: The receiving entity.
     192    @type recipient: L{jid.JID}
     193
    171194    @ivar sender: The sending entity.
    172195    @type sender: L{jid.JID}
    173     @ivar recipient: The receiving entity.
    174     @type recipient: L{jid.JID}
     196
     197    @ivar stanzaKind: One of C{'message'}, C{'presence'}, C{'iq'}.
     198    @type stanzaKind: L{unicode}.
     199
     200    @ivar stanzaID: The optional stanza identifier.
     201    @type stanzaID: L{unicode}.
     202
     203    @ivar stanzaType: The optional stanza type.
     204    @type stanzaType: L{unicode}.
     205
     206    @ivar element: The serialized XML of this stanza.
     207    @type element: L{domish.Element}.
    175208    """
    176209
    177210    recipient = None
     
    179212    stanzaKind = None
    180213    stanzaID = None
    181214    stanzaType = None
     215    element = None
     216
    182217
    183218    def __init__(self, recipient=None, sender=None):
    184219        self.recipient = recipient
     
    217252            self.sender = jid.internJID(element['from'])
    218253        if element.hasAttribute('to'):
    219254            self.recipient = jid.internJID(element['to'])
     255        self.stanzaKind = element.name
    220256        self.stanzaType = element.getAttribute('type')
    221257        self.stanzaID = element.getAttribute('id')
    222258
     
    242278
    243279    def toElement(self):
    244280        element = domish.Element((None, self.stanzaKind))
     281        self.element = element
    245282        if self.sender is not None:
    246283            element['from'] = self.sender.full()
    247284        if self.recipient is not None:
  • wokkel/iwokkel.py

    diff --git a/wokkel/iwokkel.py b/wokkel/iwokkel.py
    a b  
    996996        """)
    997997
    998998
     999    interested = Attribute(
     1000        """
     1001        This session represents a I{interested resource}, i.e.  the user's
     1002        roster has been requested and roster pushes will be sent out to
     1003        this session.
     1004        """)
     1005
     1006
    9991007    def loggedIn(realm, mind):
    10001008        """
    10011009        Called by the realm when login occurs.
  • wokkel/test/test_generic.py

    diff --git a/wokkel/test/test_generic.py b/wokkel/test/test_generic.py
    a b  
    2424
    2525NS_VERSION = 'jabber:iq:version'
    2626
     27class CloneElementTest(unittest.TestCase):
     28    """
     29    Tests for L{xmppim.clonePresence}.
     30    """
     31
     32    def test_rootElement(self):
     33        """
     34        The copied presence stanza is not identical, but renders identically.
     35        """
     36        parent = object()
     37        originalElement = domish.Element((None, 'presence'))
     38        originalElement.parent = parent
     39        copyElement = generic.cloneElement(originalElement)
     40
     41        self.assertNotIdentical(copyElement, originalElement)
     42        self.assertEqual(copyElement.toXml(), originalElement.toXml())
     43        self.assertIdentical(None, copyElement.parent)
     44        self.assertIdentical(parent, originalElement.parent)
     45
     46
     47
    2748class VersionHandlerTest(unittest.TestCase):
    2849    """
    2950    Tests for L{wokkel.generic.VersionHandler}.
     
    110131        <message type='chat' from='other@example.org' to='user@example.org'/>
    111132        """
    112133
    113         stanza = generic.Stanza.fromElement(generic.parseXml(xml))
     134        element = generic.parseXml(xml)
     135        stanza = generic.Stanza.fromElement(element)
    114136        self.assertEqual('chat', stanza.stanzaType)
    115137        self.assertEqual(JID('other@example.org'), stanza.sender)
    116138        self.assertEqual(JID('user@example.org'), stanza.recipient)
     139        self.assertIdentical(element, stanza.element)
     140
     141
     142    def test_fromElementStanzaKind(self):
     143        """
     144        The stanza kind is also recorded in the stanza.
     145        """
     146        xml = """<presence/>"""
     147        stanza = generic.Stanza.fromElement(generic.parseXml(xml))
     148        self.assertEqual(u'presence', stanza.stanzaKind)
    117149
    118150
    119151    def test_fromElementChildParser(self):
  • wokkel/test/test_xmppim.py

    diff --git a/wokkel/test/test_xmppim.py b/wokkel/test/test_xmppim.py
    a b  
    55Tests for L{wokkel.xmppim}.
    66"""
    77
     8from zope.interface import verify
     9
     10from twisted.cred import checkers, error as ecred
     11from twisted.cred.portal import IRealm
    812from twisted.internet import defer
    913from twisted.trial import unittest
    1014from twisted.words.protocols.jabber import error
     
    1216from twisted.words.protocols.jabber.xmlstream import toResponse
    1317from twisted.words.xish import domish, utility
    1418
    15 from wokkel import xmppim
     19from wokkel import ewokkel, component, xmppim
    1620from wokkel.generic import ErrorStanza, parseXml
    1721from wokkel.test.helpers import TestableRequestHandlerMixin, XmlStreamStub
     22from wokkel.subprotocols import IQHandlerMixin
    1823
    1924NS_XML = 'http://www.w3.org/XML/1998/namespace'
    2025NS_ROSTER = 'jabber:iq:roster'
     
    99104        self.assertEquals(50, presence.priority)
    100105
    101106
     107
    102108class PresenceProtocolTest(unittest.TestCase):
    103109    """
    104110    Tests for L{xmppim.PresenceProtocol}
     
    14181424
    14191425
    14201426
     1427class InMemoryRosterTest(unittest.TestCase):
     1428    """
     1429    Tests for L{xmppim.InMemoryRoster}.
     1430    """
     1431
     1432    def setUp(self):
     1433        contacts = [
     1434            xmppim.RosterItem(JID('contact1@example.org'),
     1435                              subscriptionFrom=True,
     1436                              subscriptionTo=False),
     1437            xmppim.RosterItem(JID('contact2@example.org'),
     1438                              subscriptionFrom=False,
     1439                              subscriptionTo=True),
     1440            ]
     1441        self.roster = xmppim.InMemoryRoster(contacts)
     1442
     1443
     1444    def test_getSubscribers(self):
     1445        def gotSubscribers(subscribers):
     1446            subscribers = list(subscribers)
     1447            self.assertIn(JID('contact1@example.org'), subscribers)
     1448            self.assertNotIn(JID('contact2@example.org'), subscribers)
     1449
     1450
     1451        d = self.roster.getSubscribers()
     1452        d.addCallback(gotSubscribers)
     1453        return d
     1454
     1455
     1456    def test_getSubscriptions(self):
     1457        def gotSubscriptions(subscriptions):
     1458            subscriptions = list(subscriptions)
     1459            self.assertNotIn(JID('contact1@example.org'), subscriptions)
     1460            self.assertIn(JID('contact2@example.org'), subscriptions)
     1461
     1462        d = self.roster.getSubscriptions()
     1463        d.addCallback(gotSubscriptions)
     1464        return d
     1465
     1466
     1467class UserRosterProtocolTest(unittest.TestCase):
     1468    """
     1469    Tests for L{xmppim.UserRosterProtocol}.
     1470    """
     1471
     1472    def setUp(self):
     1473        self.stub = XmlStreamStub()
     1474        self.service = xmppim.UserRosterProtocol()
     1475        self.service.makeConnection(self.stub.xmlstream)
     1476
     1477        entity = JID(u'user@example.org')
     1478        user = xmppim.User(entity)
     1479
     1480        contact = xmppim.RosterItem(JID(u'contact@example.org'))
     1481        user.roster = xmppim.InMemoryRoster([contact])
     1482
     1483        self.session = xmppim.UserSession(user)
     1484        self.stub.xmlstream.avatar = self.session
     1485
     1486
     1487    def test_getRoster(self):
     1488        """
     1489        The returned roster is gotten from the session user.
     1490        """
     1491        def gotRoster(result):
     1492            self.assertIn(JID(u'contact@example.org'), result)
     1493
     1494        request = xmppim.RosterRequest()
     1495        d = self.service.getRoster(request)
     1496        d.addCallback(gotRoster)
     1497        return d
     1498
     1499
     1500    def test_getRosterInterested(self):
     1501        """
     1502        Requesting the roster marks the session as interested in roster pushes.
     1503        """
     1504        def gotRoster(result):
     1505            self.assertTrue(self.session.interested)
     1506
     1507        request = xmppim.RosterRequest()
     1508        d = self.service.getRoster(request)
     1509        d.addCallback(gotRoster)
     1510        return d
     1511
     1512
     1513    def test_handleRequestLocal(self):
     1514        """
     1515        Handle requests without recipient (= local server).
     1516        """
     1517        called = []
     1518        request = xmppim.RosterRequest(recipient=None)
     1519        self.patch(IQHandlerMixin, 'handleRequest', called.append)
     1520        self.service.handleRequest(request.toElement())
     1521        self.assertTrue(called)
     1522
     1523
     1524    def test_handleRequestOther(self):
     1525        """
     1526        If the request has a non-empty recipient, ignore this request.
     1527        """
     1528        request = xmppim.RosterRequest(recipient=JID('other.example.org'))
     1529        self.assertFalse(self.service.handleRequest(request.toElement()))
     1530
     1531
     1532
    14211533class MessageTest(unittest.TestCase):
    14221534    """
    14231535    Tests for L{xmppim.Message}.
     
    15981710                         "was deprecated in Wokkel 0.8.0; "
    15991711                         "please use MessageProtocol.messageReceived instead.",
    16001712                         warnings[0]['message'])
     1713
     1714
     1715
     1716class UserSessionTest(unittest.TestCase):
     1717    """
     1718    Tests for L{xmppim.UserSession}.
     1719    """
     1720
     1721    def setUp(self):
     1722        self.session = xmppim.UserSession(None)
     1723
     1724
     1725    def test_interface(self):
     1726        """
     1727        UserSession implements IUserSession.
     1728        """
     1729        verify.verifyObject(xmppim.IUserSession, self.session)
     1730
     1731
     1732class UserTest(unittest.TestCase):
     1733    """
     1734    Tests for L{xmppim.User}.
     1735    """
     1736
     1737    def setUp(self):
     1738        self.user = xmppim.User(JID('user@example.org'),
     1739                                xmppim.InMemoryRoster([]))
     1740        self.session = xmppim.UserSession(self.user)
     1741        self.session.bindResource('Home')
     1742
     1743
     1744    @defer.inlineCallbacks
     1745    def test_getPresences(self):
     1746        """
     1747        A contact with a subscription to the user gets its presence.
     1748        """
     1749        contact = xmppim.RosterItem(JID('contact@example.org'),
     1750                                     subscriptionFrom=True)
     1751        self.user.roster.roster[contact.entity] = contact
     1752
     1753        presence = xmppim.AvailabilityPresence()
     1754        self.session.presence = presence
     1755
     1756        presences = yield self.user.getPresences(JID('contact@example.org'))
     1757        self.assertEqual([presence], list(presences))
     1758
     1759        defer.returnValue(None)
     1760
     1761
     1762    def test_getPresencesNoSubscription(self):
     1763        """
     1764        A contact without a subscription raises NotSubscribed.
     1765        """
     1766        contact = xmppim.RosterItem(JID('contact@example.org'),
     1767                                     subscriptionFrom=False)
     1768        self.user.roster.roster[contact.entity] = contact
     1769
     1770        presence = xmppim.AvailabilityPresence()
     1771        self.session.presence = presence
     1772
     1773        d = self.user.getPresences(JID('contact@example.org'))
     1774        self.assertFailure(d, ewokkel.NotSubscribed)
     1775        return d
     1776
     1777
     1778    def test_getPresencesUnknown(self):
     1779        """
     1780        A contact with a subscription to the user gets its presence.
     1781        """
     1782        presence = xmppim.AvailabilityPresence()
     1783        self.session.presence = presence
     1784        d = self.user.getPresences(JID('unknown@example.org'))
     1785        self.assertFailure(d, ewokkel.NoSuchContact)
     1786        return d
     1787
     1788
     1789class AnonymousRealmTest(unittest.TestCase):
     1790    """
     1791    Tests for L{xmppim.AnonymousRealm}.
     1792    """
     1793
     1794    def setUp(self):
     1795        self.realm = xmppim.AnonymousRealm('example.org')
     1796
     1797
     1798    def test_interface(self):
     1799        """
     1800        AnonymousRealm implements IRealm.
     1801        """
     1802        verify.verifyObject(IRealm, self.realm)
     1803
     1804
     1805    def test_requestAvatarAnonymous(self):
     1806        """
     1807        An anonymous avatar ID yields a generated JID.
     1808        """
     1809        def gotAvatar(result):
     1810            def gotUser(user):
     1811                self.assertIdentical(user, avatar.user)
     1812
     1813            iface, avatar, logout = result
     1814            self.assertNotIdentical(None, avatar.user)
     1815            self.assertNotIdentical(None, avatar.user.entity)
     1816            self.assertNotIdentical(None, avatar.user.entity.host)
     1817            self.assertEqual(u'example.org', avatar.user.entity.host)
     1818
     1819            d = self.realm.lookupUser(avatar.user.entity)
     1820            d.addCallback(gotUser)
     1821            return d
     1822
     1823        avatarID = checkers.ANONYMOUS
     1824        d = self.realm.requestAvatar(avatarID, None, xmppim.IUserSession)
     1825        d.addCallback(gotAvatar)
     1826        return d
     1827
     1828
     1829    def test_requestAvatarAnonymousDifferent(self):
     1830        """
     1831        Requesting two anonymous avatar IDs yields different JIDs.
     1832        """
     1833        def gotAvatar1(result):
     1834            iface, avatar1, logout = result
     1835
     1836            def gotAvatar2(result):
     1837                iface, avatar2, logout = result
     1838                self.assertNotIdentical(avatar1, avatar2)
     1839                self.assertNotIdentical(avatar1.user, avatar2.user)
     1840                self.assertNotEqual(avatar1.user.entity,
     1841                                    avatar2.user.entity)
     1842
     1843            d = self.realm.requestAvatar(avatarID, None, xmppim.IUserSession)
     1844            d.addCallback(gotAvatar2)
     1845            return d
     1846
     1847        avatarID = checkers.ANONYMOUS
     1848        d = self.realm.requestAvatar(avatarID, None, xmppim.IUserSession)
     1849        d.addCallback(gotAvatar1)
     1850        return d
     1851
     1852
     1853    def test_logout(self):
     1854        """
     1855        When the logout function is called, the user is removed from the realm.
     1856        """
     1857        def gotAvatar(result):
     1858            iface, avatar, logout = result
     1859
     1860            logout()
     1861
     1862            entity = avatar.user.entity
     1863            d = self.realm.lookupUser(entity)
     1864            self.assertFailure(d, ewokkel.NoSuchUser)
     1865            return d
     1866
     1867        avatarID = checkers.ANONYMOUS
     1868        d = self.realm.requestAvatar(avatarID, None, xmppim.IUserSession)
     1869        d.addCallback(gotAvatar)
     1870        return d
     1871
     1872
     1873
     1874class StaticRealmTest(unittest.TestCase):
     1875    """
     1876    Tests for L{xmppim.StaticRealmTest}.
     1877    """
     1878
     1879    def setUp(self):
     1880        entity = JID(u'\u00e9lise@example.org')
     1881        self.user = xmppim.User(entity)
     1882        users = {entity: self.user}
     1883        self.realm = xmppim.StaticRealm('example.org', users)
     1884
     1885
     1886    def test_interface(self):
     1887        """
     1888        StaticRealm implements IRealm.
     1889        """
     1890        verify.verifyObject(IRealm, self.realm)
     1891
     1892
     1893    def test_requestAvatar(self):
     1894        """
     1895        A UserSession is initialized and returned from requestAvatar.
     1896        """
     1897        def gotAvatar(result):
     1898            iface, avatar, logout = result
     1899            self.assertIdentical(self.user, avatar.user)
     1900
     1901        avatarID = u'\u00e9lise'.encode('utf-8')
     1902        d = self.realm.requestAvatar(avatarID, None, xmppim.IUserSession)
     1903        d.addCallback(gotAvatar)
     1904        return d
     1905
     1906
     1907    def test_requestAvatarUnknown(self):
     1908        """
     1909        A UserSession is initialized and returned from requestAvatar.
     1910        """
     1911        avatarID = u'nobody'.encode('utf-8')
     1912        d = self.realm.requestAvatar(avatarID, None, xmppim.IUserSession)
     1913        self.assertFailure(d, ecred.LoginDenied)
     1914        return d
     1915
     1916
     1917
     1918class TestRouter(component.Router):
     1919    """
     1920    Router that only records incoming traffic for testing.
     1921    """
     1922
     1923    def __init__(self):
     1924        super(TestRouter, self).__init__()
     1925        self.output = []
     1926
     1927
     1928    def route(self, stanza):
     1929        """
     1930        All routed stanzas are recorded.
     1931        """
     1932        self.output.append(stanza)
     1933
     1934
     1935
     1936class SessionManagerTest(unittest.TestCase):
     1937    """
     1938    Tests for L{xmppim.SessionManager}.
     1939    """
     1940
     1941    def setUp(self):
     1942        self.router = TestRouter()
     1943        self.sessionManager = xmppim.SessionManager(self.router,
     1944                                                    u'example.org')
     1945        self.sessionManager.startService()
     1946
     1947        self.input = []
     1948        def onElement(element):
     1949            self.input.append(element)
     1950
     1951        self.sessionManager.xmlstream.addObserver('/*', onElement)
     1952
     1953
     1954    def test_routeOrDeliverLocal(self):
     1955        """
     1956        Stanzas for local domains are reinjected in to the XML stream.
     1957        """
     1958        element = parseXml("""<presence from='test@example.org'
     1959                                        to='other@example.org'/>""")
     1960        self.sessionManager.xmlstream.send(element)
     1961
     1962        self.assertEqual(1, len(self.input))
     1963        self.assertEqual(0, len(self.router.output))
     1964
     1965
     1966    def test_routeOrDeliverRemote(self):
     1967        """
     1968        Stanzas for other domains are sent to the router.
     1969        """
     1970        element = parseXml("""<presence from='test@example.org'
     1971                                        to='other@example.com'/>""")
     1972        self.sessionManager.xmlstream.send(element)
     1973
     1974        self.assertEqual(0, len(self.input))
     1975        self.assertEqual(1, len(self.router.output))
     1976
     1977
     1978    def test_probePresence(self):
     1979        """
     1980        Presence probes are sent to contacts with a subscription.
     1981        """
     1982        def cb(result):
     1983            self.assertEqual(1, len(self.router.output))
     1984            element = self.router.output[-1]
     1985            self.assertEqual(u'presence',
     1986                             element.name)
     1987            self.assertEqual(u'probe',
     1988                             element.getAttribute(u'type'))
     1989            self.assertEqual(u'contact2@example.com',
     1990                             element.getAttribute(u'to'))
     1991
     1992        contacts = [
     1993            xmppim.RosterItem(JID(u'contact1@example.com'),
     1994                              subscriptionFrom=True,
     1995                              subscriptionTo=False),
     1996            xmppim.RosterItem(JID(u'contact2@example.com'),
     1997                              subscriptionFrom=False,
     1998                              subscriptionTo=True),
     1999            ]
     2000        roster = xmppim.InMemoryRoster(contacts)
     2001        entity = JID(u'user@example.org')
     2002        user = xmppim.User(entity, roster)
     2003
     2004        d = self.sessionManager.probePresence(user)
     2005        d.addCallback(cb)
     2006        return d
  • wokkel/xmppim.py

    diff --git a/wokkel/xmppim.py b/wokkel/xmppim.py
    a b  
    1212
    1313import warnings
    1414
     15from zope.interface import implementer
     16
     17from twisted.cred import error as ecred, portal
    1518from twisted.internet import defer
     19from twisted.python import log, randbytes
    1620from twisted.words.protocols.jabber import error
    1721from twisted.words.protocols.jabber.jid import JID
    1822from twisted.words.xish import domish
    1923
    20 from wokkel.generic import ErrorStanza, Stanza, Request
     24from wokkel.component import InternalComponent
     25from wokkel.ewokkel import NoSuchContact
     26from wokkel.ewokkel import NotSubscribed, NoSuchResource, NoSuchUser
     27from wokkel.iwokkel import IUserSession
     28from wokkel.generic import ErrorStanza, Stanza, Request, cloneElement
    2129from wokkel.subprotocols import IQHandlerMixin
    2230from wokkel.subprotocols import XMPPHandler
    2331from wokkel.subprotocols import asyncObserver
     
    10621070
    10631071
    10641072
     1073class InMemoryRoster(object):
     1074
     1075    def __init__(self, items):
     1076        self.roster = Roster(((item.entity, item) for item in items))
     1077
     1078
     1079    def getRoster(self, version=None):
     1080        return defer.succeed(self.roster)
     1081
     1082
     1083    def getSubscribers(self):
     1084        subscribers = (entity for entity, item in self.roster.iteritems()
     1085                              if item.subscriptionFrom)
     1086        return defer.succeed(subscribers)
     1087
     1088
     1089    def getSubscriptions(self):
     1090        subscriptions = (entity for entity, item in self.roster.iteritems()
     1091                                if item.subscriptionTo)
     1092        return defer.succeed(subscriptions)
     1093
     1094
     1095    def getContact(self, entity):
     1096        try:
     1097            return defer.succeed(self.roster[entity])
     1098        except KeyError:
     1099            return defer.fail(NoSuchContact())
     1100
     1101
     1102
     1103
     1104
     1105class UserRosterProtocol(RosterServerProtocol):
     1106    """
     1107    Roster protocol handler for client connections.
     1108
     1109    This protocol is meant to be used with
     1110    L{wokkel.client.XMPPC2SServerFactory} to interact with L{UserSession} and
     1111    L{User}.
     1112    """
     1113
     1114    def handleRequest(self, iq):
     1115        """
     1116        Ignore roster requests for non-empty recipients.
     1117        """
     1118        if iq.getAttribute('to'):
     1119            return False
     1120        else:
     1121            return super(UserRosterProtocol, self).handleRequest(iq)
     1122
     1123
     1124    def getRoster(self, request):
     1125        """
     1126        Return the roster of the user associated with this session.
     1127        """
     1128        session = self.xmlstream.avatar
     1129        session.interested = True
     1130        return session.user.roster.getRoster()
     1131
     1132
     1133
    10651134class Message(Stanza):
    10661135    """
    10671136    A message stanza.
     
    11591228
    11601229            self.onMessage(message.element)
    11611230            return True
     1231
     1232
     1233
     1234@implementer(IUserSession)
     1235class UserSession(object):
     1236    """
     1237    An XMPP user session.
     1238
     1239    This represents the session for a connected client after authenticating
     1240    as L{user}.
     1241
     1242    @ivar user: The authenticated user for this session.
     1243    @type user: L{User}
     1244
     1245    @ivar mind: The protocol instance for this session.
     1246    @type mind: L{xmlstream.Xmlstream}
     1247
     1248    @ivar entity: The full JID of the entity after a resource has been
     1249       bound.
     1250    @type entity: L{JID}
     1251
     1252    @ivar connected: Flag that is C{True} while a resource is bound.
     1253    @type connected: L{boolean}
     1254
     1255    @ivar interested: Flag to record that the roster has been requested. When
     1256        L{True}, this session will receive roster pushes.
     1257    @type interested: L{boolean}
     1258
     1259    @ivar presence: Last broadcast presence from the client for this session.
     1260    @type presence: L{AvailabilityPresence}
     1261    """
     1262
     1263    user = None
     1264    mind = None
     1265    entity = None
     1266    connected = False
     1267    interested = False
     1268    presence = None
     1269
     1270    def __init__(self, user):
     1271        self.user = user
     1272
     1273
     1274    def loggedIn(self, realm, mind):
     1275        self.mind = mind
     1276        self.realm = realm
     1277
     1278
     1279    def bindResource(self, resource):
     1280        def cb(entity):
     1281            self.entity = entity
     1282            self.connected = True
     1283            return entity
     1284
     1285        d = self.user.bindResource(self, resource)
     1286        d.addCallback(cb)
     1287        return d
     1288
     1289
     1290    def logout(self):
     1291        self.connected = False
     1292
     1293        if self.entity:
     1294            self.user.unbindResource(self.entity.resource)
     1295
     1296
     1297    def send(self, element):
     1298        """
     1299        Called when the client sends a stanza.
     1300        """
     1301        self.realm.server.routeOrDeliver(element)
     1302
     1303
     1304    def receive(self, element, recipient=None):
     1305        """
     1306        Deliver a stanza to the client.
     1307        """
     1308        self.mind.send(element)
     1309
     1310
     1311    def probePresence(self):
     1312        self.user.probePresence(self)
     1313
     1314
     1315    def broadcastPresence(self, presence, available):
     1316        if available:
     1317            self.presence = presence
     1318        else:
     1319            self.presence = None
     1320            # TODO: unset probeSent on user?
     1321            # TODO: save last unavailable presence?
     1322
     1323        self.user.broadcastPresence(presence)
     1324
     1325
     1326
     1327class User(object):
     1328    """
     1329    An XMPP user account.
     1330
     1331    @ivar entity: The JID of the user.
     1332    @type entity: L{JID}
     1333
     1334    @ivar roster: The user's roster.
     1335
     1336    @ivar sessions: The currently connected sessions for this user, indexed by
     1337        resource.
     1338    @type sessions: L{dict}
     1339
     1340    @ivar probeSent: Flag that is C{True} if presence probes have been sent
     1341        out for this user. This is only done on the initial presence broadcast
     1342        of the first available resource. Subsequent resources will get the
     1343        presences stored in L{contactPresences}.
     1344    @type probeSent: L{boolean}
     1345
     1346    @ivar contactPresences: Cached presences of contacts as a mapping of L{JID}
     1347        to L{AvailabilityPresence}.
     1348    @type contactPresences: L{dict}
     1349
     1350    @ivar realm: The realm that provided this user object.
     1351    @type realm: L{IRealm} provider
     1352    """
     1353
     1354    realm = None
     1355
     1356    def __init__(self, entity, roster=None):
     1357        self.entity = entity
     1358        self.roster = roster
     1359        self.sessions = {}
     1360        self.probeSent = False
     1361        self.contactPresences = {}
     1362
     1363
     1364    def bindResource(self, session, resource):
     1365        if resource is None:
     1366            resource = randbytes.secureRandom(8).encode('hex')
     1367        elif resource in self.sessions:
     1368            resource = resource + ' ' + randbytes.secureRandom(8).encode('hex')
     1369
     1370        entity = JID(tuple=(self.entity.user, self.entity.host, resource))
     1371        self.sessions[resource] = session
     1372
     1373        return defer.succeed(entity)
     1374
     1375
     1376    def unbindResource(self, resource):
     1377        del self.sessions[resource]
     1378        return defer.succeed(None)
     1379
     1380
     1381    def deliverIQ(self, stanza):
     1382        try:
     1383            session = self.sessions[stanza.recipient.resource]
     1384        except KeyError:
     1385            raise NoSuchResource()
     1386
     1387        session.receive(stanza.element)
     1388
     1389
     1390    def deliverMessage(self, stanza):
     1391        if stanza.recipient.resource:
     1392            try:
     1393                session = self.sessions[stanza.recipient.resource]
     1394            except KeyError:
     1395                if stanza.stanzaType in ('normal', 'chat', 'headline'):
     1396                    self.deliverMessageAnyResource(stanza)
     1397                else:
     1398                    raise NoSuchResource()
     1399            else:
     1400                session.receive(stanza.element)
     1401        else:
     1402            if stanza.stanzaType == 'groupchat':
     1403                raise NotImplementedError("Groupchat message to the bare JID")
     1404            else:
     1405                self.deliverMessageAnyResource(stanza)
     1406
     1407
     1408    def deliverMessageAnyResource(self, stanza):
     1409        if stanza.stanzaType == 'headline':
     1410            recipients = set()
     1411            for resource, session in self.sessions.iteritems():
     1412                if session.presence.priority >= 0:
     1413                    recipients.add(resource)
     1414        elif stanza.stanzaType in ('chat', 'normal'):
     1415            priorities = {}
     1416            for resource, session in self.sessions.iteritems():
     1417                if not session.presence or not session.presence.available:
     1418                    continue
     1419                priority = session.presence.priority
     1420                if priority >= 0:
     1421                    priorities.setdefault(priority, set()).add(resource)
     1422            if priorities:
     1423                maxPriority = max(priorities.keys())
     1424                recipients = priorities[maxPriority]
     1425            else:
     1426                # No available resource, offline storage not supported
     1427                raise NotImplementedError("Offline storage is not supported")
     1428        else:
     1429            recipients = set()
     1430
     1431        if recipients:
     1432            for resource in recipients:
     1433                session = self.sessions[resource]
     1434                session.receive(stanza.element)
     1435        else:
     1436            # silently discard
     1437            log.msg("Discarding message to %r" % stanza.recipient)
     1438
     1439
     1440    def deliverPresence(self, stanza):
     1441        if not stanza.recipient.resource:
     1442            # record
     1443
     1444            for session in self.sessions.itervalues():
     1445                if session.presence:
     1446                    session.receive(stanza.element)
     1447
     1448
     1449    def probePresence(self, session):
     1450        """
     1451        Probe presences for this user.
     1452
     1453        If this is the first session requesting presence probes, they are
     1454        sent out to the contacts via the realm. After that, the last received
     1455        presences are sent back to the session directly.
     1456        """
     1457        if not self.probeSent:
     1458            # send out probes
     1459            self.contactPresences = {}
     1460            self.realm.server.probePresence(self)
     1461            self.probeSent = True
     1462        else:
     1463            # deliver known contact presences
     1464            for presence in self.contactPresences.itervalues():
     1465                session.receive(presence.element)
     1466
     1467
     1468    @defer.inlineCallbacks
     1469    def broadcastPresence(self, presence):
     1470        """
     1471        Broadcast presence to all subscribed contacts and myself.
     1472        """
     1473        subscribers = yield self.roster.getSubscribers()
     1474        self.realm.server.multicast(presence, subscribers)
     1475
     1476
     1477    @defer.inlineCallbacks
     1478    def getPresences(self, entity):
     1479        """
     1480        Get presences on behalf of a contact.
     1481
     1482        @param entity: The contact requesting that initiated a presence probe.
     1483        @type entity: L{JID}
     1484
     1485        @return: Deferred that fires with an iterable of
     1486            L{AvailabilityPresence}.
     1487        @rtype: L{defer.Deferred}
     1488
     1489        @raise NotSubscribed: If the contact does not have a presence
     1490            subscription from this user.
     1491        @raise NoSuchContact: If the requestor is not a contact.
     1492        """
     1493        bareEntity = entity.userhostJID()
     1494        item = yield self.roster.getContact(bareEntity)
     1495
     1496        if not item.subscriptionFrom:
     1497            raise NotSubscribed()
     1498
     1499        presences = (session.presence for session in self.sessions.itervalues()
     1500                                      if session.presence)
     1501        defer.returnValue(presences)
     1502        # TODO: send last unavailable or unavailable presence?
     1503
     1504
     1505
     1506@implementer(portal.IRealm)
     1507class BaseRealm(object):
     1508    server = None
     1509
     1510    def __init__(self, domain):
     1511        self.domain = domain
     1512
     1513
     1514    def lookupUser(self, entity):
     1515        raise NotImplementedError()
     1516
     1517
     1518    def createUser(self, entity):
     1519        raise NotImplementedError()
     1520
     1521
     1522    def getUser(self, entity):
     1523        def trapNoSuchUser(failure):
     1524            failure.trap(NoSuchUser)
     1525            return self.createUser(entity)
     1526
     1527        d = self.lookupUser(entity)
     1528        d.addErrback(trapNoSuchUser)
     1529        return d
     1530
     1531
     1532    def logoutFactory(self, session):
     1533        return session.logout
     1534
     1535
     1536    def entityFromAvatarID(self, avatarId):
     1537        localpart = avatarId.decode('utf-8')
     1538        return JID(tuple=(localpart, self.domain, None))
     1539
     1540
     1541    def requestAvatar(self, avatarId, mind, *interfaces):
     1542        if IUserSession not in interfaces:
     1543            raise NotImplementedError(self, interfaces)
     1544
     1545        entity = self.entityFromAvatarID(avatarId)
     1546
     1547        def gotUser(user):
     1548            session = UserSession(user)
     1549            session.loggedIn(self, mind)
     1550            return IUserSession, session, self.logoutFactory(session)
     1551
     1552        d = self.getUser(entity)
     1553        d.addCallback(gotUser)
     1554        return d
     1555
     1556
     1557
     1558class AnonymousRealm(BaseRealm):
     1559
     1560    def __init__(self, domain):
     1561        BaseRealm.__init__(self, domain)
     1562        self.users = {}
     1563
     1564
     1565    def entityFromAvatarID(self, avatarId):
     1566        localpart = randbytes.secureRandom(8).encode('hex')
     1567        return JID(tuple=(localpart, self.domain, None))
     1568
     1569
     1570    def lookupUser(self, entity):
     1571        try:
     1572            user = self.users[entity]
     1573        except KeyError:
     1574            return defer.fail(NoSuchUser(entity))
     1575        return defer.succeed(user)
     1576
     1577
     1578    def createUser(self, entity):
     1579        user = User(entity, InMemoryRoster([]))
     1580        user.realm = self
     1581        self.users[entity] = user
     1582        return defer.succeed(user)
     1583
     1584
     1585    def logoutFactory(self, session):
     1586        def logout():
     1587            session.logout()
     1588            del self.users[session.user.entity]
     1589        return logout
     1590
     1591
     1592
     1593class StaticRealm(BaseRealm):
     1594
     1595    def __init__(self, domain, users):
     1596        BaseRealm.__init__(self, domain)
     1597        for user in users.itervalues():
     1598            user.realm = self
     1599        self.users = users
     1600
     1601
     1602    def lookupUser(self, entity):
     1603        try:
     1604            user = self.users[entity]
     1605        except KeyError:
     1606            return defer.fail(NoSuchUser(entity))
     1607        return defer.succeed(user)
     1608
     1609
     1610    def createUser(self, entity):
     1611        return defer.fail(ecred.LoginDenied("Can't create a new user"))
     1612
     1613
     1614
     1615class SessionManager(InternalComponent):
     1616    """
     1617    Session Manager.
     1618
     1619    @ivar xmlstream: XML Stream to inject incoming stanzas from client
     1620        connections into. Stanzas where the C{'to'} attribute is not set
     1621        or is directed at the local domain are injected as if received on
     1622        the XML Stream (using C{dispatch}), other stanzas are injected as if
     1623        they were sent from the XML Stream (using C{send}).
     1624    """
     1625
     1626    def startService(self):
     1627        InternalComponent.startService(self)
     1628        self.xmlstream.send = self.routeOrDeliver
     1629
     1630
     1631    def routeOrDeliver(self, element):
     1632        """
     1633        Deliver a stanza locally or pass on for routing.
     1634        """
     1635        if (JID(element['to']).host in self.domains):
     1636            # This stanza is for local delivery
     1637            log.msg("Delivering locally: %r" % element.toXml())
     1638            self._pipe.source.dispatch(element)
     1639        else:
     1640            # This stanza is for remote routing
     1641            log.msg("Routing remotely: %r" % element.toXml())
     1642            self._pipe.sink.dispatch(element)
     1643
     1644
     1645    def multicast(self, stanza, recipients):
     1646        """
     1647
     1648        @param stanza: The stanza to send. Its C{element} attribute should
     1649            already be set.
     1650        @type stanza: L{wokkel.generic.Stanza}.
     1651
     1652        @type recipients: iterable of L{JID}.
     1653        """
     1654        if not stanza.element:
     1655            stanza.toElement()
     1656
     1657        for recipient in recipients:
     1658            clone = cloneElement(stanza.element)
     1659            clone['to'] = recipient.full()
     1660            clone.handled = False
     1661            self.routeOrDeliver(clone)
     1662
     1663
     1664    @defer.inlineCallbacks
     1665    def probePresence(self, user):
     1666        """
     1667        Request the presences of all contacts the user has a subscription to.
     1668
     1669        This will send out presence probe stanzas, even to local contacts.
     1670        """
     1671        subscriptions = yield user.roster.getSubscriptions()
     1672        for entity in subscriptions:
     1673            presence = ProbePresence(recipient=entity,
     1674                                     sender=user.entity)
     1675            self.routeOrDeliver(presence.toElement())
Note: See TracBrowser for help on using the repository browser.