[51] | 1 | Add server side support for the roster protocol. |
---|
| 2 | |
---|
[72] | 3 | * Implements roster get by calling `getRoster` and using the returned |
---|
| 4 | `Roster` to send back the roster. |
---|
[54] | 5 | |
---|
| 6 | TODO: |
---|
| 7 | * Add support for roster sets? |
---|
| 8 | |
---|
[62] | 9 | diff --git a/wokkel/test/test_xmppim.py b/wokkel/test/test_xmppim.py |
---|
| 10 | --- a/wokkel/test/test_xmppim.py |
---|
| 11 | +++ b/wokkel/test/test_xmppim.py |
---|
[72] | 12 | @@ -1339,6 +1339,85 @@ |
---|
| 13 | |
---|
| 14 | |
---|
| 15 | |
---|
[51] | 16 | +class RosterServerProtocolTest(unittest.TestCase, TestableRequestHandlerMixin): |
---|
| 17 | + """ |
---|
[62] | 18 | + Tests for L{xmppim.RosterServerProtocol}. |
---|
[51] | 19 | + """ |
---|
| 20 | + |
---|
| 21 | + def setUp(self): |
---|
| 22 | + self.stub = XmlStreamStub() |
---|
[62] | 23 | + self.service = xmppim.RosterServerProtocol() |
---|
[51] | 24 | + self.service.makeConnection(self.stub.xmlstream) |
---|
| 25 | + self.service.connectionInitialized() |
---|
| 26 | + |
---|
| 27 | + |
---|
[73] | 28 | + def test_getRosterReceived(self): |
---|
[51] | 29 | + """ |
---|
| 30 | + A roster get request should trigger getRoster with the request. |
---|
| 31 | + """ |
---|
| 32 | + xml = """ |
---|
[73] | 33 | + <iq type='get' from='user@example.org/Home'> |
---|
[51] | 34 | + <query xmlns='jabber:iq:roster'/> |
---|
| 35 | + </iq> |
---|
| 36 | + """ |
---|
| 37 | + |
---|
[73] | 38 | + def getRosterReceived(request): |
---|
| 39 | + self.assertEqual(JID('user@example.org/Home'), request.sender) |
---|
[62] | 40 | + item = xmppim.RosterItem(JID('other@example.org'), True, False, |
---|
[51] | 41 | + 'The User') |
---|
[72] | 42 | + roster = xmppim.Roster({item.entity: item}) |
---|
| 43 | + return defer.succeed(roster) |
---|
[51] | 44 | + |
---|
| 45 | + def cb(element): |
---|
| 46 | + self.assertEquals('query', element.name) |
---|
| 47 | + self.assertEquals(NS_ROSTER, element.uri) |
---|
| 48 | + itemElements = list(domish.generateElementsQNamed( |
---|
| 49 | + element.children, 'item', NS_ROSTER)) |
---|
| 50 | + self.assertEqual(1, len(itemElements)) |
---|
| 51 | + item = itemElements[0] |
---|
| 52 | + self.assertEquals(u'other@example.org', item.getAttribute('jid')) |
---|
| 53 | + self.assertEquals(u'to', item.getAttribute('subscription')) |
---|
| 54 | + self.assertEquals(u'The User', item.getAttribute('name')) |
---|
| 55 | + |
---|
[73] | 56 | + self.service.getRosterReceived = getRosterReceived |
---|
[51] | 57 | + d = self.handleRequest(xml) |
---|
| 58 | + d.addCallback(cb) |
---|
| 59 | + return d |
---|
| 60 | + |
---|
| 61 | + |
---|
[73] | 62 | + def test_getRosterReceivedNotOverridden(self): |
---|
[51] | 63 | + """ |
---|
| 64 | + If getRoster is not overridden, return feature-not-implemented. |
---|
| 65 | + """ |
---|
| 66 | + xml = """ |
---|
[73] | 67 | + <iq type='get' from='user@example.org/Home'> |
---|
[51] | 68 | + <query xmlns='jabber:iq:roster'/> |
---|
| 69 | + </iq> |
---|
| 70 | + """ |
---|
| 71 | + |
---|
| 72 | + d = self.handleRequest(xml) |
---|
| 73 | + self.assertFailure(d, NotImplementedError) |
---|
| 74 | + return d |
---|
| 75 | + |
---|
| 76 | + |
---|
[73] | 77 | + def test_setRosterReceived(self): |
---|
[51] | 78 | + """ |
---|
| 79 | + Roster set is not yet supported. |
---|
| 80 | + """ |
---|
| 81 | + xml = """ |
---|
[73] | 82 | + <iq type='set' from='user@example.org/Home'> |
---|
[51] | 83 | + <query xmlns='jabber:iq:roster'> |
---|
| 84 | + <item jid='other@example.org' name='Other User'/> |
---|
| 85 | + </query> |
---|
| 86 | + </iq> |
---|
| 87 | + """ |
---|
| 88 | + |
---|
| 89 | + d = self.handleRequest(xml) |
---|
| 90 | + self.assertFailure(d, NotImplementedError) |
---|
| 91 | + return d |
---|
[72] | 92 | + |
---|
| 93 | + |
---|
| 94 | + |
---|
| 95 | class MessageTest(unittest.TestCase): |
---|
| 96 | """ |
---|
| 97 | Tests for L{xmppim.Message}. |
---|
[62] | 98 | diff --git a/wokkel/xmppim.py b/wokkel/xmppim.py |
---|
| 99 | --- a/wokkel/xmppim.py |
---|
| 100 | +++ b/wokkel/xmppim.py |
---|
[72] | 101 | @@ -26,6 +26,7 @@ |
---|
[62] | 102 | NS_ROSTER = 'jabber:iq:roster' |
---|
| 103 | |
---|
| 104 | XPATH_ROSTER_SET = "/iq[@type='set']/query[@xmlns='%s']" % NS_ROSTER |
---|
| 105 | +XPATH_ROSTER_GET = "/iq[@type='get']/query[@xmlns='%s']" % NS_ROSTER |
---|
| 106 | |
---|
| 107 | |
---|
| 108 | |
---|
[72] | 109 | @@ -739,9 +740,9 @@ |
---|
| 110 | |
---|
| 111 | |
---|
| 112 | @classmethod |
---|
| 113 | - def fromElement(Class, element): |
---|
| 114 | + def fromElement(cls, element): |
---|
| 115 | entity = JID(element['jid']) |
---|
| 116 | - item = Class(entity) |
---|
| 117 | + item = cls(entity) |
---|
| 118 | subscription = element.getAttribute('subscription') |
---|
| 119 | if subscription == 'remove': |
---|
| 120 | item.remove = True |
---|
| 121 | @@ -780,21 +781,26 @@ |
---|
| 122 | version = None |
---|
| 123 | rosterSet = False |
---|
| 124 | |
---|
[62] | 125 | + |
---|
[72] | 126 | def parseRequest(self, element): |
---|
| 127 | - self.version = element.getAttribute('ver') |
---|
| 128 | - |
---|
| 129 | - for child in element.elements(NS_ROSTER, 'item'): |
---|
| 130 | - self.item = RosterItem.fromElement(child) |
---|
| 131 | + roster = Roster.fromElement(element) |
---|
| 132 | + self.version = roster.version |
---|
| 133 | + for item in roster.itervalues(): |
---|
| 134 | + self.item = item |
---|
| 135 | break |
---|
| 136 | + return roster |
---|
| 137 | |
---|
| 138 | |
---|
| 139 | def toElement(self): |
---|
| 140 | element = Request.toElement(self) |
---|
| 141 | - query = element.addElement((NS_ROSTER, 'query')) |
---|
| 142 | - if self.version is not None: |
---|
| 143 | - query['ver'] = self.version |
---|
[62] | 144 | + |
---|
[72] | 145 | + roster = Roster() |
---|
| 146 | + roster.version = self.version |
---|
| 147 | if self.item: |
---|
| 148 | - query.addChild(self.item.toElement(rosterSet=self.rosterSet)) |
---|
| 149 | + roster[self.item.entity] = self.item |
---|
[62] | 150 | + |
---|
[72] | 151 | + element.addChild(roster.toElement(rosterSet=self.rosterSet)) |
---|
| 152 | + |
---|
| 153 | return element |
---|
| 154 | |
---|
| 155 | |
---|
| 156 | @@ -812,7 +818,7 @@ |
---|
| 157 | |
---|
| 158 | class Roster(dict): |
---|
| 159 | """ |
---|
| 160 | - In-memory roster container. |
---|
| 161 | + Roster container. |
---|
| 162 | |
---|
| 163 | This provides a roster as a mapping from L{JID} to L{RosterItem}. If |
---|
| 164 | roster versioning is used, the C{version} attribute holds the version |
---|
| 165 | @@ -825,6 +831,28 @@ |
---|
| 166 | version = None |
---|
| 167 | |
---|
| 168 | |
---|
| 169 | + @classmethod |
---|
| 170 | + def fromElement(cls, element): |
---|
| 171 | + roster = cls() |
---|
| 172 | + roster.version = element.getAttribute('ver') |
---|
| 173 | + for element in element.elements(NS_ROSTER, 'item'): |
---|
| 174 | + item = RosterItem.fromElement(element) |
---|
| 175 | + roster[item.entity] = item |
---|
| 176 | + return roster |
---|
| 177 | + |
---|
| 178 | + |
---|
| 179 | + def toElement(self, rosterSet=False): |
---|
| 180 | + element = domish.Element((NS_ROSTER, 'query')) |
---|
| 181 | + |
---|
| 182 | + if self.version: |
---|
| 183 | + element['ver'] = self.version |
---|
| 184 | + |
---|
| 185 | + for item in self.itervalues(): |
---|
| 186 | + element.addChild(item.toElement(rosterSet)) |
---|
| 187 | + |
---|
| 188 | + return element |
---|
| 189 | + |
---|
| 190 | + |
---|
| 191 | |
---|
| 192 | class RosterClientProtocol(XMPPHandler, IQHandlerMixin): |
---|
| 193 | """ |
---|
| 194 | @@ -898,12 +926,7 @@ |
---|
| 195 | |
---|
| 196 | def processRoster(result): |
---|
| 197 | if result.query is not None: |
---|
| 198 | - roster = Roster() |
---|
| 199 | - roster.version = result.query.getAttribute('ver') |
---|
| 200 | - for element in result.query.elements(NS_ROSTER, 'item'): |
---|
| 201 | - item = RosterItem.fromElement(element) |
---|
| 202 | - roster[item.entity] = item |
---|
| 203 | - return roster |
---|
| 204 | + return Roster.fromElement(result.query) |
---|
| 205 | else: |
---|
| 206 | return None |
---|
| 207 | |
---|
| 208 | @@ -1002,6 +1025,43 @@ |
---|
| 209 | |
---|
| 210 | |
---|
| 211 | |
---|
[62] | 212 | +class RosterServerProtocol(XMPPHandler, IQHandlerMixin): |
---|
| 213 | + """ |
---|
| 214 | + XMPP subprotocol handler for the roster, server side. |
---|
| 215 | + """ |
---|
| 216 | + |
---|
| 217 | + iqHandlers = { |
---|
| 218 | + XPATH_ROSTER_GET: '_onRosterGet', |
---|
| 219 | + # XPATH_ROSTER_SET: '_onRosterSet', |
---|
| 220 | + } |
---|
| 221 | + |
---|
| 222 | + def connectionInitialized(self): |
---|
| 223 | + self.xmlstream.addObserver(XPATH_ROSTER_GET, self.handleRequest) |
---|
| 224 | + self.xmlstream.addObserver(XPATH_ROSTER_SET, self.handleRequest) |
---|
| 225 | + |
---|
| 226 | + |
---|
[72] | 227 | + def _onRosterGet(self, iq): |
---|
| 228 | + request = RosterRequest.fromElement(iq) |
---|
[62] | 229 | + |
---|
[72] | 230 | + def toResponse(roster): |
---|
| 231 | + return roster.toElement() |
---|
[62] | 232 | + |
---|
[73] | 233 | + d = self.getRosterReceived(request) |
---|
[72] | 234 | + d.addCallback(toResponse) |
---|
[62] | 235 | + return d |
---|
| 236 | + |
---|
| 237 | + |
---|
[73] | 238 | + def getRosterReceived(self, request): |
---|
[72] | 239 | + """ |
---|
| 240 | + Called when the roster is requested. |
---|
| 241 | + |
---|
| 242 | + @returns: Deferred that fires with a L{Roster}. |
---|
| 243 | + @rtype: L{defer.Deferred} |
---|
| 244 | + """ |
---|
[62] | 245 | + raise NotImplementedError() |
---|
[72] | 246 | + |
---|
| 247 | + |
---|
| 248 | + |
---|
| 249 | class Message(Stanza): |
---|
| 250 | """ |
---|
| 251 | A message stanza. |
---|