source: ralphm-patches/session_manager.patch @ 73:f574beee3bca

Last change on this file since 73:f574beee3bca was 73:f574beee3bca, checked in by Ralph Meijer <ralphm@…>, 7 years ago

Minor cleanups, improved error message handling, and upstreamed patch.

  • Upstreamed deprecation of prepareIDNName.
  • MessageProtocol? now properly handles error message stanzas.
  • Renamed methods for RosterServerProtocol?.
  • Prefer connectionInitialized for setting up stanza observers.
File size: 34.4 KB
  • new file wokkel/ewokkel.py

    # HG changeset patch
    # Parent 60135baa4e422ab203c780c9093c374481b17a7a
    
    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}.
     
    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 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.
     
    11751244
    11761245            self.onMessage(message.element)
    11771246            return True
     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 probePresence(self):
     1328        self.user.probePresence(self)
     1329
     1330
     1331    def broadcastPresence(self, presence, available):
     1332        if available:
     1333            self.presence = presence
     1334        else:
     1335            self.presence = None
     1336            # TODO: unset probeSent on user?
     1337            # TODO: save last unavailable presence?
     1338
     1339        self.user.broadcastPresence(presence)
     1340
     1341
     1342
     1343class User(object):
     1344    """
     1345    An XMPP user account.
     1346
     1347    @ivar entity: The JID of the user.
     1348    @type entity: L{JID}
     1349
     1350    @ivar roster: The user's roster.
     1351
     1352    @ivar sessions: The currently connected sessions for this user, indexed by
     1353        resource.
     1354    @type sessions: L{dict}
     1355
     1356    @ivar probeSent: Flag that is C{True} if presence probes have been sent
     1357        out for this user. This is only done on the initial presence broadcast
     1358        of the first available resource. Subsequent resources will get the
     1359        presences stored in L{contactPresences}.
     1360    @type probeSent: L{boolean}
     1361
     1362    @ivar contactPresences: Cached presences of contacts as a mapping of L{JID}
     1363        to L{AvailabilityPresence}.
     1364    @type contactPresences: L{dict}
     1365
     1366    @ivar realm: The realm that provided this user object.
     1367    @type realm: L{IRealm} provider
     1368    """
     1369
     1370    realm = None
     1371
     1372    def __init__(self, entity, roster=None):
     1373        self.entity = entity
     1374        self.roster = roster
     1375        self.sessions = {}
     1376        self.probeSent = False
     1377        self.contactPresences = {}
     1378
     1379
     1380    def bindResource(self, session, resource):
     1381        if resource is None:
     1382            resource = randbytes.secureRandom(8).encode('hex')
     1383        elif resource in self.sessions:
     1384            resource = resource + ' ' + randbytes.secureRandom(8).encode('hex')
     1385
     1386        entity = JID(tuple=(self.entity.user, self.entity.host, resource))
     1387        self.sessions[resource] = session
     1388
     1389        return defer.succeed(entity)
     1390
     1391
     1392    def unbindResource(self, resource):
     1393        del self.sessions[resource]
     1394        return defer.succeed(None)
     1395
     1396
     1397    def deliverIQ(self, stanza):
     1398        try:
     1399            session = self.sessions[stanza.recipient.resource]
     1400        except KeyError:
     1401            raise NoSuchResource()
     1402
     1403        session.receive(stanza.element)
     1404
     1405
     1406    def deliverMessage(self, stanza):
     1407        if stanza.recipient.resource:
     1408            try:
     1409                session = self.sessions[stanza.recipient.resource]
     1410            except KeyError:
     1411                if stanza.stanzaType in ('normal', 'chat', 'headline'):
     1412                    self.deliverMessageAnyResource(stanza)
     1413                else:
     1414                    raise NoSuchResource()
     1415            else:
     1416                session.receive(stanza.element)
     1417        else:
     1418            if stanza.stanzaType == 'groupchat':
     1419                raise NotImplementedError("Groupchat message to the bare JID")
     1420            else:
     1421                self.deliverMessageAnyResource(stanza)
     1422
     1423
     1424    def deliverMessageAnyResource(self, stanza):
     1425        if stanza.stanzaType == 'headline':
     1426            recipients = set()
     1427            for resource, session in self.sessions.iteritems():
     1428                if session.presence.priority >= 0:
     1429                    recipients.add(resource)
     1430        elif stanza.stanzaType in ('chat', 'normal'):
     1431            priorities = {}
     1432            for resource, session in self.sessions.iteritems():
     1433                if not session.presence or not session.presence.available:
     1434                    continue
     1435                priority = session.presence.priority
     1436                if priority >= 0:
     1437                    priorities.setdefault(priority, set()).add(resource)
     1438            if priorities:
     1439                maxPriority = max(priorities.keys())
     1440                recipients = priorities[maxPriority]
     1441            else:
     1442                # No available resource, offline storage not supported
     1443                raise NotImplementedError("Offline storage is not supported")
     1444        else:
     1445            recipients = set()
     1446
     1447        if recipients:
     1448            for resource in recipients:
     1449                session = self.sessions[resource]
     1450                session.receive(stanza.element)
     1451        else:
     1452            # silently discard
     1453            log.msg("Discarding message to %r" % stanza.recipient)
     1454
     1455
     1456    def deliverPresence(self, stanza):
     1457        if not stanza.recipient.resource:
     1458            # record
     1459
     1460            for session in self.sessions.itervalues():
     1461                if session.presence:
     1462                    session.receive(stanza.element)
     1463
     1464
     1465    def probePresence(self, session):
     1466        """
     1467        Probe presences for this user.
     1468
     1469        If this is the first session requesting presence probes, they are
     1470        sent out to the contacts via the realm. After that, the last received
     1471        presences are sent back to the session directly.
     1472        """
     1473        if not self.probeSent:
     1474            # send out probes
     1475            self.contactPresences = {}
     1476            self.realm.server.probePresence(self)
     1477            self.probeSent = True
     1478        else:
     1479            # deliver known contact presences
     1480            for presence in self.contactPresences.itervalues():
     1481                session.receive(presence.element)
     1482
     1483
     1484    @defer.inlineCallbacks
     1485    def broadcastPresence(self, presence):
     1486        """
     1487        Broadcast presence to all subscribed contacts and myself.
     1488        """
     1489        subscribers = yield self.roster.getSubscribers()
     1490        self.realm.server.multicast(presence, subscribers)
     1491
     1492
     1493    @defer.inlineCallbacks
     1494    def getPresences(self, entity):
     1495        """
     1496        Get presences on behalf of a contact.
     1497
     1498        @param entity: The contact requesting that initiated a presence probe.
     1499        @type entity: L{JID}
     1500
     1501        @return: Deferred that fires with an iterable of
     1502            L{AvailabilityPresence}.
     1503        @rtype: L{defer.Deferred}
     1504
     1505        @raise NotSubscribed: If the contact does not have a presence
     1506            subscription from this user.
     1507        @raise NoSuchContact: If the requestor is not a contact.
     1508        """
     1509        bareEntity = entity.userhostJID()
     1510        item = yield self.roster.getContact(bareEntity)
     1511
     1512        if not item.subscriptionFrom:
     1513            raise NotSubscribed()
     1514
     1515        presences = (session.presence for session in self.sessions.itervalues()
     1516                                      if session.presence)
     1517        defer.returnValue(presences)
     1518        # TODO: send last unavailable or unavailable presence?
     1519
     1520
     1521
     1522@implementer(portal.IRealm)
     1523class BaseRealm(object):
     1524    server = None
     1525
     1526    def __init__(self, domain):
     1527        self.domain = domain
     1528
     1529
     1530    def lookupUser(self, entity):
     1531        raise NotImplementedError()
     1532
     1533
     1534    def createUser(self, entity):
     1535        raise NotImplementedError()
     1536
     1537
     1538    def getUser(self, entity):
     1539        def trapNoSuchUser(failure):
     1540            failure.trap(NoSuchUser)
     1541            return self.createUser(entity)
     1542
     1543        d = self.lookupUser(entity)
     1544        d.addErrback(trapNoSuchUser)
     1545        return d
     1546
     1547
     1548    def logoutFactory(self, session):
     1549        return session.logout
     1550
     1551
     1552    def entityFromAvatarID(self, avatarId):
     1553        localpart = avatarId.decode('utf-8')
     1554        return JID(tuple=(localpart, self.domain, None))
     1555
     1556
     1557    def requestAvatar(self, avatarId, mind, *interfaces):
     1558        if IUserSession not in interfaces:
     1559            raise NotImplementedError(self, interfaces)
     1560
     1561        entity = self.entityFromAvatarID(avatarId)
     1562
     1563        def gotUser(user):
     1564            session = UserSession(user)
     1565            session.loggedIn(self, mind)
     1566            return IUserSession, session, self.logoutFactory(session)
     1567
     1568        d = self.getUser(entity)
     1569        d.addCallback(gotUser)
     1570        return d
     1571
     1572
     1573
     1574class AnonymousRealm(BaseRealm):
     1575
     1576    def __init__(self, domain):
     1577        BaseRealm.__init__(self, domain)
     1578        self.users = {}
     1579
     1580
     1581    def entityFromAvatarID(self, avatarId):
     1582        localpart = randbytes.secureRandom(8).encode('hex')
     1583        return JID(tuple=(localpart, self.domain, None))
     1584
     1585
     1586    def lookupUser(self, entity):
     1587        try:
     1588            user = self.users[entity]
     1589        except KeyError:
     1590            return defer.fail(NoSuchUser(entity))
     1591        return defer.succeed(user)
     1592
     1593
     1594    def createUser(self, entity):
     1595        user = User(entity, InMemoryRoster([]))
     1596        user.realm = self
     1597        self.users[entity] = user
     1598        return defer.succeed(user)
     1599
     1600
     1601    def logoutFactory(self, session):
     1602        def logout():
     1603            session.logout()
     1604            del self.users[session.user.entity]
     1605        return logout
     1606
     1607
     1608
     1609class StaticRealm(BaseRealm):
     1610
     1611    def __init__(self, domain, users):
     1612        BaseRealm.__init__(self, domain)
     1613        for user in users.itervalues():
     1614            user.realm = self
     1615        self.users = users
     1616
     1617
     1618    def lookupUser(self, entity):
     1619        try:
     1620            user = self.users[entity]
     1621        except KeyError:
     1622            return defer.fail(NoSuchUser(entity))
     1623        return defer.succeed(user)
     1624
     1625
     1626    def createUser(self, entity):
     1627        return defer.fail(ecred.LoginDenied("Can't create a new user"))
     1628
     1629
     1630
     1631class SessionManager(InternalComponent):
     1632    """
     1633    Session Manager.
     1634
     1635    @ivar xmlstream: XML Stream to inject incoming stanzas from client
     1636        connections into. Stanzas where the C{'to'} attribute is not set
     1637        or is directed at the local domain are injected as if received on
     1638        the XML Stream (using C{dispatch}), other stanzas are injected as if
     1639        they were sent from the XML Stream (using C{send}).
     1640    """
     1641
     1642    def startService(self):
     1643        InternalComponent.startService(self)
     1644        self.xmlstream.send = self.routeOrDeliver
     1645
     1646
     1647    def routeOrDeliver(self, element):
     1648        """
     1649        Deliver a stanza locally or pass on for routing.
     1650        """
     1651        if (JID(element['to']).host in self.domains):
     1652            # This stanza is for local delivery
     1653            log.msg("Delivering locally: %r" % element.toXml())
     1654            self._pipe.source.dispatch(element)
     1655        else:
     1656            # This stanza is for remote routing
     1657            log.msg("Routing remotely: %r" % element.toXml())
     1658            self._pipe.sink.dispatch(element)
     1659
     1660
     1661    def multicast(self, stanza, recipients):
     1662        """
     1663
     1664        @param stanza: The stanza to send. Its C{element} attribute should
     1665            already be set.
     1666        @type stanza: L{wokkel.generic.Stanza}.
     1667
     1668        @type recipients: iterable of L{JID}.
     1669        """
     1670        if not stanza.element:
     1671            stanza.toElement()
     1672
     1673        for recipient in recipients:
     1674            clone = cloneElement(stanza.element)
     1675            clone['to'] = recipient.full()
     1676            clone.handled = False
     1677            self.routeOrDeliver(clone)
     1678
     1679
     1680    @defer.inlineCallbacks
     1681    def probePresence(self, user):
     1682        """
     1683        Request the presences of all contacts the user has a subscription to.
     1684
     1685        This will send out presence probe stanzas, even to local contacts.
     1686        """
     1687        subscriptions = yield user.roster.getSubscriptions()
     1688        for entity in subscriptions:
     1689            presence = ProbePresence(recipient=entity,
     1690                                     sender=user.entity)
     1691            self.routeOrDeliver(presence.toElement())
Note: See TracBrowser for help on using the repository browser.