source:
ralphm-patches/session_manager.patch
Last change on this file was 82:276bc45eb40b, checked in by Ralph Meijer <ralphm@…>, 6 years ago | |
---|---|
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 """ 5 Exceptions for Wokkel. 6 """ 7 8 9 class WokkelError(Exception): 10 """ 11 Base exception for Wokkel. 12 """ 13 14 class NoSuchContact(WokkelError): 15 """ 16 Raised when the given contact is not present in the user's roster. 17 """ 18 19 class NotSubscribed(WokkelError): 20 """ 21 Raised when the contact does not have a presence subscription to the user. 22 """ 23 24 class NoSuchResource(WokkelError): 25 """ 26 Raised when the given resource is currently not connected. 27 """ 28 29 class 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 11 11 'DeferredXmlStreamFactory', 'prepareIDNName', 12 12 'Stanza', 'Request', 'ErrorStanza'] 13 13 14 import copy 15 14 16 from zope.interface import implements 15 17 16 18 from twisted.internet import defer, protocol … … 53 55 return results and results[0] or None 54 56 55 57 58 def 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 56 76 57 77 class FallbackHandler(XMPPHandler): 58 78 """ -
wokkel/iwokkel.py
diff --git a/wokkel/iwokkel.py b/wokkel/iwokkel.py
a b 996 996 """) 997 997 998 998 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 999 1007 def loggedIn(realm, mind): 1000 1008 """ 1001 1009 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 24 24 25 25 NS_VERSION = 'jabber:iq:version' 26 26 27 class 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 27 48 class VersionHandlerTest(unittest.TestCase): 28 49 """ 29 50 Tests for L{wokkel.generic.VersionHandler}. … … 110 131 <message type='chat' from='other@example.org' to='user@example.org'/> 111 132 """ 112 133 113 stanza = generic.Stanza.fromElement(generic.parseXml(xml)) 134 element = generic.parseXml(xml) 135 stanza = generic.Stanza.fromElement(element) 114 136 self.assertEqual('chat', stanza.stanzaType) 115 137 self.assertEqual(JID('other@example.org'), stanza.sender) 116 138 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) 117 149 118 150 119 151 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 5 5 Tests for L{wokkel.xmppim}. 6 6 """ 7 7 8 from zope.interface import verify 9 10 from twisted.cred import checkers, error as ecred 11 from twisted.cred.portal import IRealm 8 12 from twisted.internet import defer 9 13 from twisted.trial import unittest 10 14 from twisted.words.protocols.jabber import error … … 12 16 from twisted.words.protocols.jabber.xmlstream import toResponse 13 17 from twisted.words.xish import domish, utility 14 18 15 from wokkel import xmppim19 from wokkel import ewokkel, component, xmppim 16 20 from wokkel.generic import ErrorStanza, parseXml 17 21 from wokkel.test.helpers import TestableRequestHandlerMixin, XmlStreamStub 22 from wokkel.subprotocols import IQHandlerMixin 18 23 19 24 NS_XML = 'http://www.w3.org/XML/1998/namespace' 20 25 NS_ROSTER = 'jabber:iq:roster' … … 99 104 self.assertEquals(50, presence.priority) 100 105 101 106 107 102 108 class PresenceProtocolTest(unittest.TestCase): 103 109 """ 104 110 Tests for L{xmppim.PresenceProtocol} … … 1418 1424 1419 1425 1420 1426 1427 class 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 1467 class 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 1421 1533 class MessageTest(unittest.TestCase): 1422 1534 """ 1423 1535 Tests for L{xmppim.Message}. … … 1625 1737 "was deprecated in Wokkel 0.8.0; " 1626 1738 "please use MessageProtocol.messageReceived instead.", 1627 1739 warnings[0]['message']) 1740 1741 1742 1743 class 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 1759 class 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 1816 class 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 1901 class 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 1945 class 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 1963 class 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 12 12 13 13 import warnings 14 14 15 from zope.interface import implementer 16 17 from twisted.cred import error as ecred, portal 15 18 from twisted.internet import defer 19 from twisted.python import log, randbytes 16 20 from twisted.words.protocols.jabber import error 17 21 from twisted.words.protocols.jabber.jid import JID 18 22 from twisted.words.xish import domish 19 23 20 from wokkel.generic import ErrorStanza, Stanza, Request 24 from wokkel.component import InternalComponent 25 from wokkel.ewokkel import NoSuchContact 26 from wokkel.ewokkel import NotSubscribed, NoSuchResource, NoSuchUser 27 from wokkel.iwokkel import IUserSession 28 from wokkel.generic import ErrorStanza, Stanza, Request, cloneElement 21 29 from wokkel.subprotocols import IQHandlerMixin 22 30 from wokkel.subprotocols import XMPPHandler 23 31 from wokkel.subprotocols import asyncObserver … … 1062 1070 1063 1071 1064 1072 1073 class 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 1105 class 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 1065 1134 class Message(Stanza): 1066 1135 """ 1067 1136 A message stanza. … … 1175 1244 1176 1245 self.onMessage(message.element) 1177 1246 return getattr(message, "handled", False) 1247 1248 1249 1250 @implementer(IUserSession) 1251 class 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 1342 class 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) 1527 class 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 1578 class 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 1613 class 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 1635 class 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.