Commits

Ralph Meijer committed 7ef6992

Applied reworked patches to default.

Comments (0)

Files changed (3)

roster_item.patch

-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.
-
-`RosterRequest`:
- * New class to represent roster request stanzas.
-
-`RosterClientProtocol`:
- * Roster returned from `getRoster` is now indexed by `JID`s (instead of
-   the `unicode` representation of the JID.
- * Outgoing requests are now done using `RosterRequest`.
- * `onRosterSet` and `onRosterRemove` are deprecated in favor of
-   `pushReceived`, which is called with a `RosterRequest` for all roster
-   pushes.
-
-TODO:
- * Add tests for RosterRequest. Version has no coverage.
- * Add version support when sending.
-
-diff --git a/wokkel/test/test_xmppim.py b/wokkel/test/test_xmppim.py
---- a/wokkel/test/test_xmppim.py
-+++ b/wokkel/test/test_xmppim.py
-@@ -13,7 +13,8 @@
- 
- from wokkel import xmppim
- from wokkel.generic import ErrorStanza, parseXml
--from wokkel.test.helpers import XmlStreamStub
-+from wokkel.subprotocols import XMPPHandler
-+from wokkel.test.helpers import TestableRequestHandlerMixin, XmlStreamStub
- 
- NS_XML = 'http://www.w3.org/XML/1998/namespace'
- NS_ROSTER = 'jabber:iq:roster'
-@@ -449,40 +450,621 @@
- 
- 
- 
--class RosterClientProtocolTest(unittest.TestCase):
-+class RosterItemTest(unittest.TestCase):
-+    """
-+    Tests for L{xmppim.RosterItem}.
-+    """
-+
-+    def test_toElement(self):
-+        item = xmppim.RosterItem(JID('user@example.org'))
-+        element = item.toElement()
-+        self.assertEqual('item', element.name)
-+        self.assertEqual(NS_ROSTER, element.uri)
-+        self.assertFalse(element.hasAttribute('subscription'))
-+        self.assertFalse(element.hasAttribute('ask'))
-+        self.assertFalse(element.hasAttribute('name'))
-+        self.assertFalse(element.hasAttribute('approved'))
-+        self.assertEquals(0, len(list(element.elements())))
-+
-+
-+    def test_toElementMinimal(self):
-+        item = xmppim.RosterItem(JID('user@example.org'))
-+        element = item.toElement()
-+        self.assertEqual(u'user@example.org', element.getAttribute('jid'))
-+
-+
-+    def test_toElementSubscriptionNone(self):
-+        item = xmppim.RosterItem(JID('user@example.org'),
-+                                 subscriptionTo=False,
-+                                 subscriptionFrom=False)
-+        element = item.toElement()
-+        self.assertIdentical(None, element.getAttribute('subscription'))
-+
-+
-+    def test_toElementSubscriptionTo(self):
-+        item = xmppim.RosterItem(JID('user@example.org'),
-+                                 subscriptionTo=True,
-+                                 subscriptionFrom=False)
-+        element = item.toElement()
-+        self.assertEqual('to', element.getAttribute('subscription'))
-+
-+
-+    def test_toElementSubscriptionFrom(self):
-+        item = xmppim.RosterItem(JID('user@example.org'),
-+                                 subscriptionTo=False,
-+                                 subscriptionFrom=True)
-+        element = item.toElement()
-+        self.assertEqual('from', element.getAttribute('subscription'))
-+
-+
-+    def test_toElementSubscriptionBoth(self):
-+        item = xmppim.RosterItem(JID('user@example.org'),
-+                                 subscriptionTo=True,
-+                                 subscriptionFrom=True)
-+        element = item.toElement()
-+        self.assertEqual('both', element.getAttribute('subscription'))
-+
-+
-+    def test_toElementSubscriptionRemove(self):
-+        item = xmppim.RosterItem(JID('user@example.org'))
-+        item.remove = True
-+        element = item.toElement()
-+        self.assertEqual('remove', element.getAttribute('subscription'))
-+
-+
-+    def test_toElementAsk(self):
-+        item = xmppim.RosterItem(JID('user@example.org'))
-+        item.pendingOut = True
-+        element = item.toElement()
-+        self.assertEqual('subscribe', element.getAttribute('ask'))
-+
-+
-+    def test_toElementName(self):
-+        item = xmppim.RosterItem(JID('user@example.org'),
-+                                 name='Joe User')
-+        element = item.toElement()
-+        self.assertEqual(u'Joe User', element.getAttribute('name'))
-+
-+
-+    def test_toElementGroups(self):
-+        groups = set(['Friends', 'Jabber'])
-+        item = xmppim.RosterItem(JID('user@example.org'),
-+                                 groups=groups)
-+
-+        element = item.toElement()
-+        foundGroups = set()
-+        for child in element.elements():
-+            if child.uri == NS_ROSTER and child.name == 'group':
-+                foundGroups.add(unicode(child))
-+
-+        self.assertEqual(groups, foundGroups)
-+
-+
-+    def test_toElementApproved(self):
-+        """
-+        A pre-approved subscription for a roster item has an 'approved' flag.
-+        """
-+        item = xmppim.RosterItem(JID('user@example.org'))
-+        item.approved = True
-+        element = item.toElement()
-+        self.assertEqual(u'true', element.getAttribute('approved'))
-+
-+
-+    def test_fromElementMinimal(self):
-+        """
-+        A minimal roster item has a reference to the JID of the contact.
-+        """
-+
-+        xml = """
-+            <item xmlns="jabber:iq:roster"
-+                  jid="test@example.org"/>
-+        """
-+
-+        item = xmppim.RosterItem.fromElement(parseXml(xml))
-+        self.assertEqual(JID(u"test@example.org"), item.entity)
-+        self.assertIdentical(None, item.name)
-+        self.assertFalse(item.subscriptionTo)
-+        self.assertFalse(item.subscriptionFrom)
-+        self.assertFalse(item.pendingOut)
-+        self.assertFalse(item.approved)
-+        self.assertEqual(set(), item.groups)
-+
-+
-+    def test_fromElementName(self):
-+        """
-+        A roster item may have an optional name.
-+        """
-+
-+        xml = """
-+            <item xmlns="jabber:iq:roster"
-+                  jid="test@example.org"
-+                  name="Test User"/>
-+        """
-+
-+        item = xmppim.RosterItem.fromElement(parseXml(xml))
-+        self.assertEqual(u"Test User", item.name)
-+
-+
-+    def test_fromElementGroups(self):
-+        """
-+        A roster item may have one or more groups.
-+        """
-+
-+        xml = """
-+            <item xmlns="jabber:iq:roster"
-+                  jid="test@example.org">
-+              <group>Friends</group>
-+              <group>Twisted</group>
-+            </item>
-+        """
-+
-+        item = xmppim.RosterItem.fromElement(parseXml(xml))
-+        self.assertIn(u"Twisted", item.groups)
-+        self.assertIn(u"Friends", item.groups)
-+
-+
-+    def test_fromElementSubscriptionNone(self):
-+        """
-+        Subscription 'none' sets both attributes to False.
-+        """
-+
-+        xml = """
-+            <item xmlns="jabber:iq:roster"
-+                  jid="test@example.org"
-+                  subscription="none"/>
-+        """
-+
-+        item = xmppim.RosterItem.fromElement(parseXml(xml))
-+        self.assertFalse(item.remove)
-+        self.assertFalse(item.subscriptionTo)
-+        self.assertFalse(item.subscriptionFrom)
-+
-+
-+    def test_fromElementSubscriptionTo(self):
-+        """
-+        Subscription 'to' sets the corresponding attribute to True.
-+        """
-+
-+        xml = """
-+            <item xmlns="jabber:iq:roster"
-+                  jid="test@example.org"
-+                  subscription="to"/>
-+        """
-+
-+        item = xmppim.RosterItem.fromElement(parseXml(xml))
-+        self.assertFalse(item.remove)
-+        self.assertTrue(item.subscriptionTo)
-+        self.assertFalse(item.subscriptionFrom)
-+
-+
-+    def test_fromElementSubscriptionFrom(self):
-+        """
-+        Subscription 'from' sets the corresponding attribute to True.
-+        """
-+
-+        xml = """
-+            <item xmlns="jabber:iq:roster"
-+                  jid="test@example.org"
-+                  subscription="from"/>
-+        """
-+
-+        item = xmppim.RosterItem.fromElement(parseXml(xml))
-+        self.assertFalse(item.remove)
-+        self.assertFalse(item.subscriptionTo)
-+        self.assertTrue(item.subscriptionFrom)
-+
-+
-+    def test_fromElementSubscriptionBoth(self):
-+        """
-+        Subscription 'both' sets both attributes to True.
-+        """
-+
-+        xml = """
-+            <item xmlns="jabber:iq:roster"
-+                  jid="test@example.org"
-+                  subscription="both"/>
-+        """
-+
-+        item = xmppim.RosterItem.fromElement(parseXml(xml))
-+        self.assertFalse(item.remove)
-+        self.assertTrue(item.subscriptionTo)
-+        self.assertTrue(item.subscriptionFrom)
-+
-+
-+    def test_fromElementSubscriptionRemove(self):
-+        """
-+        Subscription 'remove' sets the remove attribute.
-+        """
-+
-+        xml = """
-+            <item xmlns="jabber:iq:roster"
-+                  jid="test@example.org"
-+                  subscription="remove"/>
-+        """
-+
-+        item = xmppim.RosterItem.fromElement(parseXml(xml))
-+        self.assertTrue(item.remove)
-+
-+
-+    def test_fromElementPendingOut(self):
-+        """
-+        The ask attribute, if set to 'subscription', means pending out.
-+        """
-+
-+        xml = """
-+            <item xmlns="jabber:iq:roster"
-+                  jid="test@example.org"
-+                  ask="subscribe"/>
-+        """
-+
-+        item = xmppim.RosterItem.fromElement(parseXml(xml))
-+        self.assertTrue(item.pendingOut)
-+
-+
-+    def test_fromElementApprovedTrue(self):
-+        """
-+        The approved attribute (true) signals a pre-approved subscription.
-+        """
-+
-+        xml = """
-+            <item xmlns="jabber:iq:roster"
-+                  jid="test@example.org"
-+                  approved="true"/>
-+        """
-+
-+        item = xmppim.RosterItem.fromElement(parseXml(xml))
-+        self.assertTrue(item.approved)
-+
-+
-+    def test_fromElementApproved1(self):
-+        """
-+        The approved attribute (1) signals a pre-approved subscription.
-+        """
-+
-+        xml = """
-+            <item xmlns="jabber:iq:roster"
-+                  jid="test@example.org"
-+                  approved="1"/>
-+        """
-+
-+        item = xmppim.RosterItem.fromElement(parseXml(xml))
-+        self.assertTrue(item.approved)
-+
-+
-+    def test_jidDeprecationGet(self):
-+        """
-+        Getting the jid attribute works as entity and warns deprecation.
-+        """
-+        item = xmppim.RosterItem(JID('user@example.org'))
-+        entity = self.assertWarns(DeprecationWarning,
-+                                  "wokkel.xmppim.RosterItem.jid is deprecated. "
-+                                  "Use RosterItem.entity instead.",
-+                                  xmppim.__file__,
-+                                  getattr, item, 'jid')
-+        self.assertIdentical(entity, item.entity)
-+
-+
-+    def test_jidDeprecationSet(self):
-+        """
-+        Setting the jid attribute works as entity and warns deprecation.
-+        """
-+        item = xmppim.RosterItem(JID('user@example.org'))
-+        self.assertWarns(DeprecationWarning,
-+                         "wokkel.xmppim.RosterItem.jid is deprecated. "
-+                         "Use RosterItem.entity instead.",
-+                         xmppim.__file__,
-+                         setattr, item, 'jid',
-+                         JID('other@example.org'))
-+        self.assertEqual(JID('other@example.org'), item.entity)
-+
-+
-+    def test_askDeprecationGet(self):
-+        """
-+        Getting the ask attribute works as entity and warns deprecation.
-+        """
-+        item = xmppim.RosterItem(JID('user@example.org'))
-+        item.pendingOut = True
-+        ask = self.assertWarns(DeprecationWarning,
-+                               "wokkel.xmppim.RosterItem.ask is deprecated. "
-+                               "Use RosterItem.pendingOut instead.",
-+                               xmppim.__file__,
-+                               getattr, item, 'ask')
-+        self.assertTrue(ask)
-+
-+
-+    def test_askDeprecationSet(self):
-+        """
-+        Setting the ask attribute works as entity and warns deprecation.
-+        """
-+        item = xmppim.RosterItem(JID('user@example.org'))
-+        self.assertWarns(DeprecationWarning,
-+                         "wokkel.xmppim.RosterItem.ask is deprecated. "
-+                         "Use RosterItem.pendingOut instead.",
-+                         xmppim.__file__,
-+                         setattr, item, 'ask',
-+                         True)
-+        self.assertTrue(item.pendingOut)
-+
-+
-+
-+class RosterRequestTest(unittest.TestCase):
-+    """
-+    Tests for L{xmppim.RosterRequest}.
-+    """
-+
-+    def test_fromElement(self):
-+        """
-+        A bare roster request is parsed and missing information is None.
-+        """
-+        xml = """
-+            <iq type='get' to='this@example.org/Home' from='this@example.org'>
-+              <query xmlns='jabber:iq:roster'/>
-+            </iq>
-+        """
-+
-+        request = xmppim.RosterRequest.fromElement(parseXml(xml))
-+        self.assertEqual('get', request.stanzaType)
-+        self.assertEqual(JID('this@example.org/Home'), request.recipient)
-+        self.assertEqual(JID('this@example.org'), request.sender)
-+        self.assertEqual(None, request.item)
-+        self.assertEqual(None, request.version)
-+
-+
-+    def test_fromElementItem(self):
-+        """
-+        If an item is present, parse it and put it in the request item.
-+        """
-+        xml = """
-+            <iq type='set' to='this@example.org/Home' from='this@example.org'>
-+              <query xmlns='jabber:iq:roster'>
-+                <item jid='user@example.org'/>
-+              </query>
-+            </iq>
-+        """
-+
-+        request = xmppim.RosterRequest.fromElement(parseXml(xml))
-+        self.assertNotIdentical(None, request.item)
-+        self.assertEqual(JID('user@example.org'), request.item.entity)
-+
-+
-+    def test_fromElementVersion(self):
-+        """
-+        If a ver attribute is present, put it in the request version.
-+        """
-+        xml = """
-+            <iq type='set' to='this@example.org/Home' from='this@example.org'>
-+              <query xmlns='jabber:iq:roster' ver='ver72'>
-+                <item jid='user@example.org'/>
-+              </query>
-+            </iq>
-+        """
-+        request = xmppim.RosterRequest.fromElement(parseXml(xml))
-+        self.assertEqual('ver72', request.version)
-+
-+
-+    def test_fromElementVersionEmpty(self):
-+        """
-+        The ver attribute may be empty.
-+        """
-+        xml = """
-+            <iq type='get' to='this@example.org/Home' from='this@example.org'>
-+              <query xmlns='jabber:iq:roster' ver=''/>
-+            </iq>
-+        """
-+        request = xmppim.RosterRequest.fromElement(parseXml(xml))
-+        self.assertEqual('', request.version)
-+
-+
-+    def test_toElement(self):
-+        """
-+        A roster request has a query element in the roster namespace.
-+        """
-+        request = xmppim.RosterRequest()
-+        element = request.toElement()
-+        children = element.elements()
-+        child = children.next()
-+        self.assertEqual(NS_ROSTER, child.uri)
-+        self.assertEqual('query', child.name)
-+
-+
-+    def test_toElementItem(self):
-+        """
-+        If an item is set, it is rendered as a child of the query.
-+        """
-+        request = xmppim.RosterRequest()
-+        request.item = xmppim.RosterItem(JID('user@example.org'))
-+        element = request.toElement()
-+        children = element.query.elements()
-+        child = children.next()
-+        self.assertEqual(NS_ROSTER, child.uri)
-+        self.assertEqual('item', child.name)
-+
-+
-+    def test_toElementVersion(self):
-+        """
-+        If the roster version is set, a 'ver' attribute is added.
-+        """
-+        request = xmppim.RosterRequest()
-+        request.version = 'ver72'
-+        element = request.toElement()
-+        self.assertEqual('ver72', element.query.getAttribute('ver'))
-+
-+
-+    def test_toElementVersionEmpty(self):
-+        """
-+        If the roster version is the empty string, it should add 'ver', too.
-+        """
-+        request = xmppim.RosterRequest()
-+        request.version = ''
-+        element = request.toElement()
-+        self.assertEqual('', element.query.getAttribute('ver'))
-+
-+
-+
-+class RosterClientProtocolTest(unittest.TestCase, TestableRequestHandlerMixin):
-     """
-     Tests for L{xmppim.RosterClientProtocol}.
-     """
- 
-     def setUp(self):
-         self.stub = XmlStreamStub()
--        self.protocol = xmppim.RosterClientProtocol()
--        self.protocol.xmlstream = self.stub.xmlstream
--        self.protocol.connectionInitialized()
-+        self.patch(XMPPHandler, 'request', self.request)
-+        self.service = xmppim.RosterClientProtocol()
-+        self.service.makeConnection(self.stub.xmlstream)
-+        self.service.connectionInitialized()
-+
-+
-+    def request(self, request):
-+        """
-+        Stub for the request method on XMPPHandler.
-+        """
-+        element = request.toElement()
-+        self.stub.xmlstream.send(element)
-+        return defer.Deferred()
- 
- 
-     def test_removeItem(self):
-         """
-         Removing a roster item is setting an item with subscription C{remove}.
-         """
--        d = self.protocol.removeItem(JID('test@example.org'))
-+        d = self.service.removeItem(JID('test@example.org'))
- 
-         # Inspect outgoing iq request
- 
-         iq = self.stub.output[-1]
--        self.assertEquals('set', iq.getAttribute('type'))
-+        self.assertEqual('set', iq.getAttribute('type'))
-         self.assertNotIdentical(None, iq.query)
--        self.assertEquals(NS_ROSTER, iq.query.uri)
-+        self.assertEqual(NS_ROSTER, iq.query.uri)
- 
-         children = list(domish.generateElementsQNamed(iq.query.children,
-                                                       'item', NS_ROSTER))
--        self.assertEquals(1, len(children))
-+        self.assertEqual(1, len(children))
-         child = children[0]
--        self.assertEquals('test@example.org', child['jid'])
--        self.assertEquals('remove', child['subscription'])
-+        self.assertEqual('test@example.org', child['jid'])
-+        self.assertEqual('remove', child.getAttribute('subscription'))
- 
-         # Fake successful response
- 
-         response = toResponse(iq, 'result')
--        self.stub.send(response)
-+        d.callback(response)
-         return d
-+
-+
-+    def test_getRoster(self):
-+        """
-+        A request for the roster is sent out and the response is parsed.
-+        """
-+        def cb(roster):
-+            self.assertIn(JID('user@example.org'), roster)
-+
-+        d = self.service.getRoster()
-+        d.addCallback(cb)
-+
-+        # Inspect outgoing iq request
-+
-+        iq = self.stub.output[-1]
-+        self.assertEqual('get', iq.getAttribute('type'))
-+        self.assertNotIdentical(None, iq.query)
-+        self.assertEqual(NS_ROSTER, iq.query.uri)
-+
-+        # Fake successful response
-+        response = toResponse(iq, 'result')
-+        query = response.addElement((NS_ROSTER, 'query'))
-+        item = query.addElement('item')
-+        item['jid'] = 'user@example.org'
-+
-+        d.callback(response)
-+        return d
-+
-+
-+    def test_onRosterSet(self):
-+        """
-+        A roster push causes onRosterSet to be called with the parsed item.
-+        """
-+        xml = """
-+          <iq type='set'>
-+            <query xmlns='jabber:iq:roster'>
-+              <item jid='user@example.org'/>
-+            </query>
-+          </iq>
-+        """
-+
-+        items = []
-+
-+        def onRosterSet(item):
-+            items.append(item)
-+
-+        def cb(result):
-+            self.assertEqual(1, len(items))
-+            self.assertEqual(JID('user@example.org'), items[0].entity)
-+
-+        self.service.onRosterSet = onRosterSet
-+
-+        d = self.assertWarns(DeprecationWarning,
-+                             "wokkel.xmppim.RosterClientProtocol.onRosterSet "
-+                             "is deprecated. "
-+                             "Use RosterClientProtocol.push instead.",
-+                             xmppim.__file__,
-+                             self.handleRequest, xml)
-+        d.addCallback(cb)
-+        return d
-+
-+
-+    def test_onRosterRemove(self):
-+        """
-+        A roster push causes onRosterSet to be called with the parsed item.
-+        """
-+        xml = """
-+          <iq type='set'>
-+            <query xmlns='jabber:iq:roster'>
-+              <item jid='user@example.org' subscription='remove'/>
-+            </query>
-+          </iq>
-+        """
-+
-+        entities = []
-+
-+        def onRosterRemove(entity):
-+            entities.append(entity)
-+
-+        def cb(result):
-+            self.assertEqual([JID('user@example.org')], entities)
-+
-+        self.service.onRosterRemove = onRosterRemove
-+
-+        d = self.assertWarns(DeprecationWarning,
-+                             "wokkel.xmppim.RosterClientProtocol.onRosterRemove "
-+                             "is deprecated. "
-+                             "Use RosterClientProtocol.push instead.",
-+                             xmppim.__file__,
-+                             self.handleRequest, xml)
-+        d.addCallback(cb)
-+        return d
-+
-+
-+    def test_push(self):
-+        """
-+        A roster push causes push to be called with the parsed request.
-+        """
-+        xml = """
-+          <iq type='set'>
-+            <query xmlns='jabber:iq:roster'>
-+              <item jid='user@example.org'/>
-+            </query>
-+          </iq>
-+        """
-+
-+        requests = []
-+
-+        def push(request):
-+            requests.append(request)
-+
-+        def cb(result):
-+            self.assertEqual(1, len(requests), "push was not called")
-+            self.assertEqual(JID('user@example.org'), requests[0].item.entity)
-+
-+        self.service.push = push
-+
-+        d = self.handleRequest(xml)
-+        d.addCallback(cb)
-+        return d
-diff --git a/wokkel/xmppim.py b/wokkel/xmppim.py
---- a/wokkel/xmppim.py
-+++ b/wokkel/xmppim.py
-@@ -7,21 +7,26 @@
- XMPP IM protocol support.
- 
- This module provides generic implementations for the protocols defined in
--U{RFC 3921<http://xmpp.org/rfcs/rfc3921.html>} (XMPP IM).
--
--All of it should eventually move to Twisted.
-+U{RFC 6121<http://www.xmpp.org/rfcs/rfc6121.html>} (XMPP IM).
- """
- 
-+import warnings
-+
-+from twisted.internet import defer
- from twisted.words.protocols.jabber.jid import JID
- from twisted.words.xish import domish
- 
--from wokkel.compat import IQ
--from wokkel.generic import ErrorStanza, Stanza
-+from wokkel.generic import ErrorStanza, Stanza, Request
-+from wokkel.subprotocols import IQHandlerMixin
- from wokkel.subprotocols import XMPPHandler
- 
- NS_XML = 'http://www.w3.org/XML/1998/namespace'
- NS_ROSTER = 'jabber:iq:roster'
- 
-+XPATH_ROSTER_SET = "/iq[@type='set']/query[@xmlns='%s']" % NS_ROSTER
-+
-+
-+
- class Presence(domish.Element):
-     def __init__(self, to=None, type=None):
-         domish.Element.__init__(self, (None, "presence"))
-@@ -605,8 +610,8 @@
- 
-     This represents one contact from an XMPP contact list known as roster.
- 
--    @ivar jid: The JID of the contact.
--    @type jid: L{JID}
-+    @ivar entity: The JID of the contact.
-+    @type entity: L{JID}
-     @ivar name: The optional associated nickname for this contact.
-     @type name: C{unicode}
-     @ivar subscriptionTo: Subscription state to contact's presence. If C{True},
-@@ -616,45 +621,174 @@
-     @ivar subscriptionFrom: Contact's subscription state. If C{True}, the
-                             contact is subscribed to the presence information
-                             of the roster owner.
--    @type subscriptionTo: C{bool}
--    @ivar ask: Whether subscription is pending.
--    @type ask: C{bool}
-+    @type subscriptionFrom: C{bool}
-+    @ivar pendingOut: Whether the subscription request to this contact is
-+        pending.
-+    @type pendingOut: C{bool}
-     @ivar groups: Set of groups this contact is categorized in. Groups are
-                   represented by an opaque identifier of type C{unicode}.
-     @type groups: C{set}
-+    @ivar approved: Signals pre-approved subscription.
-+    @type approved: C{bool}
-+    @ivar remove: Signals roster item removal.
-+    @type remove: C{bool}
-     """
- 
--    def __init__(self, jid):
--        self.jid = jid
--        self.name = None
--        self.subscriptionTo = False
--        self.subscriptionFrom = False
--        self.ask = None
--        self.groups = set()
-+    __subscriptionStates = {(False, False): None,
-+                            (True, False): 'to',
-+                            (False, True): 'from',
-+                            (True, True): 'both'}
- 
-+    def __init__(self, entity, subscriptionTo=False, subscriptionFrom=False,
-+                       name=None, groups=None):
-+        self.entity = entity
-+        self.subscriptionTo = subscriptionTo
-+        self.subscriptionFrom = subscriptionFrom
-+        self.name = name
-+        self.groups = groups or set()
- 
--class RosterClientProtocol(XMPPHandler):
-+        self.pendingOut = False
-+        self.approved = False
-+        self.remove = False
-+
-+
-+    def __getJID(self):
-+        warnings.warn(
-+            "wokkel.xmppim.RosterItem.jid is deprecated. "
-+            "Use RosterItem.entity instead.",
-+            DeprecationWarning)
-+        return self.entity
-+
-+
-+    def __setJID(self, value):
-+        warnings.warn(
-+            "wokkel.xmppim.RosterItem.jid is deprecated. "
-+            "Use RosterItem.entity instead.",
-+            DeprecationWarning)
-+        self.entity = value
-+
-+
-+    jid = property(__getJID, __setJID, doc="""
-+            JID of the contact. Deprecated in favour of C{entity}.""")
-+
-+
-+    def __getAsk(self):
-+        warnings.warn(
-+            "wokkel.xmppim.RosterItem.ask is deprecated. "
-+            "Use RosterItem.pendingOut instead.",
-+            DeprecationWarning)
-+        return self.pendingOut
-+
-+
-+    def __setAsk(self, value):
-+        warnings.warn(
-+            "wokkel.xmppim.RosterItem.ask is deprecated. "
-+            "Use RosterItem.pendingOut instead.",
-+            DeprecationWarning)
-+        self.pendingOut = value
-+
-+
-+    ask = property(__getAsk, __setAsk, doc="""
-+            Pending out subscription. Deprecated in favour of C{pendingOut}.""")
-+
-+
-+    def toElement(self):
-+        element = domish.Element((NS_ROSTER, 'item'))
-+        element['jid'] = self.entity.full()
-+
-+        if self.remove:
-+            subscription = 'remove'
-+        else:
-+            subscription = self.__subscriptionStates[self.subscriptionTo,
-+                                                     self.subscriptionFrom]
-+
-+            if self.pendingOut:
-+                element['ask'] = u'subscribe'
-+
-+            if self.name:
-+                element['name'] = self.name
-+
-+            if self.approved:
-+                element['approved'] = u'true'
-+
-+            if self.groups:
-+                for group in self.groups:
-+                    element.addElement('group', content=group)
-+
-+        if subscription:
-+            element['subscription'] = subscription
-+
-+        return element
-+
-+
-+    @classmethod
-+    def fromElement(Class, element):
-+        entity = JID(element['jid'])
-+        item = Class(entity)
-+        subscription = element.getAttribute('subscription')
-+        if subscription == 'remove':
-+            item.remove = True
-+        else:
-+            item.name = element.getAttribute('name')
-+            item.subscriptionTo = subscription in ('to', 'both')
-+            item.subscriptionFrom = subscription in ('from', 'both')
-+            item.pendingOut = element.getAttribute('ask') == 'subscribe'
-+            item.approved = element.getAttribute('approved') in ('true', '1')
-+            for subElement in domish.generateElementsQNamed(element.children,
-+                                                            'group', NS_ROSTER):
-+                item.groups.add(unicode(subElement))
-+        return item
-+
-+
-+
-+class RosterRequest(Request):
-+    """
-+    Roster request.
-+
-+    @ivar item: Roster item to be set or pushed.
-+    @type item: L{RosterItem}.
-+
-+    @ivar version: Roster version identifier for roster pushes and
-+        retrieving the roster as a delta from a known cached version. This
-+        should only be set if the recipient is known to support roster
-+        versioning.
-+    @type version: C{unicode}
-+    """
-+    item = None
-+    version = None
-+
-+    childParsers = {(NS_ROSTER, 'query'): '_childParser_query'}
-+
-+    def _childParser_query(self, element):
-+        self.version = element.getAttribute('ver')
-+
-+        for child in element.elements(NS_ROSTER, 'item'):
-+            self.item = RosterItem.fromElement(child)
-+            break
-+
-+
-+    def toElement(self):
-+        element = Request.toElement(self)
-+        query = element.addElement((NS_ROSTER, 'query'))
-+        if self.version is not None:
-+            query['ver'] = self.version
-+        if self.item:
-+            query.addChild(self.item.toElement())
-+        return element
-+
-+
-+
-+class RosterClientProtocol(XMPPHandler, IQHandlerMixin):
-     """
-     Client side XMPP roster protocol.
-     """
- 
-+    iqHandlers = {XPATH_ROSTER_SET: "_onRosterSet"}
-+
-+
-     def connectionInitialized(self):
--        ROSTER_SET = "/iq[@type='set']/query[@xmlns='%s']" % NS_ROSTER
--        self.xmlstream.addObserver(ROSTER_SET, self._onRosterSet)
-+        self.xmlstream.addObserver(XPATH_ROSTER_SET, self.handleRequest)
- 
--    def _parseRosterItem(self, element):
--        jid = JID(element['jid'])
--        item = RosterItem(jid)
--        item.name = element.getAttribute('name')
--        subscription = element.getAttribute('subscription')
--        item.subscriptionTo = subscription in ('to', 'both')
--        item.subscriptionFrom = subscription in ('from', 'both')
--        item.ask = element.getAttribute('ask') == 'subscribe'
--        for subElement in domish.generateElementsQNamed(element.children,
--                                                        'group', NS_ROSTER):
--            item.groups.add(unicode(subElement))
--
--        return item
- 
-     def getRoster(self):
-         """
-@@ -668,14 +802,13 @@
-             roster = {}
-             for element in domish.generateElementsQNamed(result.query.children,
-                                                          'item', NS_ROSTER):
--                item = self._parseRosterItem(element)
--                roster[item.jid.userhost()] = item
-+                item = RosterItem.fromElement(element)
-+                roster[item.entity] = item
- 
-             return roster
- 
--        iq = IQ(self.xmlstream, 'get')
--        iq.addElement((NS_ROSTER, 'query'))
--        d = iq.send()
-+        request = RosterRequest(stanzaType='get')
-+        d = self.request(request)
-         d.addCallback(processRoster)
-         return d
- 
-@@ -688,44 +821,50 @@
-         @type entity: L{JID<twisted.words.protocols.jabber.jid.JID>}
-         @rtype: L{twisted.internet.defer.Deferred}
-         """
--        iq = IQ(self.xmlstream, 'set')
--        iq.addElement((NS_ROSTER, 'query'))
--        item = iq.query.addElement('item')
--        item['jid'] = entity.full()
--        item['subscription'] = 'remove'
--        return iq.send()
-+        request = RosterRequest(stanzaType='set')
-+        request.item = RosterItem(entity)
-+        request.item.remove = True
-+        return self.request(request)
- 
- 
-     def _onRosterSet(self, iq):
--        if iq.handled or \
--           iq.hasAttribute('from') and iq['from'] != self.xmlstream:
--            return
-+        request = RosterRequest.fromElement(iq)
- 
--        iq.handled = True
-+        d = defer.maybeDeferred(self.push, request)
-+        return d
- 
--        itemElement = iq.query.item
- 
--        if unicode(itemElement['subscription']) == 'remove':
--            self.onRosterRemove(JID(itemElement['jid']))
-+    def push(self, request):
-+        """
-+        Called when a roster push was received.
-+
-+        Override this to handle roster pushes.
-+
-+        For backwards compatibility, the default implementation calls
-+        the deprecated C{onRosterSet} or C{onRosterRemove} if defined on
-+        C{self}.
-+
-+        @param request: The push request.
-+        @type request: L{RosterRequest}
-+        """
-+        item = request.item
-+
-+        if item.remove:
-+            if hasattr(self, 'onRosterRemove'):
-+                warnings.warn(
-+                    "wokkel.xmppim.RosterClientProtocol.onRosterRemove "
-+                    "is deprecated. "
-+                    "Use RosterClientProtocol.push instead.",
-+                    DeprecationWarning)
-+                return defer.maybeDeferred(self.onRosterRemove, item.entity)
-         else:
--            item = self._parseRosterItem(iq.query.item)
--            self.onRosterSet(item)
--
--    def onRosterSet(self, item):
--        """
--        Called when a roster push for a new or update item was received.
--
--        @param item: The pushed roster item.
--        @type item: L{RosterItem}
--        """
--
--    def onRosterRemove(self, entity):
--        """
--        Called when a roster push for the removal of an item was received.
--
--        @param entity: The entity for which the roster item has been removed.
--        @type entity: L{JID}
--        """
-+            if hasattr(self, 'onRosterSet'):
-+                warnings.warn(
-+                    "wokkel.xmppim.RosterClientProtocol.onRosterSet "
-+                    "is deprecated. "
-+                    "Use RosterClientProtocol.push instead.",
-+                    DeprecationWarning)
-+                return defer.maybeDeferred(self.onRosterSet, item)
- 
- 
- 

roster_item_sender.patch

-# HG changeset patch
-# Parent a37017f764f8fd1f02f18b05d3a7e0a1e7973c33
-Record sender on received roster sets to support alternative roster sources.
-
- * `RosterItem` gains `sender` attribute that holds the JID of the sender
-   entity from which the roster push was received.
- * `pushReceived` can raise `RosterPushIgnored` to return a
-   `service-unavailable` stanza error for unwanted pushes.
-
-diff --git a/wokkel/test/test_xmppim.py b/wokkel/test/test_xmppim.py
---- a/wokkel/test/test_xmppim.py
-+++ b/wokkel/test/test_xmppim.py
-@@ -7,6 +7,7 @@
- 
- from twisted.internet import defer
- from twisted.trial import unittest
-+from twisted.words.protocols.jabber import error
- from twisted.words.protocols.jabber.jid import JID
- from twisted.words.protocols.jabber.xmlstream import toResponse
- from twisted.words.xish import domish, utility
-@@ -944,6 +945,8 @@
-         requests = []
- 
-         def push(request):
-+            self.assertIs(None, request.recipient)
-+            self.assertIs(None, request.sender)
-             requests.append(request)
- 
-         def cb(result):
-@@ -955,3 +958,66 @@
-         d = self.handleRequest(xml)
-         d.addCallback(cb)
-         return d
-+
-+
-+    def test_pushOtherSource(self):
-+        """
-+        Roster pushes can be sent from other entities, too.
-+        """
-+        xml = """
-+          <iq type='set' to='this@example.org/Home' from='other@example.org'>
-+            <query xmlns='jabber:iq:roster'>
-+              <item jid='user@example.org'/>
-+            </query>
-+          </iq>
-+        """
-+
-+        requests = []
-+
-+        def push(request):
-+            requests.append(request)
-+
-+        def cb(result):
-+            request = requests[0]
-+            self.assertEquals(JID('this@example.org/Home'), request.recipient)
-+            self.assertEquals(JID('other@example.org'), request.sender)
-+
-+        self.service.push = push
-+
-+        d = self.handleRequest(xml)
-+        d.addCallback(cb)
-+        return d
-+
-+
-+    def test_pushReceivedIgnored(self):
-+        """
-+        Ignored roster pushes return a service unavailable error.
-+        """
-+        xml = """
-+          <iq type='set' to='this@example.org/Home' from='bad@example.org'>
-+            <query xmlns='jabber:iq:roster'>
-+              <item jid='user@example.org'/>
-+            </query>
-+          </iq>
-+        """
-+
-+        def push(request):
-+            """
-+            Explicitly ignore pushes from other entities.
-+
-+            This assumes the recipient address is the same as the user, which
-+            should hold for regular XMPP client connections.
-+            """
-+            if (request.sender and
-+                request.sender.userhost() != request.recipient.userhost()):
-+                raise xmppim.RosterPushIgnored()
-+
-+        def cb(result):
-+            self.assertEquals('service-unavailable', result.condition)
-+
-+        self.service.push = push
-+
-+        d = self.handleRequest(xml)
-+        self.assertFailure(d, error.StanzaError)
-+        d.addCallback(cb)
-+        return d
-diff --git a/wokkel/xmppim.py b/wokkel/xmppim.py
---- a/wokkel/xmppim.py
-+++ b/wokkel/xmppim.py
-@@ -13,6 +13,7 @@
- import warnings
- 
- from twisted.internet import defer
-+from twisted.words.protocols.jabber import error
- from twisted.words.protocols.jabber.jid import JID
- from twisted.words.xish import domish
- 
-@@ -776,6 +777,13 @@
- 
- 
- 
-+class RosterPushIgnored(Exception):
-+    """
-+    Raised when this entity doesn't want to accept/trust a roster push.
-+    """
-+
-+
-+
- class RosterClientProtocol(XMPPHandler, IQHandlerMixin):
-     """
-     Client side XMPP roster protocol.
-@@ -826,9 +834,14 @@
- 
- 
-     def _onRosterSet(self, iq):
-+        def trapIgnored(failure):
-+            failure.trap(RosterPushIgnored)
-+            raise error.StanzaError('service-unavailable')
-+
-         request = RosterRequest.fromElement(iq)
- 
-         d = defer.maybeDeferred(self.push, request)
-+        d.addErrback(trapIgnored)
-         return d
- 
- 
-@@ -838,6 +851,16 @@
- 
-         Override this to handle roster pushes.
- 
-+        RFC 6121 specifically allows entities other than a user's server to
-+        hold a roster for that user. However, how a client should deal with
-+        that is currently not yet specfied. For now, it is advisable to ignore
-+        roster pushes from other entities. I.e. when C{request.sender} is set
-+        but the sender's bare JID is different from the user's bare JID.
-+
-+        To avert presence leaks, a handler can raise L{RosterPushIgnored} when
-+        not accepting a roster push (directly or via Deferred). This will
-+        result in a L{'service-unavailable'} error being sent in return.
-+
-         For backwards compatibility, the default implementation calls
-         the deprecated C{onRosterSet} or C{onRosterRemove} if defined on
-         C{self}.
-roster_item.patch #+c2s
-roster_item_sender.patch
 pubsub_client_example.patch
 
 copy_xmppim.patch #+c2s