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