Changeset 49:537d1413b661 in ralphm-patches
- Timestamp:
- May 25, 2011, 9:50:21 AM (11 years ago)
- Branch:
- default
- Files:
-
- 4 added
- 4 edited
Legend:
- Unmodified
- Added
- Removed
-
roster_item.patch
r36 r49 1 diff -r c9d1bf0b783d wokkel/test/test_xmppim.py 2 --- a/wokkel/test/test_xmppim.py Sun Jan 10 15:01:41 2010 +0100 3 +++ b/wokkel/test/test_xmppim.py Fri Feb 12 19:49:36 2010 +0100 4 @@ -449,6 +449,96 @@ 5 6 7 8 +class RosterItemTest(unittest.TestCase): 9 + """ 10 + Tests for L{xmppim.RosterItem}. 11 + """ 12 + 13 + def test_toElement(self): 14 + item = xmppim.RosterItem(JID('user@example.org')) 15 + element = item.toElement() 16 + self.assertEquals('item', element.name) 17 + self.assertEquals(NS_ROSTER, element.uri) 18 + self.assertTrue(element.hasAttribute('subscription')) 19 + self.assertFalse(element.hasAttribute('ask')) 20 + self.assertFalse(element.hasAttribute('name')) 21 + self.assertEquals(0, len(list(element.elements()))) 22 + 23 + 24 + def test_toElementJID(self): 25 + item = xmppim.RosterItem(JID('user@example.org')) 26 + element = item.toElement() 27 + self.assertEquals(u'user@example.org', element.getAttribute('jid')) 28 + 29 + 30 + def test_toElementSubscriptionNone(self): 31 + item = xmppim.RosterItem(JID('user@example.org'), 32 + subscriptionTo=False, 33 + subscriptionFrom=False) 34 + element = item.toElement() 35 + self.assertEquals('none', element.getAttribute('subscription')) 36 + 37 + 38 + def test_toElementSubscriptionTo(self): 39 + item = xmppim.RosterItem(JID('user@example.org'), 40 + subscriptionTo=True, 41 + subscriptionFrom=False) 42 + element = item.toElement() 43 + self.assertEquals('to', element.getAttribute('subscription')) 44 + 45 + 46 + def test_toElementSubscriptionFrom(self): 47 + item = xmppim.RosterItem(JID('user@example.org'), 48 + subscriptionTo=False, 49 + subscriptionFrom=True) 50 + element = item.toElement() 51 + self.assertEquals('from', element.getAttribute('subscription')) 52 + 53 + 54 + def test_toElementSubscriptionBoth(self): 55 + item = xmppim.RosterItem(JID('user@example.org'), 56 + subscriptionTo=True, 57 + subscriptionFrom=True) 58 + element = item.toElement() 59 + self.assertEquals('both', element.getAttribute('subscription')) 60 + 61 + 62 + def test_toElementRemove(self): 63 + item = xmppim.RosterItem(JID('user@example.org'), 64 + remove=True) 65 + element = item.toElement() 66 + self.assertEquals('remove', element.getAttribute('subscription')) 67 + 68 + 69 + def test_toElementAsk(self): 70 + item = xmppim.RosterItem(JID('user@example.org'), 71 + ask=True) 72 + element = item.toElement() 73 + self.assertEquals('subscribe', element.getAttribute('ask')) 74 + 75 + 76 + def test_toElementName(self): 77 + item = xmppim.RosterItem(JID('user@example.org'), 78 + name='Joe User') 79 + element = item.toElement() 80 + self.assertEquals(u'Joe User', element.getAttribute('name')) 81 + 82 + 83 + def test_toElementGroups(self): 84 + groups = set(['Friends', 'Jabber']) 85 + item = xmppim.RosterItem(JID('user@example.org'), 86 + groups=groups) 87 + 88 + element = item.toElement() 89 + foundGroups = set() 90 + for child in element.elements(): 91 + if child.uri == NS_ROSTER and child.name == 'group': 92 + foundGroups.add(unicode(child)) 93 + 94 + self.assertEqual(groups, foundGroups) 95 + 96 + 97 + 98 class RosterClientProtocolTest(unittest.TestCase): 1 diff -r c91f18811c37 wokkel/im.py 2 --- a/wokkel/im.py Mon May 23 18:19:44 2011 +0200 3 +++ b/wokkel/im.py Wed May 25 09:24:29 2011 +0200 4 @@ -7,21 +7,27 @@ 5 XMPP IM protocol support. 6 7 This module provides generic implementations for the protocols defined in 8 -U{RFC 3921<http://www.xmpp.org/rfcs/rfc3921.html>} (XMPP IM). 9 - 10 -All of it should eventually move to Twisted. 11 +U{RFC 6121<http://www.xmpp.org/rfcs/rfc6121.html>} (XMPP IM). 12 """ 13 14 +import warnings 15 + 16 +from twisted.internet import defer 17 from twisted.words.protocols.jabber.jid import JID, internJID 18 +from twisted.words.protocols.jabber import error 19 from twisted.words.xish import domish 20 21 from wokkel.compat import IQ 22 from wokkel.generic import ErrorStanza, Stanza 23 -from wokkel.subprotocols import XMPPHandler 24 +from wokkel.subprotocols import IQHandlerMixin, XMPPHandler 25 26 NS_XML = 'http://www.w3.org/XML/1998/namespace' 27 NS_ROSTER = 'jabber:iq:roster' 28 29 +XPATH_ROSTER_SET = "/iq[@type='set']/query[@xmlns='%s']" % NS_ROSTER 30 + 31 + 32 + 33 class BasePresence(Stanza): 99 34 """ 100 Tests for L{xmppim.RosterClientProtocol}. 101 diff -r c9d1bf0b783d wokkel/xmppim.py 102 --- a/wokkel/xmppim.py Sun Jan 10 15:01:41 2010 +0100 103 +++ b/wokkel/xmppim.py Fri Feb 12 19:49:36 2010 +0100 104 @@ -588,7 +588,7 @@ 35 Stanza of kind presence. 36 @@ -349,8 +355,8 @@ 37 38 This represents one contact from an XMPP contact list known as roster. 39 40 - @ivar jid: The JID of the contact. 41 - @type jid: L{JID} 42 + @ivar entity: The JID of the contact. 43 + @type entity: L{JID} 44 @ivar name: The optional associated nickname for this contact. 45 @type name: C{unicode} 46 @ivar subscriptionTo: Subscription state to contact's presence. If C{True}, 47 @@ -360,47 +366,126 @@ 105 48 @ivar subscriptionFrom: Contact's subscription state. If C{True}, the 106 49 contact is subscribed to the presence information 107 50 of the roster owner. 108 51 - @type subscriptionTo: C{bool} 52 - @ivar ask: Whether subscription is pending. 53 - @type ask: C{bool} 109 54 + @type subscriptionFrom: C{bool} 110 @ivar ask: Whether subscription is pending. 111 @type ask: C{bool} 55 + @ivar pendingOut: Whether the subscription request to this contact is 56 + pending. 57 + @type pendingOut: C{bool} 112 58 @ivar groups: Set of groups this contact is categorized in. Groups are 113 @@ -596,13 +596,44 @@ 59 represented by an opaque identifier of type C{unicode}. 114 60 @type groups: C{set} 115 61 """ 116 62 117 63 - def __init__(self, jid): 118 + subscriptionStates = {(False, False): 'none', 119 + (True, False): 'to', 120 + (False, True): 'from', 121 + (True, True): 'both'} 122 + 123 + def __init__(self, jid, subscriptionTo=False, subscriptionFrom=False, 124 + ask=None, name=False, groups=None, remove=False): 125 self.jid = jid 64 - self.jid = jid 126 65 - self.name = None 127 66 - self.subscriptionTo = False … … 129 68 - self.ask = None 130 69 - self.groups = set() 70 + __subscriptionStates = {(False, False): None, 71 + (True, False): 'to', 72 + (False, True): 'from', 73 + (True, True): 'both'} 74 75 + def __init__(self, entity, subscriptionTo=False, subscriptionFrom=False, 76 + name=None, groups=None): 77 + self.entity = entity 131 78 + self.subscriptionTo = subscriptionTo 132 79 + self.subscriptionFrom = subscriptionFrom 133 + self.ask = ask134 80 + self.name = name 135 81 + self.groups = groups or set() 136 + self.remove = remove 82 83 + self.pendingOut = False 84 + self.approved = False 85 + self.remove = False 86 87 -class RosterClientProtocol(XMPPHandler): 88 + 89 + def __getJID(self): 90 + warnings.warn("Use RosterItem.entity instead.", DeprecationWarning) 91 + return self.entity 92 + 93 + 94 + def __setJID(self, value): 95 + warnings.warn("Use RosterItem.entity instead.", DeprecationWarning) 96 + self.entity = value 97 + 98 + 99 + jid = property(__getJID, __setJID, doc=""" 100 + JID of the contact. Deprecated in favour of C{entity}.""") 101 + 102 + 103 + def __getAsk(self): 104 + warnings.warn("Use RosterItem.pendingOut instead.", DeprecationWarning) 105 + return self.pendingOut 106 + 107 + 108 + def __setAsk(self, value): 109 + warnings.warn("Use RosterItem.pendingOut instead.", DeprecationWarning) 110 + self.pendingOut = value 111 + 112 + 113 + ask = property(__getAsk, __setAsk, doc=""" 114 + Pending out subscription. Deprecated in favour of C{pendingOut}.""") 137 115 + 138 116 + 139 117 + def toElement(self): 140 118 + element = domish.Element((NS_ROSTER, 'item')) 141 + element['jid'] = self. jid.full()119 + element['jid'] = self.entity.full() 142 120 + 143 121 + if self.remove: 144 122 + subscription = 'remove' 145 123 + else: 146 + subscription = self.subscriptionStates[self.subscriptionTo, 147 + self.subscriptionFrom] 148 + element['subscription'] = subscription 149 + 150 + if self.ask: 151 + element['ask'] = 'subscribe' 152 + if self.name: 153 + element['name'] = self.name 154 + 155 + if self.groups: 156 + for group in self.groups: 157 + element.addElement('group', content=group) 124 + subscription = self.__subscriptionStates[self.subscriptionTo, 125 + self.subscriptionFrom] 126 + 127 + if self.pendingOut: 128 + element['ask'] = u'subscribe' 129 + 130 + if self.name: 131 + element['name'] = self.name 132 + 133 + if self.approved: 134 + element['approved'] = u'true' 135 + 136 + if self.groups: 137 + for group in self.groups: 138 + element.addElement('group', content=group) 139 + 140 + if subscription: 141 + element['subscription'] = subscription 158 142 + 159 143 + return element 160 144 + 161 162 163 class RosterClientProtocol(XMPPHandler): 164 @@ -662,9 +693,8 @@ 145 + 146 + @classmethod 147 + def fromElement(Class, element): 148 + entity = internJID(element['jid']) 149 + item = Class(entity) 150 + subscription = element.getAttribute('subscription') 151 + if subscription == 'remove': 152 + item.remove = True 153 + else: 154 + item.name = element.getAttribute('name') 155 + item.subscriptionTo = subscription in ('to', 'both') 156 + item.subscriptionFrom = subscription in ('from', 'both') 157 + item.pendingOut = element.getAttribute('ask') == 'subscribe' 158 + item.approved = element.getAttribute('approved') in ('true', '1') 159 + for subElement in domish.generateElementsQNamed(element.children, 160 + 'group', NS_ROSTER): 161 + item.groups.add(unicode(subElement)) 162 + return item 163 + 164 + 165 + 166 +class RosterPushIgnored(Exception): 167 + """ 168 + Raised when this entity doesn't want to accept/trust a roster push. 169 + """ 170 + 171 + 172 + 173 +class RosterClientProtocol(XMPPHandler, IQHandlerMixin): 174 """ 175 Client side XMPP roster protocol. 176 """ 177 178 + iqHandlers = {XPATH_ROSTER_SET: "_onRosterSet"} 179 + 180 def connectionInitialized(self): 181 - ROSTER_SET = "/iq[@type='set']/query[@xmlns='%s']" % NS_ROSTER 182 - self.xmlstream.addObserver(ROSTER_SET, self._onRosterSet) 183 - 184 - 185 - def _parseRosterItem(self, element): 186 - jid = internJID(element['jid']) 187 - item = RosterItem(jid) 188 - item.name = element.getAttribute('name') 189 - subscription = element.getAttribute('subscription') 190 - item.subscriptionTo = subscription in ('to', 'both') 191 - item.subscriptionFrom = subscription in ('from', 'both') 192 - item.ask = element.getAttribute('ask') == 'subscribe' 193 - for subElement in domish.generateElementsQNamed(element.children, 194 - 'group', NS_ROSTER): 195 - item.groups.add(unicode(subElement)) 196 - 197 - return item 198 + self.xmlstream.addObserver(XPATH_ROSTER_SET, self.handleRequest) 199 200 201 def getRoster(self): 202 @@ -415,8 +500,8 @@ 203 roster = {} 204 for element in domish.generateElementsQNamed(result.query.children, 205 'item', NS_ROSTER): 206 - item = self._parseRosterItem(element) 207 - roster[item.jid.userhost()] = item 208 + item = RosterItem.fromElement(element) 209 + roster[item.entity] = item 210 211 return roster 212 213 @@ -437,32 +522,37 @@ 165 214 """ 166 215 iq = IQ(self.xmlstream, 'set') … … 169 218 - item['jid'] = entity.full() 170 219 - item['subscription'] = 'remove' 171 + item = RosterItem(entity, remove=True) 220 + item = RosterItem(entity) 221 + item.remove = True 172 222 + iq.query.addChild(item.toElement()) 173 223 return iq.send() 174 224 175 225 226 def _onRosterSet(self, iq): 227 - if iq.handled or \ 228 - iq.hasAttribute('from') and iq['from'] != self.xmlstream: 229 - return 230 - 231 - iq.handled = True 232 + def eb(failure): 233 + failure.trap(RosterPushIgnored) 234 + raise error.StanzaError('service-unavailable') 235 236 itemElement = iq.query.item 237 + item = RosterItem.fromElement(iq.query.item) 238 239 - if unicode(itemElement['subscription']) == 'remove': 240 - self.onRosterRemove(internJID(itemElement['jid'])) 241 + if item.remove: 242 + d = defer.maybeDeferred(self.onRosterRemove, item.entity) 243 else: 244 - item = self._parseRosterItem(iq.query.item) 245 - self.onRosterSet(item) 246 + d = defer.maybeDeferred(self.onRosterSet, item) 247 + 248 + d.addErrback(eb) 249 + return d 250 251 252 def onRosterSet(self, item): 253 """ 254 Called when a roster push for a new or update item was received. 255 256 + Raise L{RosterPushIgnored} when not accepting this roster push 257 + (directly or via Deferred). This will result in a 258 + L{'service-unavailable'} error being sent in return. 259 + 260 @param item: The pushed roster item. 261 @type item: L{RosterItem} 262 """ 263 @@ -472,6 +562,10 @@ 264 """ 265 Called when a roster push for the removal of an item was received. 266 267 + Raise L{RosterPushIgnored} when not accepting this roster push 268 + (directly or via Deferred). This will result in a 269 + L{'service-unavailable'} error being sent in return. 270 + 271 @param entity: The entity for which the roster item has been removed. 272 @type entity: L{JID} 273 """ 274 diff -r c91f18811c37 wokkel/test/test_im.py 275 --- a/wokkel/test/test_im.py Mon May 23 18:19:44 2011 +0200 276 +++ b/wokkel/test/test_im.py Wed May 25 09:24:29 2011 +0200 277 @@ -7,13 +7,14 @@ 278 279 from twisted.internet import defer 280 from twisted.trial import unittest 281 +from twisted.words.protocols.jabber import error 282 from twisted.words.protocols.jabber.jid import JID 283 from twisted.words.protocols.jabber.xmlstream import toResponse 284 from twisted.words.xish import domish, utility 285 286 from wokkel import im 287 from wokkel.generic import ErrorStanza, parseXml 288 -from wokkel.test.helpers import XmlStreamStub 289 +from wokkel.test.helpers import TestableRequestHandlerMixin, XmlStreamStub 290 291 NS_XML = 'http://www.w3.org/XML/1998/namespace' 292 NS_ROSTER = 'jabber:iq:roster' 293 @@ -389,23 +390,356 @@ 294 295 296 297 -class RosterClientProtocolTest(unittest.TestCase): 298 +class RosterItemTest(unittest.TestCase): 299 + """ 300 + Tests for L{im.RosterItem}. 301 + """ 302 + 303 + def test_jidDeprecationGet(self): 304 + """ 305 + Getting the jid attribute works as entity and warns deprecation. 306 + """ 307 + item = im.RosterItem(JID('user@example.org')) 308 + entity = self.assertWarns(DeprecationWarning, 309 + "Use RosterItem.entity instead.", 310 + im.__file__, 311 + getattr, item, 'jid') 312 + self.assertIdentical(entity, item.entity) 313 + 314 + 315 + def test_jidDeprecationSet(self): 316 + """ 317 + Setting the jid attribute works as entity and warns deprecation. 318 + """ 319 + item = im.RosterItem(JID('user@example.org')) 320 + entity = self.assertWarns(DeprecationWarning, 321 + "Use RosterItem.entity instead.", 322 + im.__file__, 323 + setattr, item, 'jid', 324 + JID('other@example.org')) 325 + self.assertEquals(JID('other@example.org'), item.entity) 326 + 327 + 328 + def test_askDeprecationGet(self): 329 + """ 330 + Getting the ask attribute works as entity and warns deprecation. 331 + """ 332 + item = im.RosterItem(JID('user@example.org')) 333 + item.pendingOut = True 334 + ask = self.assertWarns(DeprecationWarning, 335 + "Use RosterItem.pendingOut instead.", 336 + im.__file__, 337 + getattr, item, 'ask') 338 + self.assertTrue(ask) 339 + 340 + 341 + def test_askDeprecationSet(self): 342 + """ 343 + Setting the ask attribute works as entity and warns deprecation. 344 + """ 345 + item = im.RosterItem(JID('user@example.org')) 346 + entity = self.assertWarns(DeprecationWarning, 347 + "Use RosterItem.pendingOut instead.", 348 + im.__file__, 349 + setattr, item, 'ask', 350 + True) 351 + self.assertTrue(item.pendingOut) 352 + 353 + 354 + def test_toElement(self): 355 + item = im.RosterItem(JID('user@example.org')) 356 + element = item.toElement() 357 + self.assertEquals('item', element.name) 358 + self.assertEquals(NS_ROSTER, element.uri) 359 + self.assertFalse(element.hasAttribute('subscription')) 360 + self.assertFalse(element.hasAttribute('ask')) 361 + self.assertFalse(element.hasAttribute('name')) 362 + self.assertFalse(element.hasAttribute('approved')) 363 + self.assertEquals(0, len(list(element.elements()))) 364 + 365 + 366 + def test_toElementMinimal(self): 367 + item = im.RosterItem(JID('user@example.org')) 368 + element = item.toElement() 369 + self.assertEquals(u'user@example.org', element.getAttribute('jid')) 370 + 371 + 372 + def test_toElementSubscriptionNone(self): 373 + item = im.RosterItem(JID('user@example.org'), 374 + subscriptionTo=False, 375 + subscriptionFrom=False) 376 + element = item.toElement() 377 + self.assertIdentical(None, element.getAttribute('subscription')) 378 + 379 + 380 + def test_toElementSubscriptionTo(self): 381 + item = im.RosterItem(JID('user@example.org'), 382 + subscriptionTo=True, 383 + subscriptionFrom=False) 384 + element = item.toElement() 385 + self.assertEquals('to', element.getAttribute('subscription')) 386 + 387 + 388 + def test_toElementSubscriptionFrom(self): 389 + item = im.RosterItem(JID('user@example.org'), 390 + subscriptionTo=False, 391 + subscriptionFrom=True) 392 + element = item.toElement() 393 + self.assertEquals('from', element.getAttribute('subscription')) 394 + 395 + 396 + def test_toElementSubscriptionBoth(self): 397 + item = im.RosterItem(JID('user@example.org'), 398 + subscriptionTo=True, 399 + subscriptionFrom=True) 400 + element = item.toElement() 401 + self.assertEquals('both', element.getAttribute('subscription')) 402 + 403 + 404 + def test_toElementSubscriptionRemove(self): 405 + item = im.RosterItem(JID('user@example.org')) 406 + item.remove = True 407 + element = item.toElement() 408 + self.assertEquals('remove', element.getAttribute('subscription')) 409 + 410 + 411 + def test_toElementAsk(self): 412 + item = im.RosterItem(JID('user@example.org')) 413 + item.pendingOut = True 414 + element = item.toElement() 415 + self.assertEquals('subscribe', element.getAttribute('ask')) 416 + 417 + 418 + def test_toElementName(self): 419 + item = im.RosterItem(JID('user@example.org'), 420 + name='Joe User') 421 + element = item.toElement() 422 + self.assertEquals(u'Joe User', element.getAttribute('name')) 423 + 424 + 425 + def test_toElementGroups(self): 426 + groups = set(['Friends', 'Jabber']) 427 + item = im.RosterItem(JID('user@example.org'), 428 + groups=groups) 429 + 430 + element = item.toElement() 431 + foundGroups = set() 432 + for child in element.elements(): 433 + if child.uri == NS_ROSTER and child.name == 'group': 434 + foundGroups.add(unicode(child)) 435 + 436 + self.assertEqual(groups, foundGroups) 437 + 438 + 439 + def test_toElementApproved(self): 440 + """ 441 + A pre-approved subscription for a roster item has an 'approved' flag. 442 + """ 443 + item = im.RosterItem(JID('user@example.org')) 444 + item.approved = True 445 + element = item.toElement() 446 + self.assertEquals(u'true', element.getAttribute('approved')) 447 + 448 + 449 + def test_fromElementMinimal(self): 450 + """ 451 + A minimal roster item has a reference to the JID of the contact. 452 + """ 453 + 454 + xml = """ 455 + <item xmlns="jabber:iq:roster" 456 + jid="test@example.org"/> 457 + """ 458 + 459 + item = im.RosterItem.fromElement(parseXml(xml)) 460 + self.assertEqual(JID(u"test@example.org"), item.entity) 461 + self.assertIdentical(None, item.name) 462 + self.assertFalse(item.subscriptionTo) 463 + self.assertFalse(item.subscriptionFrom) 464 + self.assertFalse(item.pendingOut) 465 + self.assertFalse(item.approved) 466 + self.assertEquals(set(), item.groups) 467 + 468 + 469 + def test_fromElementName(self): 470 + """ 471 + A roster item may have an optional name. 472 + """ 473 + 474 + xml = """ 475 + <item xmlns="jabber:iq:roster" 476 + jid="test@example.org" 477 + name="Test User"/> 478 + """ 479 + 480 + item = im.RosterItem.fromElement(parseXml(xml)) 481 + self.assertEqual(u"Test User", item.name) 482 + 483 + 484 + def test_fromElementGroups(self): 485 + """ 486 + A roster item may have one or more groups. 487 + """ 488 + 489 + xml = """ 490 + <item xmlns="jabber:iq:roster" 491 + jid="test@example.org"> 492 + <group>Friends</group> 493 + <group>Twisted</group> 494 + </item> 495 + """ 496 + 497 + item = im.RosterItem.fromElement(parseXml(xml)) 498 + self.assertIn(u"Twisted", item.groups) 499 + self.assertIn(u"Friends", item.groups) 500 + 501 + 502 + def test_fromElementSubscriptionNone(self): 503 + """ 504 + Subscription 'none' sets both attributes to False. 505 + """ 506 + 507 + xml = """ 508 + <item xmlns="jabber:iq:roster" 509 + jid="test@example.org" 510 + subscription="none"/> 511 + """ 512 + 513 + item = im.RosterItem.fromElement(parseXml(xml)) 514 + self.assertFalse(item.remove) 515 + self.assertFalse(item.subscriptionTo) 516 + self.assertFalse(item.subscriptionFrom) 517 + 518 + 519 + def test_fromElementSubscriptionTo(self): 520 + """ 521 + Subscription 'to' sets the corresponding attribute to True. 522 + """ 523 + 524 + xml = """ 525 + <item xmlns="jabber:iq:roster" 526 + jid="test@example.org" 527 + subscription="to"/> 528 + """ 529 + 530 + item = im.RosterItem.fromElement(parseXml(xml)) 531 + self.assertFalse(item.remove) 532 + self.assertTrue(item.subscriptionTo) 533 + self.assertFalse(item.subscriptionFrom) 534 + 535 + 536 + def test_fromElementSubscriptionFrom(self): 537 + """ 538 + Subscription 'from' sets the corresponding attribute to True. 539 + """ 540 + 541 + xml = """ 542 + <item xmlns="jabber:iq:roster" 543 + jid="test@example.org" 544 + subscription="from"/> 545 + """ 546 + 547 + item = im.RosterItem.fromElement(parseXml(xml)) 548 + self.assertFalse(item.remove) 549 + self.assertFalse(item.subscriptionTo) 550 + self.assertTrue(item.subscriptionFrom) 551 + 552 + 553 + def test_fromElementSubscriptionBoth(self): 554 + """ 555 + Subscription 'both' sets both attributes to True. 556 + """ 557 + 558 + xml = """ 559 + <item xmlns="jabber:iq:roster" 560 + jid="test@example.org" 561 + subscription="both"/> 562 + """ 563 + 564 + item = im.RosterItem.fromElement(parseXml(xml)) 565 + self.assertFalse(item.remove) 566 + self.assertTrue(item.subscriptionTo) 567 + self.assertTrue(item.subscriptionFrom) 568 + 569 + 570 + def test_fromElementSubscriptionRemove(self): 571 + """ 572 + Subscription 'remove' sets the remove attribute. 573 + """ 574 + 575 + xml = """ 576 + <item xmlns="jabber:iq:roster" 577 + jid="test@example.org" 578 + subscription="remove"/> 579 + """ 580 + 581 + item = im.RosterItem.fromElement(parseXml(xml)) 582 + self.assertTrue(item.remove) 583 + 584 + 585 + def test_fromElementPendingOut(self): 586 + """ 587 + The ask attribute, if set to 'subscription', means pending out. 588 + """ 589 + 590 + xml = """ 591 + <item xmlns="jabber:iq:roster" 592 + jid="test@example.org" 593 + ask="subscribe"/> 594 + """ 595 + 596 + item = im.RosterItem.fromElement(parseXml(xml)) 597 + self.assertTrue(item.pendingOut) 598 + 599 + 600 + def test_fromElementApprovedTrue(self): 601 + """ 602 + The approved attribute (true) signals a pre-approved subscription. 603 + """ 604 + 605 + xml = """ 606 + <item xmlns="jabber:iq:roster" 607 + jid="test@example.org" 608 + approved="true"/> 609 + """ 610 + 611 + item = im.RosterItem.fromElement(parseXml(xml)) 612 + self.assertTrue(item.approved) 613 + 614 + 615 + def test_fromElementApproved1(self): 616 + """ 617 + The approved attribute (1) signals a pre-approved subscription. 618 + """ 619 + 620 + xml = """ 621 + <item xmlns="jabber:iq:roster" 622 + jid="test@example.org" 623 + approved="1"/> 624 + """ 625 + 626 + item = im.RosterItem.fromElement(parseXml(xml)) 627 + self.assertTrue(item.approved) 628 + 629 + 630 + 631 +class RosterClientProtocolTest(unittest.TestCase, TestableRequestHandlerMixin): 632 """ 633 Tests for L{im.RosterClientProtocol}. 634 """ 635 636 def setUp(self): 637 self.stub = XmlStreamStub() 638 - self.protocol = im.RosterClientProtocol() 639 - self.protocol.xmlstream = self.stub.xmlstream 640 - self.protocol.connectionInitialized() 641 + self.service = im.RosterClientProtocol() 642 + self.service.makeConnection(self.stub.xmlstream) 643 + self.service.connectionInitialized() 644 645 646 def test_removeItem(self): 647 """ 648 Removing a roster item is setting an item with subscription C{remove}. 649 """ 650 - d = self.protocol.removeItem(JID('test@example.org')) 651 + d = self.service.removeItem(JID('test@example.org')) 652 653 # Inspect outgoing iq request 654 655 @@ -419,10 +753,117 @@ 656 self.assertEquals(1, len(children)) 657 child = children[0] 658 self.assertEquals('test@example.org', child['jid']) 659 - self.assertEquals('remove', child['subscription']) 660 + self.assertEquals('remove', child.getAttribute('subscription')) 661 662 # Fake successful response 663 664 response = toResponse(iq, 'result') 665 self.stub.send(response) 666 return d 667 + 668 + 669 + def test_getRoster(self): 670 + def cb(roster): 671 + self.assertIn(JID('user@example.org'), roster) 672 + 673 + 674 + d = self.service.getRoster() 675 + d.addCallback(cb) 676 + 677 + # Inspect outgoing iq request 678 + 679 + iq = self.stub.output[-1] 680 + self.assertEquals('get', iq.getAttribute('type')) 681 + self.assertNotIdentical(None, iq.query) 682 + self.assertEquals(NS_ROSTER, iq.query.uri) 683 + 684 + # Fake successful response 685 + response = toResponse(iq, 'result') 686 + query = response.addElement((NS_ROSTER, 'query')) 687 + item = query.addElement('item') 688 + item['jid'] = 'user@example.org' 689 + 690 + self.stub.send(response) 691 + return d 692 + 693 + 694 + def test_onRosterSet(self): 695 + """ 696 + A roster push causes onRosterSet to be called with the parsed item. 697 + """ 698 + xml = """ 699 + <iq type='set'> 700 + <query xmlns='jabber:iq:roster'> 701 + <item jid='user@example.org'/> 702 + </query> 703 + </iq> 704 + """ 705 + 706 + items = [] 707 + 708 + def onRosterSet(item): 709 + items.append(item) 710 + 711 + def cb(result): 712 + self.assertEquals(1, len(items)) 713 + self.assertEquals(JID('user@example.org'), items[0].entity) 714 + 715 + self.service.onRosterSet = onRosterSet 716 + 717 + d = self.handleRequest(xml) 718 + d.addCallback(cb) 719 + return d 720 + 721 + 722 + def test_onRosterSetUntrusted(self): 723 + """ 724 + Roster pushes from untrusted sources will be not be handled. 725 + """ 726 + xml = """ 727 + <iq type='set' from='bad@example.org'> 728 + <query xmlns='jabber:iq:roster'> 729 + <item jid='user@example.org'/> 730 + </query> 731 + </iq> 732 + """ 733 + 734 + def onRosterSet(item): 735 + raise im.RosterPushIgnored() 736 + 737 + def cb(result): 738 + self.assertEquals('service-unavailable', result.condition) 739 + 740 + self.service.onRosterSet = onRosterSet 741 + 742 + d = self.handleRequest(xml) 743 + self.assertFailure(d, error.StanzaError) 744 + d.addCallback(cb) 745 + return d 746 + 747 + 748 + def test_onRosterRemove(self): 749 + """ 750 + A roster push causes onRosterSet to be called with the parsed item. 751 + """ 752 + xml = """ 753 + <iq type='set'> 754 + <query xmlns='jabber:iq:roster'> 755 + <item jid='user@example.org' subscription='remove'/> 756 + </query> 757 + </iq> 758 + """ 759 + 760 + entities = [] 761 + 762 + def onRosterRemove(entity): 763 + entities.append(entity) 764 + 765 + def cb(result): 766 + self.assertEquals([JID('user@example.org')], entities) 767 + 768 + self.service.onRosterRemove = onRosterRemove 769 + 770 + d = self.handleRequest(xml) 771 + d.addCallback(cb) 772 + return d 773 + -
roster_server.patch
r37 r49 1 diff -r 7b9f484b0b44 wokkel/xmppim.py 2 --- a/wokkel/xmppim.py Fri Feb 12 19:49:36 2010 +0100 3 +++ b/wokkel/xmppim.py Sat Feb 13 18:57:26 2010 +0100 4 @@ -12,6 +12,7 @@ 5 All of it should eventually move to Twisted. 6 """ 1 diff -r 3c5fb05162e1 wokkel/im.py 2 --- a/wokkel/im.py Wed May 25 09:39:42 2011 +0200 3 +++ b/wokkel/im.py Wed May 25 09:40:28 2011 +0200 4 @@ -14,7 +14,7 @@ 7 5 6 from twisted.internet import defer 7 from twisted.words.protocols.jabber.jid import JID, internJID 8 -from twisted.words.protocols.jabber import error 8 9 +from twisted.words.protocols.jabber import error, xmlstream 9 from twisted.words.protocols.jabber.jid import JID10 10 from twisted.words.xish import domish 11 11 12 @@ -22,6 +23,9 @@ 13 NS_XML = 'http://www.w3.org/XML/1998/namespace' 12 from wokkel.compat import IQ 13 @@ -25,6 +25,7 @@ 14 14 NS_ROSTER = 'jabber:iq:roster' 15 15 16 +XPATH_ROSTER_GET = "//iq[@type='get']/query[@xmlns='%s']" % NS_ROSTER 17 +XPATH_ROSTER_SET = "//iq[@type='set']/query[@xmlns='%s']" % NS_ROSTER 18 + 19 class Presence(domish.Element): 20 def __init__(self, to=None, type=None): 21 domish.Element.__init__(self, (None, "presence")) 22 @@ -636,6 +640,7 @@ 16 XPATH_ROSTER_SET = "/iq[@type='set']/query[@xmlns='%s']" % NS_ROSTER 17 +XPATH_ROSTER_GET = "/iq[@type='get']/query[@xmlns='%s']" % NS_ROSTER 23 18 24 19 25 20 26 + 27 class RosterClientProtocol(XMPPHandler): 28 """ 29 Client side XMPP roster protocol. 30 @@ -645,6 +650,7 @@ 31 ROSTER_SET = "/iq[@type='set']/query[@xmlns='%s']" % NS_ROSTER 32 self.xmlstream.addObserver(ROSTER_SET, self._onRosterSet) 33 34 + 35 def _parseRosterItem(self, element): 36 jid = JID(element['jid']) 37 item = RosterItem(jid) 38 @@ -659,6 +665,7 @@ 39 40 return item 41 42 + 43 def getRoster(self): 44 """ 45 Retrieve contact list. 46 @@ -713,6 +720,7 @@ 47 item = self._parseRosterItem(iq.query.item) 48 self.onRosterSet(item) 49 50 + 51 def onRosterSet(self, item): 52 """ 53 Called when a roster push for a new or update item was received. 54 @@ -721,6 +729,7 @@ 55 @type item: L{RosterItem} 56 """ 57 58 + 59 def onRosterRemove(self, entity): 60 """ 61 Called when a roster push for the removal of an item was received. 62 @@ -729,6 +738,48 @@ 21 @@ -579,3 +580,43 @@ 22 @param entity: The entity for which the roster item has been removed. 63 23 @type entity: L{JID} 64 24 """ 65 25 + 66 26 + 67 27 + … … 89 49 + iq.handled = True 90 50 + 91 + d = self.getRoster( JID(iq["from"]))51 + d = self.getRoster(internJID(iq["from"])) 92 52 + d.addCallback(self._toRosterReply, iq) 93 53 + d.addErrback(lambda _: error.ErrorStanza('internal-error').toResponse(iq)) … … 103 63 + def getRoster(self, entity): 104 64 + raise NotImplemented 105 +106 +107 +108 class MessageProtocol(XMPPHandler):109 """110 Generic XMPP subprotocol handler for incoming message stanzas. -
series
r48 r49 1 request-tracking.patch 2 request-xmpphandler.patch 3 request-stanza.patch 4 disco-addressing.patch 1 copy_xmppim.patch 2 jid_cleanup.patch 3 roster_item.patch #+c2s 4 roster_item_more.patch 5 roster_server.patch #+c2s 6 xmpp_client_service.patch #+c2s 7 8 request-tracking.patch #+request 9 request-xmpphandler.patch #+request 10 request-stanza.patch #+request 11 disco-addressing.patch #+request 12 13 pubsub_resource_example.patch 14 5 15 disco_warning.patch #-compatible 16 pubsub-item.patch #-compatible 17 6 18 pubsub-default-type-attribute.patch #+deferred 7 pubsub-item.patch #-compatible8 roster_item.patch9 roster_server.patch10 xmpp_client_service.patch11 19 disco_simplify_gatherResults.patch #+deferred 12 20 deprecate_xmpphandler.patch #+deferred -
xmpp_client_service.patch
r41 r49 1 diff -r 62f841ed2a99doc/examples/client_service.tac1 diff -r d7fa09914b70 doc/examples/client_service.tac 2 2 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 3 +++ b/doc/examples/client_service.tac Thu Feb 25 09:47:52 2010 +01003 +++ b/doc/examples/client_service.tac Wed May 25 09:46:06 2011 +0200 4 4 @@ -0,0 +1,74 @@ 5 5 +from twisted.application import service, strports … … 77 77 + 78 78 +sessionManager.connectionManager = c2sFactory 79 diff -r 62f841ed2a99wokkel/client.py80 --- a/wokkel/client.py Sat Feb 13 18:57:26 2010 +010081 +++ b/wokkel/client.py Thu Feb 25 09:47:52 2010 +010079 diff -r d7fa09914b70 wokkel/client.py 80 --- a/wokkel/client.py Wed May 25 09:40:28 2011 +0200 81 +++ b/wokkel/client.py Wed May 25 09:46:06 2011 +0200 82 82 @@ -10,13 +10,27 @@ 83 83 that should probably eventually move there. … … 470 470 + else: 471 471 + raise Exception("No connection manager set") 472 diff -r 62f841ed2a99wokkel/component.py473 --- a/wokkel/component.py Sat Feb 13 18:57:26 2010 +0100474 +++ b/wokkel/component.py Thu Feb 25 09:47:52 2010 +0100472 diff -r d7fa09914b70 wokkel/component.py 473 --- a/wokkel/component.py Wed May 25 09:40:28 2011 +0200 474 +++ b/wokkel/component.py Wed May 25 09:46:06 2011 +0200 475 475 @@ -313,12 +313,25 @@ 476 476 """ … … 501 501 502 502 503 diff -r 62f841ed2a99 wokkel/xmppim.py 504 --- a/wokkel/xmppim.py Sat Feb 13 18:57:26 2010 +0100 505 +++ b/wokkel/xmppim.py Thu Feb 25 09:47:52 2010 +0100 506 @@ -12,8 +12,11 @@ 507 All of it should eventually move to Twisted. 508 """ 509 510 +import copy 511 + 512 +from twisted.python import log 513 from twisted.words.protocols.jabber import error, xmlstream 514 -from twisted.words.protocols.jabber.jid import JID 515 +from twisted.words.protocols.jabber.jid import JID, internJID 516 from twisted.words.xish import domish 517 518 from wokkel.compat import IQ 519 @@ -85,7 +88,7 @@ 520 handler(presence) 521 522 def _onPresenceAvailable(self, presence): 523 - entity = JID(presence["from"]) 524 + entity = internJID(presence["from"]) 525 526 show = unicode(presence.show or '') 527 if show not in ['away', 'xa', 'chat', 'dnd']: 528 @@ -101,23 +104,23 @@ 529 self.availableReceived(entity, show, statuses, priority) 530 531 def _onPresenceUnavailable(self, presence): 532 - entity = JID(presence["from"]) 533 + entity = internJID(presence["from"]) 534 535 statuses = self._getStatuses(presence) 536 537 self.unavailableReceived(entity, statuses) 538 539 def _onPresenceSubscribed(self, presence): 540 - self.subscribedReceived(JID(presence["from"])) 541 + self.subscribedReceived(internJID(presence["from"])) 542 543 def _onPresenceUnsubscribed(self, presence): 544 - self.unsubscribedReceived(JID(presence["from"])) 545 + self.unsubscribedReceived(internJID(presence["from"])) 546 547 def _onPresenceSubscribe(self, presence): 548 - self.subscribeReceived(JID(presence["from"])) 549 + self.subscribeReceived(internJID(presence["from"])) 550 551 def _onPresenceUnsubscribe(self, presence): 552 - self.unsubscribeReceived(JID(presence["from"])) 553 + self.unsubscribeReceived(internJID(presence["from"])) 554 555 556 def availableReceived(self, entity, show=None, statuses=None, priority=0): 557 @@ -125,7 +128,7 @@ 558 Available presence was received. 559 560 @param entity: entity from which the presence was received. 561 - @type entity: {JID} 562 + @type entity: L{JID} 563 @param show: detailed presence information. One of C{'away'}, C{'xa'}, 564 C{'chat'}, C{'dnd'} or C{None}. 565 @type show: C{str} or C{NoneType} 566 @@ -143,7 +146,7 @@ 567 Unavailable presence was received. 568 569 @param entity: entity from which the presence was received. 570 - @type entity: {JID} 571 + @type entity: L{JID} 572 @param statuses: dictionary of natural language descriptions of the 573 availability status, keyed by the language 574 descriptor. A status without a language 575 @@ -156,7 +159,7 @@ 576 Subscription approval confirmation was received. 577 578 @param entity: entity from which the confirmation was received. 579 - @type entity: {JID} 580 + @type entity: L{JID} 581 """ 582 583 def unsubscribedReceived(self, entity): 584 @@ -164,7 +167,7 @@ 585 Unsubscription confirmation was received. 586 587 @param entity: entity from which the confirmation was received. 588 - @type entity: {JID} 589 + @type entity: L{JID} 590 """ 591 592 def subscribeReceived(self, entity): 593 @@ -172,7 +175,7 @@ 594 Subscription request was received. 595 596 @param entity: entity from which the request was received. 597 - @type entity: {JID} 598 + @type entity: L{JID} 599 """ 600 601 def unsubscribeReceived(self, entity): 602 @@ -180,7 +183,7 @@ 603 Unsubscription request was received. 604 605 @param entity: entity from which the request was received. 606 - @type entity: {JID} 607 + @type entity: L{JID} 608 """ 609 610 def available(self, entity=None, show=None, statuses=None, priority=0): 611 @@ -188,7 +191,7 @@ 612 Send available presence. 613 614 @param entity: optional entity to which the presence should be sent. 615 - @type entity: {JID} 616 + @type entity: L{JID} 617 @param show: optional detailed presence information. One of C{'away'}, 618 C{'xa'}, C{'chat'}, C{'dnd'}. 619 @type show: C{str} 620 @@ -207,7 +210,7 @@ 621 Send unavailable presence. 622 623 @param entity: optional entity to which the presence should be sent. 624 - @type entity: {JID} 625 + @type entity: L{JID} 626 @param statuses: dictionary of natural language descriptions of the 627 availability status, keyed by the language 628 descriptor. A status without a language 629 @@ -221,7 +224,7 @@ 630 Send subscription request 631 632 @param entity: entity to subscribe to. 633 - @type entity: {JID} 634 + @type entity: L{JID} 635 """ 636 self.send(Presence(to=entity, type='subscribe')) 637 638 @@ -230,7 +233,7 @@ 639 Send unsubscription request 640 641 @param entity: entity to unsubscribe from. 642 - @type entity: {JID} 643 + @type entity: L{JID} 644 """ 645 self.send(Presence(to=entity, type='unsubscribe')) 646 647 @@ -239,7 +242,7 @@ 648 Send subscription confirmation. 649 650 @param entity: entity that subscribed. 651 - @type entity: {JID} 652 + @type entity: L{JID} 653 """ 654 self.send(Presence(to=entity, type='subscribed')) 655 656 @@ -248,7 +251,7 @@ 657 Send unsubscription confirmation. 658 659 @param entity: entity that unsubscribed. 660 - @type entity: {JID} 661 + @type entity: L{JID} 662 """ 663 self.send(Presence(to=entity, type='unsubscribed')) 664 665 @@ -395,7 +398,10 @@ 503 diff -r d7fa09914b70 wokkel/im.py 504 --- a/wokkel/im.py Wed May 25 09:40:28 2011 +0200 505 +++ b/wokkel/im.py Wed May 25 09:46:06 2011 +0200 506 @@ -170,7 +170,10 @@ 666 507 self.xmlstream.addObserver("/presence", self._onPresence) 667 508 … … 675 516 676 517 presenceType = stanza.stanzaType or 'available' 677 @@ - 405,14 +411,19 @@518 @@ -180,14 +183,19 @@ 678 519 except KeyError: 679 520 return … … 697 538 698 539 def errorReceived(self, presence): 699 @@ -478,7 +489,7 @@ 700 701 @param recipient: Optional Recipient to which the presence should be 702 sent. 703 - @type recipient: {JID} 704 + @type recipient: L{JID} 705 706 @param show: Optional detailed presence information. One of C{'away'}, 707 C{'xa'}, C{'chat'}, C{'dnd'}. 708 @@ -503,7 +514,7 @@ 709 Send unavailable presence. 710 711 @param recipient: Optional entity to which the presence should be sent. 712 - @type recipient: {JID} 713 + @type recipient: L{JID} 714 715 @param statuses: dictionary of natural language descriptions of the 716 availability status, keyed by the language descriptor. A status 717 @@ -520,7 +531,7 @@ 718 Send subscription request 719 720 @param recipient: Entity to subscribe to. 721 - @type recipient: {JID} 722 + @type recipient: L{JID} 723 """ 724 presence = SubscriptionPresence(recipient=recipient, sender=sender) 725 presence.stanzaType = 'subscribe' 726 @@ -532,7 +543,7 @@ 727 Send unsubscription request 728 729 @param recipient: Entity to unsubscribe from. 730 - @type recipient: {JID} 731 + @type recipient: L{JID} 732 """ 733 presence = SubscriptionPresence(recipient=recipient, sender=sender) 734 presence.stanzaType = 'unsubscribe' 735 @@ -544,7 +555,7 @@ 736 Send subscription confirmation. 737 738 @param recipient: Entity that subscribed. 739 - @type recipient: {JID} 740 + @type recipient: L{JID} 741 """ 742 presence = SubscriptionPresence(recipient=recipient, sender=sender) 743 presence.stanzaType = 'subscribed' 744 @@ -556,7 +567,7 @@ 745 Send unsubscription confirmation. 746 747 @param recipient: Entity that unsubscribed. 748 - @type recipient: {JID} 749 + @type recipient: L{JID} 750 """ 751 presence = SubscriptionPresence(recipient=recipient, sender=sender) 752 presence.stanzaType = 'unsubscribed' 753 @@ -568,7 +579,7 @@ 754 Send presence probe. 755 756 @param recipient: Entity to be probed. 757 - @type recipient: {JID} 758 + @type recipient: L{JID} 759 """ 760 presence = ProbePresence(recipient=recipient, sender=sender) 761 self.send(presence.toElement()) 762 @@ -652,7 +663,7 @@ 763 764 765 def _parseRosterItem(self, element): 766 - jid = JID(element['jid']) 767 + jid = internJID(element['jid']) 768 item = RosterItem(jid) 769 item.name = element.getAttribute('name') 770 subscription = element.getAttribute('subscription') 771 @@ -715,7 +726,7 @@ 772 itemElement = iq.query.item 773 774 if unicode(itemElement['subscription']) == 'remove': 775 - self.onRosterRemove(JID(itemElement['jid'])) 776 + self.onRosterRemove(internJID(itemElement['jid'])) 777 else: 778 item = self._parseRosterItem(iq.query.item) 779 self.onRosterSet(item) 780 @@ -763,7 +774,7 @@ 781 def _onRosterGet(self, iq): 782 iq.handled = True 783 784 - d = self.getRoster(JID(iq["from"])) 785 + d = self.getRoster(internJID(iq["from"])) 786 d.addCallback(self._toRosterReply, iq) 787 d.addErrback(lambda _: error.ErrorStanza('internal-error').toResponse(iq)) 788 d.addBoth(self.send) 789 @@ -808,3 +819,433 @@ 790 """ 791 Called when a message stanza was received. 792 """ 793 + 794 + 795 + 540 @@ -583,6 +591,436 @@ 541 542 543 796 544 +class AccountIQHandler(XMPPHandler): 797 545 + … … 1221 969 + outPresence['to'] = fromJID.userhost() 1222 970 + self.send(outPresence) 971 + 972 + 973 + 974 class RosterServerProtocol(XMPPHandler): 975 """ 976 XMPP subprotocol handler for the roster, server side.
Note: See TracChangeset
for help on using the changeset viewer.