source: ralphm-patches/session_manager.patch

Last change on this file was 82:276bc45eb40b, checked in by Ralph Meijer <ralphm@…>, 4 years ago

Move presence probe to session manager.

File size: 33.4 KB
  • new file wokkel/ewokkel.py

    # HG changeset patch
    # Parent d800a4363f602f6ff344181f0d20b5809157b0b1
    # Parent  55eb3f11240d719a97e32eabf3ce03fe346cb8bf
    
    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  
    1111           'DeferredXmlStreamFactory', 'prepareIDNName',
    1212           'Stanza', 'Request', 'ErrorStanza']
    1313
     14import copy
     15
    1416from zope.interface import implements
    1517
    1618from twisted.internet import defer, protocol
     
    5355    return results and results[0] or None
    5456
    5557
     58def cloneElement(element):
     59    """
     60    Make a deep copy of a serialized element.
     61
     62    The returned element is an orphaned deep copy of the given original.
     63
     64    @note: Since the reference to the original parent, if any, is gone,
     65    inherited attributes like C{xml:lang} are not preserved.
     66
     67    @type element: L{domish.Element}.
     68    """
     69    parent = element.parent
     70    element.parent = None
     71    clone = copy.deepcopy(element)
     72    element.parent = parent
     73    return clone
     74
     75
    5676
    5777class FallbackHandler(XMPPHandler):
    5878    """
  • 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_getRosterReceived(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.getRosterReceived(request)
     1496        d.addCallback(gotRoster)
     1497        return d
     1498
     1499
     1500    def test_getRosterReceivedInterested(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.getRosterReceived(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}.
     
    16251737                         "was deprecated in Wokkel 0.8.0; "
    16261738                         "please use MessageProtocol.messageReceived instead.",
    16271739                         warnings[0]['message'])
     1740
     1741
     1742
     1743class UserSessionTest(unittest.TestCase):
     1744    """
     1745    Tests for L{xmppim.UserSession}.
     1746    """
     1747
     1748    def setUp(self):
     1749        self.session = xmppim.UserSession(None)
     1750
     1751
     1752    def test_interface(self):
     1753        """
     1754        UserSession implements IUserSession.
     1755        """
     1756        verify.verifyObject(xmppim.IUserSession, self.session)
     1757
     1758
     1759class UserTest(unittest.TestCase):
     1760    """
     1761    Tests for L{xmppim.User}.
     1762    """
     1763
     1764    def setUp(self):
     1765        self.user = xmppim.User(JID('user@example.org'),
     1766                                xmppim.InMemoryRoster([]))
     1767        self.session = xmppim.UserSession(self.user)
     1768        self.session.bindResource('Home')
     1769
     1770
     1771    @defer.inlineCallbacks
     1772    def test_getPresences(self):
     1773        """
     1774        A contact with a subscription to the user gets its presence.
     1775        """
     1776        contact = xmppim.RosterItem(JID('contact@example.org'),
     1777                                     subscriptionFrom=True)
     1778        self.user.roster.roster[contact.entity] = contact
     1779
     1780        presence = xmppim.AvailabilityPresence()
     1781        self.session.presence = presence
     1782
     1783        presences = yield self.user.getPresences(JID('contact@example.org'))
     1784        self.assertEqual([presence], list(presences))
     1785
     1786        defer.returnValue(None)
     1787
     1788
     1789    def test_getPresencesNoSubscription(self):
     1790        """
     1791        A contact without a subscription raises NotSubscribed.
     1792        """
     1793        contact = xmppim.RosterItem(JID('contact@example.org'),
     1794                                     subscriptionFrom=False)
     1795        self.user.roster.roster[contact.entity] = contact
     1796
     1797        presence = xmppim.AvailabilityPresence()
     1798        self.session.presence = presence
     1799
     1800        d = self.user.getPresences(JID('contact@example.org'))
     1801        self.assertFailure(d, ewokkel.NotSubscribed)
     1802        return d
     1803
     1804
     1805    def test_getPresencesUnknown(self):
     1806        """
     1807        A contact with a subscription to the user gets its presence.
     1808        """
     1809        presence = xmppim.AvailabilityPresence()
     1810        self.session.presence = presence
     1811        d = self.user.getPresences(JID('unknown@example.org'))
     1812        self.assertFailure(d, ewokkel.NoSuchContact)
     1813        return d
     1814
     1815
     1816class AnonymousRealmTest(unittest.TestCase):
     1817    """
     1818    Tests for L{xmppim.AnonymousRealm}.
     1819    """
     1820
     1821    def setUp(self):
     1822        self.realm = xmppim.AnonymousRealm('example.org')
     1823
     1824
     1825    def test_interface(self):
     1826        """
     1827        AnonymousRealm implements IRealm.
     1828        """
     1829        verify.verifyObject(IRealm, self.realm)
     1830
     1831
     1832    def test_requestAvatarAnonymous(self):
     1833        """
     1834        An anonymous avatar ID yields a generated JID.
     1835        """
     1836        def gotAvatar(result):
     1837            def gotUser(user):
     1838                self.assertIdentical(user, avatar.user)
     1839
     1840            iface, avatar, logout = result
     1841            self.assertNotIdentical(None, avatar.user)
     1842            self.assertNotIdentical(None, avatar.user.entity)
     1843            self.assertNotIdentical(None, avatar.user.entity.host)
     1844            self.assertEqual(u'example.org', avatar.user.entity.host)
     1845
     1846            d = self.realm.lookupUser(avatar.user.entity)
     1847            d.addCallback(gotUser)
     1848            return d
     1849
     1850        avatarID = checkers.ANONYMOUS
     1851        d = self.realm.requestAvatar(avatarID, None, xmppim.IUserSession)
     1852        d.addCallback(gotAvatar)
     1853        return d
     1854
     1855
     1856    def test_requestAvatarAnonymousDifferent(self):
     1857        """
     1858        Requesting two anonymous avatar IDs yields different JIDs.
     1859        """
     1860        def gotAvatar1(result):
     1861            iface, avatar1, logout = result
     1862
     1863            def gotAvatar2(result):
     1864                iface, avatar2, logout = result
     1865                self.assertNotIdentical(avatar1, avatar2)
     1866                self.assertNotIdentical(avatar1.user, avatar2.user)
     1867                self.assertNotEqual(avatar1.user.entity,
     1868                                    avatar2.user.entity)
     1869
     1870            d = self.realm.requestAvatar(avatarID, None, xmppim.IUserSession)
     1871            d.addCallback(gotAvatar2)
     1872            return d
     1873
     1874        avatarID = checkers.ANONYMOUS
     1875        d = self.realm.requestAvatar(avatarID, None, xmppim.IUserSession)
     1876        d.addCallback(gotAvatar1)
     1877        return d
     1878
     1879
     1880    def test_logout(self):
     1881        """
     1882        When the logout function is called, the user is removed from the realm.
     1883        """
     1884        def gotAvatar(result):
     1885            iface, avatar, logout = result
     1886
     1887            logout()
     1888
     1889            entity = avatar.user.entity
     1890            d = self.realm.lookupUser(entity)
     1891            self.assertFailure(d, ewokkel.NoSuchUser)
     1892            return d
     1893
     1894        avatarID = checkers.ANONYMOUS
     1895        d = self.realm.requestAvatar(avatarID, None, xmppim.IUserSession)
     1896        d.addCallback(gotAvatar)
     1897        return d
     1898
     1899
     1900
     1901class StaticRealmTest(unittest.TestCase):
     1902    """
     1903    Tests for L{xmppim.StaticRealmTest}.
     1904    """
     1905
     1906    def setUp(self):
     1907        entity = JID(u'\u00e9lise@example.org')
     1908        self.user = xmppim.User(entity)
     1909        users = {entity: self.user}
     1910        self.realm = xmppim.StaticRealm('example.org', users)
     1911
     1912
     1913    def test_interface(self):
     1914        """
     1915        StaticRealm implements IRealm.
     1916        """
     1917        verify.verifyObject(IRealm, self.realm)
     1918
     1919
     1920    def test_requestAvatar(self):
     1921        """
     1922        A UserSession is initialized and returned from requestAvatar.
     1923        """
     1924        def gotAvatar(result):
     1925            iface, avatar, logout = result
     1926            self.assertIdentical(self.user, avatar.user)
     1927
     1928        avatarID = u'\u00e9lise'.encode('utf-8')
     1929        d = self.realm.requestAvatar(avatarID, None, xmppim.IUserSession)
     1930        d.addCallback(gotAvatar)
     1931        return d
     1932
     1933
     1934    def test_requestAvatarUnknown(self):
     1935        """
     1936        A UserSession is initialized and returned from requestAvatar.
     1937        """
     1938        avatarID = u'nobody'.encode('utf-8')
     1939        d = self.realm.requestAvatar(avatarID, None, xmppim.IUserSession)
     1940        self.assertFailure(d, ecred.LoginDenied)
     1941        return d
     1942
     1943
     1944
     1945class TestRouter(component.Router):
     1946    """
     1947    Router that only records incoming traffic for testing.
     1948    """
     1949
     1950    def __init__(self):
     1951        super(TestRouter, self).__init__()
     1952        self.output = []
     1953
     1954
     1955    def route(self, stanza):
     1956        """
     1957        All routed stanzas are recorded.
     1958        """
     1959        self.output.append(stanza)
     1960
     1961
     1962
     1963class SessionManagerTest(unittest.TestCase):
     1964    """
     1965    Tests for L{xmppim.SessionManager}.
     1966    """
     1967
     1968    def setUp(self):
     1969        self.router = TestRouter()
     1970        self.sessionManager = xmppim.SessionManager(self.router,
     1971                                                    u'example.org')
     1972        self.sessionManager.startService()
     1973
     1974        self.input = []
     1975        def onElement(element):
     1976            self.input.append(element)
     1977
     1978        self.sessionManager.xmlstream.addObserver('/*', onElement)
     1979
     1980
     1981    def test_routeOrDeliverLocal(self):
     1982        """
     1983        Stanzas for local domains are reinjected in to the XML stream.
     1984        """
     1985        element = parseXml("""<presence from='test@example.org'
     1986                                        to='other@example.org'/>""")
     1987        self.sessionManager.xmlstream.send(element)
     1988
     1989        self.assertEqual(1, len(self.input))
     1990        self.assertEqual(0, len(self.router.output))
     1991
     1992
     1993    def test_routeOrDeliverRemote(self):
     1994        """
     1995        Stanzas for other domains are sent to the router.
     1996        """
     1997        element = parseXml("""<presence from='test@example.org'
     1998                                        to='other@example.com'/>""")
     1999        self.sessionManager.xmlstream.send(element)
     2000
     2001        self.assertEqual(0, len(self.input))
     2002        self.assertEqual(1, len(self.router.output))
     2003
     2004
     2005    def test_probePresence(self):
     2006        """
     2007        Presence probes are sent to contacts with a subscription.
     2008        """
     2009        def cb(result):
     2010            self.assertEqual(1, len(self.router.output))
     2011            element = self.router.output[-1]
     2012            self.assertEqual(u'presence',
     2013                             element.name)
     2014            self.assertEqual(u'probe',
     2015                             element.getAttribute(u'type'))
     2016            self.assertEqual(u'contact2@example.com',
     2017                             element.getAttribute(u'to'))
     2018
     2019        contacts = [
     2020            xmppim.RosterItem(JID(u'contact1@example.com'),
     2021                              subscriptionFrom=True,
     2022                              subscriptionTo=False),
     2023            xmppim.RosterItem(JID(u'contact2@example.com'),
     2024                              subscriptionFrom=False,
     2025                              subscriptionTo=True),
     2026            ]
     2027        roster = xmppim.InMemoryRoster(contacts)
     2028        entity = JID(u'user@example.org')
     2029        user = xmppim.User(entity, roster)
     2030
     2031        d = self.sessionManager.probePresence(user)
     2032        d.addCallback(cb)
     2033        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 getRosterReceived(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.
     
    11751244
    11761245            self.onMessage(message.element)
    11771246            return getattr(message, "handled", False)
     1247
     1248
     1249
     1250@implementer(IUserSession)
     1251class UserSession(object):
     1252    """
     1253    An XMPP user session.
     1254
     1255    This represents the session for a connected client after authenticating
     1256    as L{user}.
     1257
     1258    @ivar user: The authenticated user for this session.
     1259    @type user: L{User}
     1260
     1261    @ivar mind: The protocol instance for this session.
     1262    @type mind: L{xmlstream.Xmlstream}
     1263
     1264    @ivar entity: The full JID of the entity after a resource has been
     1265       bound.
     1266    @type entity: L{JID}
     1267
     1268    @ivar connected: Flag that is C{True} while a resource is bound.
     1269    @type connected: L{boolean}
     1270
     1271    @ivar interested: Flag to record that the roster has been requested. When
     1272        L{True}, this session will receive roster pushes.
     1273    @type interested: L{boolean}
     1274
     1275    @ivar presence: Last broadcast presence from the client for this session.
     1276    @type presence: L{AvailabilityPresence}
     1277    """
     1278
     1279    user = None
     1280    mind = None
     1281    entity = None
     1282    connected = False
     1283    interested = False
     1284    presence = None
     1285
     1286    def __init__(self, user):
     1287        self.user = user
     1288
     1289
     1290    def loggedIn(self, realm, mind):
     1291        self.mind = mind
     1292        self.realm = realm
     1293
     1294
     1295    def bindResource(self, resource):
     1296        def cb(entity):
     1297            self.entity = entity
     1298            self.connected = True
     1299            return entity
     1300
     1301        d = self.user.bindResource(self, resource)
     1302        d.addCallback(cb)
     1303        return d
     1304
     1305
     1306    def logout(self):
     1307        self.connected = False
     1308
     1309        if self.entity:
     1310            self.user.unbindResource(self.entity.resource)
     1311
     1312
     1313    def send(self, element):
     1314        """
     1315        Called when the client sends a stanza.
     1316        """
     1317        self.realm.server.routeOrDeliver(element)
     1318
     1319
     1320    def receive(self, element, recipient=None):
     1321        """
     1322        Deliver a stanza to the client.
     1323        """
     1324        self.mind.send(element)
     1325
     1326
     1327    def broadcastPresence(self, presence):
     1328        doProbe = presence.available and not self.presence
     1329
     1330        if presence.available:
     1331            self.presence = presence
     1332        else:
     1333            self.presence = None
     1334
     1335        self.user.broadcastPresence(presence)
     1336
     1337        if doProbe:
     1338            self.user.probePresence(self)
     1339
     1340
     1341
     1342class User(object):
     1343    """
     1344    An XMPP user account.
     1345
     1346    @ivar entity: The JID of the user.
     1347    @type entity: L{JID}
     1348
     1349    @ivar roster: The user's roster.
     1350
     1351    @ivar sessions: The currently connected sessions for this user, indexed by
     1352        resource.
     1353    @type sessions: L{dict}
     1354
     1355    @ivar probeSent: Flag that is C{True} if presence probes have been sent
     1356        out for this user. This is only done on the initial presence broadcast
     1357        of the first available resource. Subsequent resources will get the
     1358        presences stored in L{contactPresences}.
     1359    @type probeSent: L{boolean}
     1360
     1361    @ivar contactPresences: Cached presences of contacts as a mapping of L{JID}
     1362        to L{AvailabilityPresence}.
     1363    @type contactPresences: L{dict}
     1364
     1365    @ivar realm: The realm that provided this user object.
     1366    @type realm: L{IRealm} provider
     1367    """
     1368
     1369    realm = None
     1370
     1371    def __init__(self, entity, roster=None):
     1372        self.entity = entity
     1373        self.roster = roster
     1374        self.sessions = {}
     1375        self.probeSent = False
     1376        self.contactPresences = {}
     1377
     1378
     1379    def bindResource(self, session, resource):
     1380        if not self.sessions:
     1381            self.probeSent = False
     1382
     1383        if resource is None:
     1384            resource = randbytes.secureRandom(8).encode('hex')
     1385        elif resource in self.sessions:
     1386            resource = resource + ' ' + randbytes.secureRandom(8).encode('hex')
     1387
     1388        entity = JID(tuple=(self.entity.user, self.entity.host, resource))
     1389        self.sessions[resource] = session
     1390
     1391        return defer.succeed(entity)
     1392
     1393
     1394    def unbindResource(self, resource):
     1395        del self.sessions[resource]
     1396        return defer.succeed(None)
     1397
     1398
     1399    def deliverIQ(self, stanza):
     1400        try:
     1401            session = self.sessions[stanza.recipient.resource]
     1402        except KeyError:
     1403            raise NoSuchResource()
     1404
     1405        session.receive(stanza.element)
     1406
     1407
     1408    def deliverMessage(self, stanza):
     1409        if stanza.recipient.resource:
     1410            try:
     1411                session = self.sessions[stanza.recipient.resource]
     1412            except KeyError:
     1413                if stanza.stanzaType in ('normal', 'chat', 'headline'):
     1414                    self.deliverMessageAnyResource(stanza)
     1415                else:
     1416                    raise NoSuchResource()
     1417            else:
     1418                session.receive(stanza.element)
     1419        else:
     1420            if stanza.stanzaType == 'groupchat':
     1421                raise NotImplementedError("Groupchat message to the bare JID")
     1422            else:
     1423                self.deliverMessageAnyResource(stanza)
     1424
     1425
     1426    def deliverMessageAnyResource(self, stanza):
     1427        if stanza.stanzaType == 'headline':
     1428            recipients = set()
     1429            for resource, session in self.sessions.iteritems():
     1430                if session.presence.priority >= 0:
     1431                    recipients.add(resource)
     1432        elif stanza.stanzaType in ('chat', 'normal'):
     1433            priorities = {}
     1434            for resource, session in self.sessions.iteritems():
     1435                if not session.presence or not session.presence.available:
     1436                    continue
     1437                priority = session.presence.priority
     1438                if priority >= 0:
     1439                    priorities.setdefault(priority, set()).add(resource)
     1440            if priorities:
     1441                maxPriority = max(priorities.keys())
     1442                recipients = priorities[maxPriority]
     1443            else:
     1444                # No available resource, offline storage not supported
     1445                raise NotImplementedError("Offline storage is not supported")
     1446        else:
     1447            recipients = set()
     1448
     1449        if recipients:
     1450            for resource in recipients:
     1451                session = self.sessions[resource]
     1452                session.receive(stanza.element)
     1453        else:
     1454            # silently discard
     1455            log.msg("Discarding message to %r" % stanza.recipient)
     1456
     1457
     1458    def deliverPresence(self, stanza):
     1459        if not stanza.recipient.resource:
     1460            # TODO: record presence in self.contactPresences for future probes
     1461
     1462            for session in self.sessions.itervalues():
     1463                if session.presence:
     1464                    session.receive(stanza.element)
     1465
     1466
     1467    def probePresence(self, session):
     1468        """
     1469        Probe presences for this user.
     1470
     1471        If this is the first session requesting presence probes, they are
     1472        sent out to the contacts via the realm. After that, the last received
     1473        presences are sent back to the session directly.
     1474        """
     1475        if not self.probeSent:
     1476            # send out probes
     1477            self.contactPresences = {}
     1478            self.realm.server.probePresence(self)
     1479            self.probeSent = True
     1480        else:
     1481            # deliver known contact presences
     1482            for presence in self.contactPresences.itervalues():
     1483                session.receive(presence.element)
     1484
     1485
     1486    @defer.inlineCallbacks
     1487    def broadcastPresence(self, presence):
     1488        """
     1489        Broadcast presence to all subscribed contacts and myself.
     1490        """
     1491        # TODO: set probeSent to False when last session becomes unavailable
     1492        # TODO: save last unavailable presence?
     1493        subscribers = yield self.roster.getSubscribers()
     1494        self.realm.server.multicast(presence, subscribers)
     1495
     1496
     1497    @defer.inlineCallbacks
     1498    def getPresences(self, entity):
     1499        """
     1500        Get presences on behalf of a contact.
     1501
     1502        @param entity: The contact requesting that initiated a presence probe.
     1503        @type entity: L{JID}
     1504
     1505        @return: Deferred that fires with an iterable of
     1506            L{AvailabilityPresence}.
     1507        @rtype: L{defer.Deferred}
     1508
     1509        @raise NotSubscribed: If the contact does not have a presence
     1510            subscription from this user.
     1511        @raise NoSuchContact: If the requestor is not a contact.
     1512        """
     1513        bareEntity = entity.userhostJID()
     1514        item = yield self.roster.getContact(bareEntity)
     1515
     1516        if not item.subscriptionFrom:
     1517            raise NotSubscribed()
     1518
     1519        presences = (session.presence for session in self.sessions.itervalues()
     1520                                      if session.presence)
     1521        defer.returnValue(presences)
     1522        # TODO: send last unavailable or unavailable presence?
     1523
     1524
     1525
     1526@implementer(portal.IRealm)
     1527class BaseRealm(object):
     1528    server = None
     1529
     1530    def __init__(self, domain):
     1531        self.domain = domain
     1532
     1533
     1534    def lookupUser(self, entity):
     1535        raise NotImplementedError()
     1536
     1537
     1538    def createUser(self, entity):
     1539        raise NotImplementedError()
     1540
     1541
     1542    def getUser(self, entity):
     1543        def trapNoSuchUser(failure):
     1544            failure.trap(NoSuchUser)
     1545            return self.createUser(entity)
     1546
     1547        d = self.lookupUser(entity)
     1548        d.addErrback(trapNoSuchUser)
     1549        return d
     1550
     1551
     1552    def logoutFactory(self, session):
     1553        return session.logout
     1554
     1555
     1556    def entityFromAvatarID(self, avatarId):
     1557        localpart = avatarId.decode('utf-8')
     1558        return JID(tuple=(localpart, self.domain, None))
     1559
     1560
     1561    def requestAvatar(self, avatarId, mind, *interfaces):
     1562        if IUserSession not in interfaces:
     1563            raise NotImplementedError(self, interfaces)
     1564
     1565        entity = self.entityFromAvatarID(avatarId)
     1566
     1567        def gotUser(user):
     1568            session = UserSession(user)
     1569            session.loggedIn(self, mind)
     1570            return IUserSession, session, self.logoutFactory(session)
     1571
     1572        d = self.getUser(entity)
     1573        d.addCallback(gotUser)
     1574        return d
     1575
     1576
     1577
     1578class AnonymousRealm(BaseRealm):
     1579
     1580    def __init__(self, domain):
     1581        BaseRealm.__init__(self, domain)
     1582        self.users = {}
     1583
     1584
     1585    def entityFromAvatarID(self, avatarId):
     1586        localpart = randbytes.secureRandom(8).encode('hex')
     1587        return JID(tuple=(localpart, self.domain, None))
     1588
     1589
     1590    def lookupUser(self, entity):
     1591        try:
     1592            user = self.users[entity]
     1593        except KeyError:
     1594            return defer.fail(NoSuchUser(entity))
     1595        return defer.succeed(user)
     1596
     1597
     1598    def createUser(self, entity):
     1599        user = User(entity, InMemoryRoster([]))
     1600        user.realm = self
     1601        self.users[entity] = user
     1602        return defer.succeed(user)
     1603
     1604
     1605    def logoutFactory(self, session):
     1606        def logout():
     1607            session.logout()
     1608            del self.users[session.user.entity]
     1609        return logout
     1610
     1611
     1612
     1613class StaticRealm(BaseRealm):
     1614
     1615    def __init__(self, domain, users):
     1616        BaseRealm.__init__(self, domain)
     1617        for user in users.itervalues():
     1618            user.realm = self
     1619        self.users = users
     1620
     1621
     1622    def lookupUser(self, entity):
     1623        try:
     1624            user = self.users[entity]
     1625        except KeyError:
     1626            return defer.fail(NoSuchUser(entity))
     1627        return defer.succeed(user)
     1628
     1629
     1630    def createUser(self, entity):
     1631        return defer.fail(ecred.LoginDenied("Can't create a new user"))
     1632
     1633
     1634
     1635class SessionManager(InternalComponent):
     1636    """
     1637    Session Manager.
     1638
     1639    @ivar xmlstream: XML Stream to inject incoming stanzas from client
     1640        connections into. Stanzas where the C{'to'} attribute is not set
     1641        or is directed at the local domain are injected as if received on
     1642        the XML Stream (using C{dispatch}), other stanzas are injected as if
     1643        they were sent from the XML Stream (using C{send}).
     1644    """
     1645
     1646    def startService(self):
     1647        InternalComponent.startService(self)
     1648        self.xmlstream.send = self.routeOrDeliver
     1649
     1650
     1651    def routeOrDeliver(self, element):
     1652        """
     1653        Deliver a stanza locally or pass on for routing.
     1654        """
     1655        if (JID(element['to']).host in self.domains):
     1656            # This stanza is for local delivery
     1657            log.msg("Delivering locally: %r" % element.toXml())
     1658            self._pipe.source.dispatch(element)
     1659        else:
     1660            # This stanza is for remote routing
     1661            log.msg("Routing remotely: %r" % element.toXml())
     1662            self._pipe.sink.dispatch(element)
     1663
     1664
     1665    def multicast(self, stanza, recipients):
     1666        """
     1667
     1668        @param stanza: The stanza to send. Its C{element} attribute should
     1669            already be set.
     1670        @type stanza: L{wokkel.generic.Stanza}.
     1671
     1672        @type recipients: iterable of L{JID}.
     1673        """
     1674        if not stanza.element:
     1675            stanza.toElement()
     1676
     1677        for recipient in recipients:
     1678            clone = cloneElement(stanza.element)
     1679            clone['to'] = recipient.full()
     1680            clone.handled = False
     1681            self.routeOrDeliver(clone)
     1682
     1683
     1684    @defer.inlineCallbacks
     1685    def probePresence(self, user):
     1686        """
     1687        Request the presences of all contacts the user has a subscription to.
     1688
     1689        This will send out presence probe stanzas, even to local contacts.
     1690        """
     1691        subscriptions = yield user.roster.getSubscriptions()
     1692        for entity in subscriptions:
     1693            presence = ProbePresence(recipient=entity,
     1694                                     sender=user.entity)
     1695            self.routeOrDeliver(presence.toElement())
Note: See TracBrowser for help on using the repository browser.