source:
ralphm-patches/roster_item.patch
@
57:0d8b6cf41728
Last change on this file since 57:0d8b6cf41728 was 57:0d8b6cf41728, checked in by Ralph Meijer <ralphm@…>, 10 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 a3c50205821b 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://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 14 from twisted.words.protocols.jabber import error 15 15 from twisted.words.protocols.jabber import jid 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 IQHandlerMixin 20 21 from wokkel.subprotocols import XMPPHandler 21 22 22 23 NS_XML = 'http://www.w3.org/XML/1998/namespace' 23 24 NS_ROSTER = 'jabber:iq:roster' 24 25 26 XPATH_ROSTER_SET = "/iq[@type='set']/query[@xmlns='%s']" % NS_ROSTER 27 28 29 25 30 class BasePresence(Stanza): 26 31 """ 27 32 Stanza of kind presence. … … 377 382 378 383 This represents one contact from an XMPP contact list known as roster. 379 384 380 @ivar jid: The JID of the contact.381 @type jid: L{jid.JID}385 @ivar entity: The JID of the contact. 386 @type entity: L{jid.JID} 382 387 @ivar name: The optional associated nickname for this contact. 383 388 @type name: C{unicode} 384 389 @ivar subscriptionTo: Subscription state to contact's presence. If C{True}, … … 388 393 @ivar subscriptionFrom: Contact's subscription state. If C{True}, the 389 394 contact is subscribed to the presence information 390 395 of the roster owner. 391 @type subscriptionTo: C{bool} 392 @ivar ask: Whether subscription is pending. 393 @type ask: C{bool} 396 @type subscriptionFrom: C{bool} 397 @ivar pendingOut: Whether the subscription request to this contact is 398 pending. 399 @type pendingOut: C{bool} 394 400 @ivar groups: Set of groups this contact is categorized in. Groups are 395 401 represented by an opaque identifier of type C{unicode}. 396 402 @type groups: C{set} 397 403 """ 398 404 399 def __init__(self, jid): 400 self.jid = jid 401 self.name = None 402 self.subscriptionTo = False 403 self.subscriptionFrom = False 404 self.ask = None 405 self.groups = set() 405 __subscriptionStates = {(False, False): None, 406 (True, False): 'to', 407 (False, True): 'from', 408 (True, True): 'both'} 406 409 410 def __init__(self, entity, subscriptionTo=False, subscriptionFrom=False, 411 name=None, groups=None): 412 self.entity = entity 413 self.subscriptionTo = subscriptionTo 414 self.subscriptionFrom = subscriptionFrom 415 self.name = name 416 self.groups = groups or set() 407 417 418 self.pendingOut = False 419 self.approved = False 420 self.remove = False 408 421 409 class RosterClientProtocol(XMPPHandler): 422 423 def toElement(self): 424 element = domish.Element((NS_ROSTER, 'item')) 425 element['jid'] = self.entity.full() 426 427 if self.remove: 428 subscription = 'remove' 429 else: 430 subscription = self.__subscriptionStates[self.subscriptionTo, 431 self.subscriptionFrom] 432 433 if self.pendingOut: 434 element['ask'] = u'subscribe' 435 436 if self.name: 437 element['name'] = self.name 438 439 if self.approved: 440 element['approved'] = u'true' 441 442 if self.groups: 443 for group in self.groups: 444 element.addElement('group', content=group) 445 446 if subscription: 447 element['subscription'] = subscription 448 449 return element 450 451 452 @classmethod 453 def fromElement(Class, element): 454 entity = jid.internJID(element['jid']) 455 item = Class(entity) 456 subscription = element.getAttribute('subscription') 457 if subscription == 'remove': 458 item.remove = True 459 else: 460 item.name = element.getAttribute('name') 461 item.subscriptionTo = subscription in ('to', 'both') 462 item.subscriptionFrom = subscription in ('from', 'both') 463 item.pendingOut = element.getAttribute('ask') == 'subscribe' 464 item.approved = element.getAttribute('approved') in ('true', '1') 465 for subElement in domish.generateElementsQNamed(element.children, 466 'group', NS_ROSTER): 467 item.groups.add(unicode(subElement)) 468 return item 469 470 471 472 class RosterPushIgnored(Exception): 473 """ 474 Raised when this entity doesn't want to accept/trust a roster push. 475 """ 476 477 478 479 class RosterClientProtocol(XMPPHandler, IQHandlerMixin): 410 480 """ 411 481 Client side XMPP roster protocol. 412 482 """ 413 483 484 iqHandlers = {XPATH_ROSTER_SET: "_onRosterSet"} 485 486 414 487 def connectionInitialized(self): 415 ROSTER_SET = "/iq[@type='set']/query[@xmlns='%s']" % NS_ROSTER 416 self.xmlstream.addObserver(ROSTER_SET, self._onRosterSet) 417 418 419 def _parseRosterItem(self, element): 420 entity = jid.internJID(element['jid']) 421 item = RosterItem(entity) 422 item.name = element.getAttribute('name') 423 subscription = element.getAttribute('subscription') 424 item.subscriptionTo = subscription in ('to', 'both') 425 item.subscriptionFrom = subscription in ('from', 'both') 426 item.ask = element.getAttribute('ask') == 'subscribe' 427 for subElement in domish.generateElementsQNamed(element.children, 428 'group', NS_ROSTER): 429 item.groups.add(unicode(subElement)) 430 431 return item 488 self.xmlstream.addObserver(XPATH_ROSTER_SET, self.handleRequest) 432 489 433 490 434 491 def getRoster(self): … … 443 500 roster = {} 444 501 for element in domish.generateElementsQNamed(result.query.children, 445 502 'item', NS_ROSTER): 446 item = self._parseRosterItem(element)447 roster[item. jid.userhost()] = item503 item = RosterItem.fromElement(element) 504 roster[item.entity] = item 448 505 449 506 return roster 450 507 … … 465 522 """ 466 523 iq = IQ(self.xmlstream, 'set') 467 524 iq.addElement((NS_ROSTER, 'query')) 468 item = iq.query.addElement('item')469 item ['jid'] = entity.full()470 i tem['subscription'] = 'remove'525 item = RosterItem(entity) 526 item.remove = True 527 iq.query.addChild(item.toElement()) 471 528 return iq.send() 472 529 473 530 474 531 def _onRosterSet(self, iq): 475 if iq.handled or \476 iq.hasAttribute('from') and iq['from'] != self.xmlstream:477 r eturn532 def eb(failure): 533 failure.trap(RosterPushIgnored) 534 raise error.StanzaError('service-unavailable') 478 535 479 i q.handled = True536 item = RosterItem.fromElement(iq.query.item) 480 537 481 itemElement = iq.query.item 538 if item.remove: 539 d = defer.maybeDeferred(self.onRosterRemove, item.entity) 540 else: 541 d = defer.maybeDeferred(self.onRosterSet, item) 482 542 483 if unicode(itemElement['subscription']) == 'remove': 484 self.onRosterRemove(jid.internJID(itemElement['jid'])) 485 else: 486 item = self._parseRosterItem(iq.query.item) 487 self.onRosterSet(item) 543 d.addErrback(eb) 544 return d 488 545 489 546 490 547 def onRosterSet(self, item): 491 548 """ 492 549 Called when a roster push for a new or update item was received. 493 550 551 Raise L{RosterPushIgnored} when not accepting this roster push 552 (directly or via Deferred). This will result in a 553 L{'service-unavailable'} error being sent in return. 554 494 555 @param item: The pushed roster item. 495 556 @type item: L{RosterItem} 496 557 """ … … 500 561 """ 501 562 Called when a roster push for the removal of an item was received. 502 563 564 Raise L{RosterPushIgnored} when not accepting this roster push 565 (directly or via Deferred). This will result in a 566 L{'service-unavailable'} error being sent in return. 567 503 568 @param entity: The entity for which the roster item has been removed. 504 569 @type entity: L{jid.JID} 505 570 """ -
wokkel/test/test_im.py
diff -r a3c50205821b 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.