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