source:
ralphm-patches/session_manager.patch
@
75:9b7b8b99da61
Last change on this file since 75:9b7b8b99da61 was 75:9b7b8b99da61, checked in by Ralph Meijer <ralphm@…>, 7 years ago | |
---|---|
File size: 34.5 KB |
-
new file wokkel/ewokkel.py
# HG changeset patch # Parent d800a4363f602f6ff344181f0d20b5809157b0b1 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 7 7 Generic XMPP protocol helpers. 8 8 """ 9 9 10 import copy 11 10 12 from zope.interface import implements 11 13 12 14 from twisted.internet import defer, protocol … … 66 68 return rootElement 67 69 68 70 71 def 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 69 89 70 90 class FallbackHandler(XMPPHandler): 71 91 """ … … 168 188 """ 169 189 Abstract representation of a stanza. 170 190 191 @ivar recipient: The receiving entity. 192 @type recipient: L{jid.JID} 193 171 194 @ivar sender: The sending entity. 172 195 @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}. 175 208 """ 176 209 177 210 recipient = None … … 179 212 stanzaKind = None 180 213 stanzaID = None 181 214 stanzaType = None 215 element = None 216 182 217 183 218 def __init__(self, recipient=None, sender=None): 184 219 self.recipient = recipient … … 217 252 self.sender = jid.internJID(element['from']) 218 253 if element.hasAttribute('to'): 219 254 self.recipient = jid.internJID(element['to']) 255 self.stanzaKind = element.name 220 256 self.stanzaType = element.getAttribute('type') 221 257 self.stanzaID = element.getAttribute('id') 222 258 … … 242 278 243 279 def toElement(self): 244 280 element = domish.Element((None, self.stanzaKind)) 281 self.element = element 245 282 if self.sender is not None: 246 283 element['from'] = self.sender.full() 247 284 if self.recipient is not None: -
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_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 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 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 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 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 1343 class 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) 1523 class 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 1574 class 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 1609 class 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 1631 class 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.