Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions wokkel/iwokkel.py
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,22 @@ def notifyPublish(service, nodeIdentifier, notifications):
L{Element<twisted.words.xish.domish.Element>})
"""

def notifyRetract(service, nodeIdentifier, notifications):
"""
Send out items retraction notifications.

@param service: The entity the notifications will originate from.
@type service: L{JID<twisted.words.protocols.jabber.jid.JID>}
@param nodeIdentifier: The identifier of the node that was retracted
from.
@type nodeIdentifier: C{unicode}
@param notifications: The notifications as tuples of subscriber, the
list of subscriptions and the list of items to be notified.
@type notifications: C{list} of
(L{JID<twisted.words.protocols.jabber.jid.JID>}, C{list} of
L{Subscription<wokkel.pubsub.Subscription>}, C{list} of
L{Element<twisted.words.xish.domish.Element>})
"""

def notifyDelete(service, nodeIdentifier, subscribers,
redirectURI=None):
Expand Down
57 changes: 56 additions & 1 deletion wokkel/pubsub.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,9 @@
'pubsub[@xmlns="' + NS_PUBSUB + '" or ' + \
'@xmlns="' + NS_PUBSUB_OWNER + '"]'

BOOL_TRUE = ('1','true')
BOOL_FALSE = ('0','false')

class SubscriptionPending(Exception):
"""
Raised when the requested subscription is pending acceptance.
Expand Down Expand Up @@ -252,6 +255,7 @@ class PubSubRequest(generic.Stanza):
subscriptionIdentifier = None
subscriptions = None
affiliations = None
notify = None

# Map request iq type and subelement name to request verb
_requestVerbMap = {
Expand Down Expand Up @@ -293,7 +297,7 @@ class PubSubRequest(generic.Stanza):
'configureGet': ['nodeOrEmpty'],
'configureSet': ['nodeOrEmpty', 'configure'],
'items': ['node', 'maxItems', 'itemIdentifiers', 'subidOrNone'],
'retract': ['node', 'itemIdentifiers'],
'retract': ['node', 'notify', 'itemIdentifiers'],
'purge': ['node'],
'delete': ['node'],
'affiliationsGet': ['nodeOrEmpty'],
Expand Down Expand Up @@ -566,6 +570,23 @@ def _parse_affiliations(self, verbElement):
self.affiliations[entity] = affiliation


def _parse_notify(self, verbElement):
value = verbElement.getAttribute('notify')

if value:
if value in BOOL_TRUE:
self.notify = True
elif value in BOOL_FALSE:
self.notify = False
else:
raise BadRequest(text="Field notify must be a boolean value")


def _render_notify(self, verbElement):
if self.notify is not None:
verbElement['notify'] = "true" if self.notify else "false"


def parseElement(self, element):
"""
Parse the publish-subscribe verb and parameters out of a request.
Expand Down Expand Up @@ -955,6 +976,28 @@ def cb(iq):
d.addCallback(cb)
return d

def retractItems(self, service, nodeIdentifier, itemIdentifiers,
notify=None, sender=None):
"""
Retract items from a publish subscribe node.

@param service: The publish subscribe service that keeps the node.
@type service: L{JID<twisted.words.protocols.jabber.jid.JID>}
@param nodeIdentifier: The identifier of the node from which items are
being retracted.
@type nodeIdentifier: C{unicode}
@param itemIdentifiers: Identifiers of the items to be retracted.
@type itemIdentifiers: C{set}
@param notify: True if notification is required
@type notify: C{unicode}
"""
request = PubSubRequest('retract')
request.recipient = service
request.nodeIdentifier = nodeIdentifier
request.itemIdentifiers = itemIdentifiers
request.notify = notify
request.sender = sender
return request.send(self.xmlstream)

def getOptions(self, service, nodeIdentifier, subscriber,
subscriptionIdentifier=None, sender=None):
Expand Down Expand Up @@ -1377,6 +1420,18 @@ def notifyPublish(self, service, nodeIdentifier, notifications):
self.send(message)


def notifyRetract(self, service, nodeIdentifier, notifications):
for subscriber, subscriptions, items in notifications:
message = self._createNotification('items', service,
nodeIdentifier, subscriber,
subscriptions)
for item in items:
retract = domish.Element((NS_PUBSUB_EVENT, "retract"))
retract['id'] = item['id']
message.event.items.addChild(retract)
self.send(message)


def notifyDelete(self, service, nodeIdentifier, subscribers,
redirectURI=None):
for subscriber in subscribers:
Expand Down
91 changes: 91 additions & 0 deletions wokkel/test/test_pubsub.py
Original file line number Diff line number Diff line change
Expand Up @@ -906,6 +906,47 @@ def test_itemsWithSender(self):
return d


def test_retractItems(self):
"""
Test sending items retraction.
"""
d = self.protocol.retractItems(JID('pubsub.example.org'), 'test',
itemIdentifiers=['item1', 'item2'])

iq = self.stub.output[-1]
self.assertEquals('pubsub.example.org', iq.getAttribute('to'))
self.assertEquals('set', iq.getAttribute('type'))
self.assertEquals('pubsub', iq.pubsub.name)
self.assertEquals(NS_PUBSUB, iq.pubsub.uri)
children = list(domish.generateElementsQNamed(iq.pubsub.children,
'retract', NS_PUBSUB))
self.assertEquals(1, len(children))
child = children[0]
self.assertEquals('test', child['node'])
itemIdentifiers = [item.getAttribute('id') for item in
domish.generateElementsQNamed(child.children, 'item',
NS_PUBSUB)]
self.assertEquals(['item1', 'item2'], itemIdentifiers)

self.stub.send(toResponse(iq, 'result'))
return d


def test_retractItemsWithSender(self):
"""
Test retracting items request from a specific JID.
"""
d = self.protocol.retractItems(JID('pubsub.example.org'), 'test',
itemIdentifiers=['item1', 'item2'],
sender=JID('user@example.org'))

iq = self.stub.output[-1]
self.assertEquals('user@example.org', iq['from'])

self.stub.send(toResponse(iq, 'result'))
return d


def test_getOptions(self):
def cb(form):
self.assertEqual('form', form.formType)
Expand Down Expand Up @@ -3152,6 +3193,34 @@ def test_notifyPublish(self):
self.assertEquals('current', itemElements[0].getAttribute('id'))


def test_notifyRetract(self):
"""
Items retraction notifications are sent to the subscribers.
"""
subscriber = JID('user@example.org')
subscriptions = [pubsub.Subscription('test', subscriber, 'subscribed')]
items = [pubsub.Item('current')]
notifications = [(subscriber, subscriptions, items)]
self.service.notifyRetract(JID('pubsub.example.org'), 'test',
notifications)
message = self.stub.output[-1]

self.assertEquals('message', message.name)
self.assertIdentical(None, message.uri)
self.assertEquals('user@example.org', message['to'])
self.assertEquals('pubsub.example.org', message['from'])
self.assertTrue(message.event)
self.assertEquals(NS_PUBSUB_EVENT, message.event.uri)
self.assertTrue(message.event.items)
self.assertEquals(NS_PUBSUB_EVENT, message.event.items.uri)
self.assertTrue(message.event.items.hasAttribute('node'))
self.assertEquals('test', message.event.items['node'])
itemElements = list(domish.generateElementsQNamed(
message.event.items.children, 'retract', NS_PUBSUB_EVENT))
self.assertEquals(1, len(itemElements))
self.assertEquals('current', itemElements[0].getAttribute('id'))


def test_notifyPublishCollection(self):
"""
Publish notifications are sent to the subscribers of collections.
Expand All @@ -3174,6 +3243,28 @@ def test_notifyPublishCollection(self):
self.assertIn('', headers['Collection'])


def test_notifyRetractCollection(self):
"""
Retraction notifications are sent to the subscribers of collections.

The node the item was retracted from is on the C{items} element, while
the subscribed-to node is in the C{'Collections'} SHIM header.
"""
subscriber = JID('user@example.org')
subscriptions = [pubsub.Subscription('', subscriber, 'subscribed')]
items = [pubsub.Item('current')]
notifications = [(subscriber, subscriptions, items)]
self.service.notifyRetract(JID('pubsub.example.org'), 'test',
notifications)
message = self.stub.output[-1]

self.assertTrue(message.event.items.hasAttribute('node'))
self.assertEquals('test', message.event.items['node'])
headers = shim.extractHeaders(message)
self.assertIn('Collection', headers)
self.assertIn('', headers['Collection'])


def test_notifyDelete(self):
"""
Subscribers should be sent a delete notification.
Expand Down