source:
ralphm-patches/roster_item.patch
@
54:03ec57713c90
Last change on this file since 54:03ec57713c90 was 54:03ec57713c90, checked in by Ralph Meijer <ralphm@…>, 11 years ago | |
---|---|
File size: 22.2 KB |
-
wokkel/im.py
Clean up of RosterItem and RosterClientProtocol. `RosterItem`: * Renamed attributes `jid` and `ask` to `entity` and `pendingOut` respectively. * Can represent roster items to be removed or that have been removed. * Now has `fromElement` and `toElement` methods. `RosterClientProtocol`: * Roster returned from `getRoster` is now indexed by `JID`s (instead of the `unicode` representation of the JID. * `onRosterSet` and `onRosterRemove` can raise `RosterPushIgnored` to return a `service-unavailable` stanza error. diff -r 2e2717d3db8f wokkel/im.py
a b 7 7 XMPP IM protocol support. 8 8 9 9 This module provides generic implementations for the protocols defined in 10 U{RFC 3921<http://www.xmpp.org/rfcs/rfc3921.html>} (XMPP IM). 11 12 All of it should eventually move to Twisted. 10 U{RFC 6121<http://www.xmpp.org/rfcs/rfc6121.html>} (XMPP IM). 13 11 """ 14 12 13 from twisted.internet import defer 15 14 from twisted.words.protocols.jabber import jid 15 from twisted.words.protocols.jabber import error 16 16 from twisted.words.xish import domish 17 17 18 18 from wokkel.compat import IQ 19 19 from wokkel.generic import ErrorStanza, Stanza 20 from wokkel.subprotocols import XMPPHandler20 from wokkel.subprotocols import IQHandlerMixin, XMPPHandler 21 21 22 22 NS_XML = 'http://www.w3.org/XML/1998/namespace' 23 23 NS_ROSTER = 'jabber:iq:roster' 24 24 25 XPATH_ROSTER_SET = "/iq[@type='set']/query[@xmlns='%s']" % NS_ROSTER 26 27 28 25 29 class BasePresence(Stanza): 26 30 """ 27 31 Stanza of kind presence. … … 349 353 350 354 This represents one contact from an XMPP contact list known as roster. 351 355 352 @ivar jid: The JID of the contact.353 @type jid: L{jid.JID}356 @ivar entity: The JID of the contact. 357 @type entity: L{jid.JID} 354 358 @ivar name: The optional associated nickname for this contact. 355 359 @type name: C{unicode} 356 360 @ivar subscriptionTo: Subscription state to contact's presence. If C{True}, … … 360 364 @ivar subscriptionFrom: Contact's subscription state. If C{True}, the 361 365 contact is subscribed to the presence information 362 366 of the roster owner. 363 @type subscriptionTo: C{bool} 364 @ivar ask: Whether subscription is pending. 365 @type ask: C{bool} 367 @type subscriptionFrom: C{bool} 368 @ivar pendingOut: Whether the subscription request to this contact is 369 pending. 370 @type pendingOut: C{bool} 366 371 @ivar groups: Set of groups this contact is categorized in. Groups are 367 372 represented by an opaque identifier of type C{unicode}. 368 373 @type groups: C{set} 369 374 """ 370 375 371 def __init__(self, jid): 372 self.jid = jid 373 self.name = None 374 self.subscriptionTo = False 375 self.subscriptionFrom = False 376 self.ask = None 377 self.groups = set() 376 __subscriptionStates = {(False, False): None, 377 (True, False): 'to', 378 (False, True): 'from', 379 (True, True): 'both'} 378 380 381 def __init__(self, entity, subscriptionTo=False, subscriptionFrom=False, 382 name=None, groups=None): 383 self.entity = entity 384 self.subscriptionTo = subscriptionTo 385 self.subscriptionFrom = subscriptionFrom 386 self.name = name 387 self.groups = groups or set() 379 388 389 self.pendingOut = False 390 self.approved = False 391 self.remove = False 380 392 381 class RosterClientProtocol(XMPPHandler): 393 394 def toElement(self): 395 element = domish.Element((NS_ROSTER, 'item')) 396 element['jid'] = self.entity.full() 397 398 if self.remove: 399 subscription = 'remove' 400 else: 401 subscription = self.__subscriptionStates[self.subscriptionTo, 402 self.subscriptionFrom] 403 404 if self.pendingOut: 405 element['ask'] = u'subscribe' 406 407 if self.name: 408 element['name'] = self.name 409 410 if self.approved: 411 element['approved'] = u'true' 412 413 if self.groups: 414 for group in self.groups: 415 element.addElement('group', content=group) 416 417 if subscription: 418 element['subscription'] = subscription 419 420 return element 421 422 423 @classmethod 424 def fromElement(Class, element): 425 entity = jid.internJID(element['jid']) 426 item = Class(entity) 427 subscription = element.getAttribute('subscription') 428 if subscription == 'remove': 429 item.remove = True 430 else: 431 item.name = element.getAttribute('name') 432 item.subscriptionTo = subscription in ('to', 'both') 433 item.subscriptionFrom = subscription in ('from', 'both') 434 item.pendingOut = element.getAttribute('ask') == 'subscribe' 435 item.approved = element.getAttribute('approved') in ('true', '1') 436 for subElement in domish.generateElementsQNamed(element.children, 437 'group', NS_ROSTER): 438 item.groups.add(unicode(subElement)) 439 return item 440 441 442 443 class RosterPushIgnored(Exception): 444 """ 445 Raised when this entity doesn't want to accept/trust a roster push. 446 """ 447 448 449 450 class RosterClientProtocol(XMPPHandler, IQHandlerMixin): 382 451 """ 383 452 Client side XMPP roster protocol. 384 453 """ 385 454 455 iqHandlers = {XPATH_ROSTER_SET: "_onRosterSet"} 456 457 386 458 def connectionInitialized(self): 387 ROSTER_SET = "/iq[@type='set']/query[@xmlns='%s']" % NS_ROSTER 388 self.xmlstream.addObserver(ROSTER_SET, self._onRosterSet) 389 390 391 def _parseRosterItem(self, element): 392 entity = jid.internJID(element['jid']) 393 item = RosterItem(entity) 394 item.name = element.getAttribute('name') 395 subscription = element.getAttribute('subscription') 396 item.subscriptionTo = subscription in ('to', 'both') 397 item.subscriptionFrom = subscription in ('from', 'both') 398 item.ask = element.getAttribute('ask') == 'subscribe' 399 for subElement in domish.generateElementsQNamed(element.children, 400 'group', NS_ROSTER): 401 item.groups.add(unicode(subElement)) 402 403 return item 459 self.xmlstream.addObserver(XPATH_ROSTER_SET, self.handleRequest) 404 460 405 461 406 462 def getRoster(self): … … 415 471 roster = {} 416 472 for element in domish.generateElementsQNamed(result.query.children, 417 473 'item', NS_ROSTER): 418 item = self._parseRosterItem(element)419 roster[item. jid.userhost()] = item474 item = RosterItem.fromElement(element) 475 roster[item.entity] = item 420 476 421 477 return roster 422 478 … … 437 493 """ 438 494 iq = IQ(self.xmlstream, 'set') 439 495 iq.addElement((NS_ROSTER, 'query')) 440 item = iq.query.addElement('item')441 item ['jid'] = entity.full()442 i tem['subscription'] = 'remove'496 item = RosterItem(entity) 497 item.remove = True 498 iq.query.addChild(item.toElement()) 443 499 return iq.send() 444 500 445 501 446 502 def _onRosterSet(self, iq): 447 if iq.handled or \448 iq.hasAttribute('from') and iq['from'] != self.xmlstream:449 r eturn503 def eb(failure): 504 failure.trap(RosterPushIgnored) 505 raise error.StanzaError('service-unavailable') 450 506 451 i q.handled = True507 item = RosterItem.fromElement(iq.query.item) 452 508 453 itemElement = iq.query.item 509 if item.remove: 510 d = defer.maybeDeferred(self.onRosterRemove, item.entity) 511 else: 512 d = defer.maybeDeferred(self.onRosterSet, item) 454 513 455 if unicode(itemElement['subscription']) == 'remove': 456 self.onRosterRemove(jid.internJID(itemElement['jid'])) 457 else: 458 item = self._parseRosterItem(iq.query.item) 459 self.onRosterSet(item) 514 d.addErrback(eb) 515 return d 460 516 461 517 462 518 def onRosterSet(self, item): 463 519 """ 464 520 Called when a roster push for a new or update item was received. 465 521 522 Raise L{RosterPushIgnored} when not accepting this roster push 523 (directly or via Deferred). This will result in a 524 L{'service-unavailable'} error being sent in return. 525 466 526 @param item: The pushed roster item. 467 527 @type item: L{RosterItem} 468 528 """ … … 472 532 """ 473 533 Called when a roster push for the removal of an item was received. 474 534 535 Raise L{RosterPushIgnored} when not accepting this roster push 536 (directly or via Deferred). This will result in a 537 L{'service-unavailable'} error being sent in return. 538 475 539 @param entity: The entity for which the roster item has been removed. 476 540 @type entity: L{jid.JID} 477 541 """ -
wokkel/test/test_im.py
diff -r 2e2717d3db8f wokkel/test/test_im.py
a b 7 7 8 8 from twisted.internet import defer 9 9 from twisted.trial import unittest 10 from twisted.words.protocols.jabber import error 10 11 from twisted.words.protocols.jabber.jid import JID 11 12 from twisted.words.protocols.jabber.xmlstream import toResponse 12 13 from twisted.words.xish import domish, utility 13 14 14 15 from wokkel import im 15 16 from wokkel.generic import ErrorStanza, parseXml 16 from wokkel.test.helpers import XmlStreamStub17 from wokkel.test.helpers import TestableRequestHandlerMixin, XmlStreamStub 17 18 18 19 NS_XML = 'http://www.w3.org/XML/1998/namespace' 19 20 NS_ROSTER = 'jabber:iq:roster' … … 389 390 390 391 391 392 392 class RosterClientProtocolTest(unittest.TestCase): 393 class RosterItemTest(unittest.TestCase): 394 """ 395 Tests for L{im.RosterItem}. 396 """ 397 398 def test_toElement(self): 399 item = im.RosterItem(JID('user@example.org')) 400 element = item.toElement() 401 self.assertEquals('item', element.name) 402 self.assertEquals(NS_ROSTER, element.uri) 403 self.assertFalse(element.hasAttribute('subscription')) 404 self.assertFalse(element.hasAttribute('ask')) 405 self.assertFalse(element.hasAttribute('name')) 406 self.assertFalse(element.hasAttribute('approved')) 407 self.assertEquals(0, len(list(element.elements()))) 408 409 410 def test_toElementMinimal(self): 411 item = im.RosterItem(JID('user@example.org')) 412 element = item.toElement() 413 self.assertEquals(u'user@example.org', element.getAttribute('jid')) 414 415 416 def test_toElementSubscriptionNone(self): 417 item = im.RosterItem(JID('user@example.org'), 418 subscriptionTo=False, 419 subscriptionFrom=False) 420 element = item.toElement() 421 self.assertIdentical(None, element.getAttribute('subscription')) 422 423 424 def test_toElementSubscriptionTo(self): 425 item = im.RosterItem(JID('user@example.org'), 426 subscriptionTo=True, 427 subscriptionFrom=False) 428 element = item.toElement() 429 self.assertEquals('to', element.getAttribute('subscription')) 430 431 432 def test_toElementSubscriptionFrom(self): 433 item = im.RosterItem(JID('user@example.org'), 434 subscriptionTo=False, 435 subscriptionFrom=True) 436 element = item.toElement() 437 self.assertEquals('from', element.getAttribute('subscription')) 438 439 440 def test_toElementSubscriptionBoth(self): 441 item = im.RosterItem(JID('user@example.org'), 442 subscriptionTo=True, 443 subscriptionFrom=True) 444 element = item.toElement() 445 self.assertEquals('both', element.getAttribute('subscription')) 446 447 448 def test_toElementSubscriptionRemove(self): 449 item = im.RosterItem(JID('user@example.org')) 450 item.remove = True 451 element = item.toElement() 452 self.assertEquals('remove', element.getAttribute('subscription')) 453 454 455 def test_toElementAsk(self): 456 item = im.RosterItem(JID('user@example.org')) 457 item.pendingOut = True 458 element = item.toElement() 459 self.assertEquals('subscribe', element.getAttribute('ask')) 460 461 462 def test_toElementName(self): 463 item = im.RosterItem(JID('user@example.org'), 464 name='Joe User') 465 element = item.toElement() 466 self.assertEquals(u'Joe User', element.getAttribute('name')) 467 468 469 def test_toElementGroups(self): 470 groups = set(['Friends', 'Jabber']) 471 item = im.RosterItem(JID('user@example.org'), 472 groups=groups) 473 474 element = item.toElement() 475 foundGroups = set() 476 for child in element.elements(): 477 if child.uri == NS_ROSTER and child.name == 'group': 478 foundGroups.add(unicode(child)) 479 480 self.assertEqual(groups, foundGroups) 481 482 483 def test_toElementApproved(self): 484 """ 485 A pre-approved subscription for a roster item has an 'approved' flag. 486 """ 487 item = im.RosterItem(JID('user@example.org')) 488 item.approved = True 489 element = item.toElement() 490 self.assertEquals(u'true', element.getAttribute('approved')) 491 492 493 def test_fromElementMinimal(self): 494 """ 495 A minimal roster item has a reference to the JID of the contact. 496 """ 497 498 xml = """ 499 <item xmlns="jabber:iq:roster" 500 jid="test@example.org"/> 501 """ 502 503 item = im.RosterItem.fromElement(parseXml(xml)) 504 self.assertEqual(JID(u"test@example.org"), item.entity) 505 self.assertIdentical(None, item.name) 506 self.assertFalse(item.subscriptionTo) 507 self.assertFalse(item.subscriptionFrom) 508 self.assertFalse(item.pendingOut) 509 self.assertFalse(item.approved) 510 self.assertEquals(set(), item.groups) 511 512 513 def test_fromElementName(self): 514 """ 515 A roster item may have an optional name. 516 """ 517 518 xml = """ 519 <item xmlns="jabber:iq:roster" 520 jid="test@example.org" 521 name="Test User"/> 522 """ 523 524 item = im.RosterItem.fromElement(parseXml(xml)) 525 self.assertEqual(u"Test User", item.name) 526 527 528 def test_fromElementGroups(self): 529 """ 530 A roster item may have one or more groups. 531 """ 532 533 xml = """ 534 <item xmlns="jabber:iq:roster" 535 jid="test@example.org"> 536 <group>Friends</group> 537 <group>Twisted</group> 538 </item> 539 """ 540 541 item = im.RosterItem.fromElement(parseXml(xml)) 542 self.assertIn(u"Twisted", item.groups) 543 self.assertIn(u"Friends", item.groups) 544 545 546 def test_fromElementSubscriptionNone(self): 547 """ 548 Subscription 'none' sets both attributes to False. 549 """ 550 551 xml = """ 552 <item xmlns="jabber:iq:roster" 553 jid="test@example.org" 554 subscription="none"/> 555 """ 556 557 item = im.RosterItem.fromElement(parseXml(xml)) 558 self.assertFalse(item.remove) 559 self.assertFalse(item.subscriptionTo) 560 self.assertFalse(item.subscriptionFrom) 561 562 563 def test_fromElementSubscriptionTo(self): 564 """ 565 Subscription 'to' sets the corresponding attribute to True. 566 """ 567 568 xml = """ 569 <item xmlns="jabber:iq:roster" 570 jid="test@example.org" 571 subscription="to"/> 572 """ 573 574 item = im.RosterItem.fromElement(parseXml(xml)) 575 self.assertFalse(item.remove) 576 self.assertTrue(item.subscriptionTo) 577 self.assertFalse(item.subscriptionFrom) 578 579 580 def test_fromElementSubscriptionFrom(self): 581 """ 582 Subscription 'from' sets the corresponding attribute to True. 583 """ 584 585 xml = """ 586 <item xmlns="jabber:iq:roster" 587 jid="test@example.org" 588 subscription="from"/> 589 """ 590 591 item = im.RosterItem.fromElement(parseXml(xml)) 592 self.assertFalse(item.remove) 593 self.assertFalse(item.subscriptionTo) 594 self.assertTrue(item.subscriptionFrom) 595 596 597 def test_fromElementSubscriptionBoth(self): 598 """ 599 Subscription 'both' sets both attributes to True. 600 """ 601 602 xml = """ 603 <item xmlns="jabber:iq:roster" 604 jid="test@example.org" 605 subscription="both"/> 606 """ 607 608 item = im.RosterItem.fromElement(parseXml(xml)) 609 self.assertFalse(item.remove) 610 self.assertTrue(item.subscriptionTo) 611 self.assertTrue(item.subscriptionFrom) 612 613 614 def test_fromElementSubscriptionRemove(self): 615 """ 616 Subscription 'remove' sets the remove attribute. 617 """ 618 619 xml = """ 620 <item xmlns="jabber:iq:roster" 621 jid="test@example.org" 622 subscription="remove"/> 623 """ 624 625 item = im.RosterItem.fromElement(parseXml(xml)) 626 self.assertTrue(item.remove) 627 628 629 def test_fromElementPendingOut(self): 630 """ 631 The ask attribute, if set to 'subscription', means pending out. 632 """ 633 634 xml = """ 635 <item xmlns="jabber:iq:roster" 636 jid="test@example.org" 637 ask="subscribe"/> 638 """ 639 640 item = im.RosterItem.fromElement(parseXml(xml)) 641 self.assertTrue(item.pendingOut) 642 643 644 def test_fromElementApprovedTrue(self): 645 """ 646 The approved attribute (true) signals a pre-approved subscription. 647 """ 648 649 xml = """ 650 <item xmlns="jabber:iq:roster" 651 jid="test@example.org" 652 approved="true"/> 653 """ 654 655 item = im.RosterItem.fromElement(parseXml(xml)) 656 self.assertTrue(item.approved) 657 658 659 def test_fromElementApproved1(self): 660 """ 661 The approved attribute (1) signals a pre-approved subscription. 662 """ 663 664 xml = """ 665 <item xmlns="jabber:iq:roster" 666 jid="test@example.org" 667 approved="1"/> 668 """ 669 670 item = im.RosterItem.fromElement(parseXml(xml)) 671 self.assertTrue(item.approved) 672 673 674 675 class RosterClientProtocolTest(unittest.TestCase, TestableRequestHandlerMixin): 393 676 """ 394 677 Tests for L{im.RosterClientProtocol}. 395 678 """ 396 679 397 680 def setUp(self): 398 681 self.stub = XmlStreamStub() 399 self. protocol= im.RosterClientProtocol()400 self. protocol.xmlstream = self.stub.xmlstream401 self. protocol.connectionInitialized()682 self.service = im.RosterClientProtocol() 683 self.service.makeConnection(self.stub.xmlstream) 684 self.service.connectionInitialized() 402 685 403 686 404 687 def test_removeItem(self): 405 688 """ 406 689 Removing a roster item is setting an item with subscription C{remove}. 407 690 """ 408 d = self. protocol.removeItem(JID('test@example.org'))691 d = self.service.removeItem(JID('test@example.org')) 409 692 410 693 # Inspect outgoing iq request 411 694 … … 419 702 self.assertEquals(1, len(children)) 420 703 child = children[0] 421 704 self.assertEquals('test@example.org', child['jid']) 422 self.assertEquals('remove', child ['subscription'])705 self.assertEquals('remove', child.getAttribute('subscription')) 423 706 424 707 # Fake successful response 425 708 426 709 response = toResponse(iq, 'result') 427 710 self.stub.send(response) 428 711 return d 712 713 714 def test_getRoster(self): 715 def cb(roster): 716 self.assertIn(JID('user@example.org'), roster) 717 718 719 d = self.service.getRoster() 720 d.addCallback(cb) 721 722 # Inspect outgoing iq request 723 724 iq = self.stub.output[-1] 725 self.assertEquals('get', iq.getAttribute('type')) 726 self.assertNotIdentical(None, iq.query) 727 self.assertEquals(NS_ROSTER, iq.query.uri) 728 729 # Fake successful response 730 response = toResponse(iq, 'result') 731 query = response.addElement((NS_ROSTER, 'query')) 732 item = query.addElement('item') 733 item['jid'] = 'user@example.org' 734 735 self.stub.send(response) 736 return d 737 738 739 def test_onRosterSet(self): 740 """ 741 A roster push causes onRosterSet to be called with the parsed item. 742 """ 743 xml = """ 744 <iq type='set'> 745 <query xmlns='jabber:iq:roster'> 746 <item jid='user@example.org'/> 747 </query> 748 </iq> 749 """ 750 751 items = [] 752 753 def onRosterSet(item): 754 items.append(item) 755 756 def cb(result): 757 self.assertEquals(1, len(items)) 758 self.assertEquals(JID('user@example.org'), items[0].entity) 759 760 self.service.onRosterSet = onRosterSet 761 762 d = self.handleRequest(xml) 763 d.addCallback(cb) 764 return d 765 766 767 def test_onRosterSetUntrusted(self): 768 """ 769 Roster pushes from untrusted sources will be not be handled. 770 """ 771 xml = """ 772 <iq type='set' from='bad@example.org'> 773 <query xmlns='jabber:iq:roster'> 774 <item jid='user@example.org'/> 775 </query> 776 </iq> 777 """ 778 779 def onRosterSet(item): 780 raise im.RosterPushIgnored() 781 782 def cb(result): 783 self.assertEquals('service-unavailable', result.condition) 784 785 self.service.onRosterSet = onRosterSet 786 787 d = self.handleRequest(xml) 788 self.assertFailure(d, error.StanzaError) 789 d.addCallback(cb) 790 return d 791 792 793 def test_onRosterRemove(self): 794 """ 795 A roster push causes onRosterSet to be called with the parsed item. 796 """ 797 xml = """ 798 <iq type='set'> 799 <query xmlns='jabber:iq:roster'> 800 <item jid='user@example.org' subscription='remove'/> 801 </query> 802 </iq> 803 """ 804 805 entities = [] 806 807 def onRosterRemove(entity): 808 entities.append(entity) 809 810 def cb(result): 811 self.assertEquals([JID('user@example.org')], entities) 812 813 self.service.onRosterRemove = onRosterRemove 814 815 d = self.handleRequest(xml) 816 d.addCallback(cb) 817 return d 818
Note: See TracBrowser
for help on using the repository browser.