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@…>, 8 years ago

Upstreamed Request patches, split out c2s patches in managable chunks, prepare for release of Wokkel 0.7.0.

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  
    77XMPP IM protocol support.
    88
    99This 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.
     10U{RFC 6121<http://www.xmpp.org/rfcs/rfc6121.html>} (XMPP IM).
    1311"""
    1412
     13from twisted.internet import defer
    1514from twisted.words.protocols.jabber import jid
     15from twisted.words.protocols.jabber import error
    1616from twisted.words.xish import domish
    1717
    1818from wokkel.compat import IQ
    1919from wokkel.generic import ErrorStanza, Stanza
    20 from wokkel.subprotocols import XMPPHandler
     20from wokkel.subprotocols import IQHandlerMixin, XMPPHandler
    2121
    2222NS_XML = 'http://www.w3.org/XML/1998/namespace'
    2323NS_ROSTER = 'jabber:iq:roster'
    2424
     25XPATH_ROSTER_SET = "/iq[@type='set']/query[@xmlns='%s']" % NS_ROSTER
     26
     27
     28
    2529class BasePresence(Stanza):
    2630    """
    2731    Stanza of kind presence.
     
    349353
    350354    This represents one contact from an XMPP contact list known as roster.
    351355
    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}
    354358    @ivar name: The optional associated nickname for this contact.
    355359    @type name: C{unicode}
    356360    @ivar subscriptionTo: Subscription state to contact's presence. If C{True},
     
    360364    @ivar subscriptionFrom: Contact's subscription state. If C{True}, the
    361365                            contact is subscribed to the presence information
    362366                            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}
    366371    @ivar groups: Set of groups this contact is categorized in. Groups are
    367372                  represented by an opaque identifier of type C{unicode}.
    368373    @type groups: C{set}
    369374    """
    370375
    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'}
    378380
     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()
    379388
     389        self.pendingOut = False
     390        self.approved = False
     391        self.remove = False
    380392
    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
     443class RosterPushIgnored(Exception):
     444    """
     445    Raised when this entity doesn't want to accept/trust a roster push.
     446    """
     447
     448
     449
     450class RosterClientProtocol(XMPPHandler, IQHandlerMixin):
    382451    """
    383452    Client side XMPP roster protocol.
    384453    """
    385454
     455    iqHandlers = {XPATH_ROSTER_SET: "_onRosterSet"}
     456
     457
    386458    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)
    404460
    405461
    406462    def getRoster(self):
     
    415471            roster = {}
    416472            for element in domish.generateElementsQNamed(result.query.children,
    417473                                                         'item', NS_ROSTER):
    418                 item = self._parseRosterItem(element)
    419                 roster[item.jid.userhost()] = item
     474                item = RosterItem.fromElement(element)
     475                roster[item.entity] = item
    420476
    421477            return roster
    422478
     
    437493        """
    438494        iq = IQ(self.xmlstream, 'set')
    439495        iq.addElement((NS_ROSTER, 'query'))
    440         item = iq.query.addElement('item')
    441         item['jid'] = entity.full()
    442         item['subscription'] = 'remove'
     496        item = RosterItem(entity)
     497        item.remove = True
     498        iq.query.addChild(item.toElement())
    443499        return iq.send()
    444500
    445501
    446502    def _onRosterSet(self, iq):
    447         if iq.handled or \
    448            iq.hasAttribute('from') and iq['from'] != self.xmlstream:
    449             return
     503        def eb(failure):
     504            failure.trap(RosterPushIgnored)
     505            raise error.StanzaError('service-unavailable')
    450506
    451         iq.handled = True
     507        item = RosterItem.fromElement(iq.query.item)
    452508
    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)
    454513
    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
    460516
    461517
    462518    def onRosterSet(self, item):
    463519        """
    464520        Called when a roster push for a new or update item was received.
    465521
     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
    466526        @param item: The pushed roster item.
    467527        @type item: L{RosterItem}
    468528        """
     
    472532        """
    473533        Called when a roster push for the removal of an item was received.
    474534
     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
    475539        @param entity: The entity for which the roster item has been removed.
    476540        @type entity: L{jid.JID}
    477541        """
  • wokkel/test/test_im.py

    diff -r 2e2717d3db8f wokkel/test/test_im.py
    a b  
    77
    88from twisted.internet import defer
    99from twisted.trial import unittest
     10from twisted.words.protocols.jabber import error
    1011from twisted.words.protocols.jabber.jid import JID
    1112from twisted.words.protocols.jabber.xmlstream import toResponse
    1213from twisted.words.xish import domish, utility
    1314
    1415from wokkel import im
    1516from wokkel.generic import ErrorStanza, parseXml
    16 from wokkel.test.helpers import XmlStreamStub
     17from wokkel.test.helpers import TestableRequestHandlerMixin, XmlStreamStub
    1718
    1819NS_XML = 'http://www.w3.org/XML/1998/namespace'
    1920NS_ROSTER = 'jabber:iq:roster'
     
    389390
    390391
    391392
    392 class RosterClientProtocolTest(unittest.TestCase):
     393class 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
     675class RosterClientProtocolTest(unittest.TestCase, TestableRequestHandlerMixin):
    393676    """
    394677    Tests for L{im.RosterClientProtocol}.
    395678    """
    396679
    397680    def setUp(self):
    398681        self.stub = XmlStreamStub()
    399         self.protocol = im.RosterClientProtocol()
    400         self.protocol.xmlstream = self.stub.xmlstream
    401         self.protocol.connectionInitialized()
     682        self.service = im.RosterClientProtocol()
     683        self.service.makeConnection(self.stub.xmlstream)
     684        self.service.connectionInitialized()
    402685
    403686
    404687    def test_removeItem(self):
    405688        """
    406689        Removing a roster item is setting an item with subscription C{remove}.
    407690        """
    408         d = self.protocol.removeItem(JID('test@example.org'))
     691        d = self.service.removeItem(JID('test@example.org'))
    409692
    410693        # Inspect outgoing iq request
    411694
     
    419702        self.assertEquals(1, len(children))
    420703        child = children[0]
    421704        self.assertEquals('test@example.org', child['jid'])
    422         self.assertEquals('remove', child['subscription'])
     705        self.assertEquals('remove', child.getAttribute('subscription'))
    423706
    424707        # Fake successful response
    425708
    426709        response = toResponse(iq, 'result')
    427710        self.stub.send(response)
    428711        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.