Add support for roster versioning.

This was XEP-0237, but is now part of RFC 6121.

Roster backends should now expose two additional methods:

version(jid):
    Return the version of the given JID's roster.
set_version(jid, version):
    Update the version of the given JID's roster.

A new state field will be passed to the backend if an item
has been marked for removal. This is 'removed' which will
be set to True.
This commit is contained in:
Lance Stout 2012-03-07 11:44:07 -08:00
parent d41ada6b66
commit a71823dc04
8 changed files with 145 additions and 4 deletions

View File

@ -18,6 +18,7 @@ import logging
from sleekxmpp.stanza import StreamFeatures
from sleekxmpp.basexmpp import BaseXMPP
from sleekxmpp.exceptions import XMPPError
from sleekxmpp.xmlstream import XMLStream
from sleekxmpp.xmlstream.matcher import MatchXPath
from sleekxmpp.xmlstream.handler import Callback
@ -111,6 +112,7 @@ class ClientXMPP(BaseXMPP):
self.register_plugin('feature_session')
self.register_plugin('feature_mechanisms',
pconfig={'use_mech': sasl_mech} if sasl_mech else None)
self.register_plugin('feature_rosterver')
@property
def password(self):
@ -240,6 +242,8 @@ class ClientXMPP(BaseXMPP):
iq = self.Iq()
iq['type'] = 'get'
iq.enable('roster')
if 'rosterver' in self.features:
iq['roster']['ver'] = self.client_roster.version
if not block and callback is None:
callback = lambda resp: self._handle_roster(resp, request=True)
@ -279,15 +283,22 @@ class ClientXMPP(BaseXMPP):
to a request for the roster, and not an
empty acknowledgement from the server.
"""
if iq['from'].bare and iq['from'].bare != self.boundjid.bare:
raise XMPPError(condition='service-unavailable')
if iq['type'] == 'set' or (iq['type'] == 'result' and request):
roster = self.client_roster
if iq['roster']['ver']:
roster.version = iq['roster']['ver']
for jid in iq['roster']['items']:
item = iq['roster']['items'][jid]
roster = self.roster[iq['to'].bare]
roster[jid]['name'] = item['name']
roster[jid]['groups'] = item['groups']
roster[jid]['from'] = item['subscription'] in ['from', 'both']
roster[jid]['to'] = item['subscription'] in ['to', 'both']
roster[jid]['pending_out'] = (item['ask'] == 'subscribe')
roster[jid].save(remove=(item['subscription'] == 'remove'))
self.event('roster_received', iq)
self.event("roster_update", iq)

View File

@ -6,4 +6,4 @@
See the file LICENSE for copying permission.
"""
__all__ = ['feature_starttls', 'feature_mechanisms', 'feature_bind']
__all__ = ['feature_starttls', 'feature_mechanisms', 'feature_bind', 'feature_rosterver']

View File

@ -0,0 +1,10 @@
"""
SleekXMPP: The Sleek XMPP Library
Copyright (C) 2012 Nathanael C. Fritz
This file is part of SleekXMPP.
See the file LICENSE for copying permission.
"""
from sleekxmpp.features.feature_rosterver.rosterver import feature_rosterver
from sleekxmpp.features.feature_rosterver.stanza import RosterVer

View File

@ -0,0 +1,42 @@
"""
SleekXMPP: The Sleek XMPP Library
Copyright (C) 2012 Nathanael C. Fritz
This file is part of SleekXMPP.
See the file LICENSE for copying permission.
"""
import logging
from sleekxmpp.stanza import Iq, StreamFeatures
from sleekxmpp.features.feature_rosterver import stanza
from sleekxmpp.xmlstream import register_stanza_plugin
from sleekxmpp.plugins.base import base_plugin
log = logging.getLogger(__name__)
class feature_rosterver(base_plugin):
def plugin_init(self):
self.name = 'Roster Versioning'
self.rfc = '6121'
self.description = 'Roster Versioning'
self.stanza = stanza
self.xmpp.register_feature('rosterver',
self._handle_rosterver,
restart=False,
order=9000)
register_stanza_plugin(StreamFeatures, stanza.RosterVer)
def _handle_rosterver(self, features):
"""Enable using roster versioning.
Arguments:
features -- The stream features stanza.
"""
log.debug("Enabling roster versioning.")
self.xmpp.features.add('rosterver')

View File

@ -0,0 +1,17 @@
"""
SleekXMPP: The Sleek XMPP Library
Copyright (C) 2012 Nathanael C. Fritz
This file is part of SleekXMPP.
See the file LICENSE for copying permission.
"""
from sleekxmpp.xmlstream import ElementBase
class RosterVer(ElementBase):
name = 'ver'
namespace = 'urn:xmpp:features:rosterver'
interfaces = set()
plugin_attrib = 'rosterver'

View File

@ -134,6 +134,7 @@ class RosterItem(object):
'subscription': 'none',
'name': '',
'groups': []}
self._db_state = {}
self.load()
@ -171,16 +172,25 @@ class RosterItem(object):
return self._state
return None
def save(self):
def save(self, remove=False):
"""
Save the item's state information to an external datastore,
if one has been provided.
Arguments:
remove -- If True, expunge the item from the datastore.
"""
self['subscription'] = self._subscription()
if remove:
self._state['removed'] = True
if self.db:
self.db.save(self.owner, self.jid,
self._state, self._db_state)
# Finally, remove the in-memory copy if needed.
if remove:
del self.xmpp.roster[self.owner][self.jid]
def __getitem__(self, key):
"""Return a state field's value."""
if key in self._state:

View File

@ -57,12 +57,29 @@ class RosterNode(object):
self.auto_authorize = True
self.auto_subscribe = True
self.last_status = None
self._version = ''
self._jids = {}
if self.db:
if hasattr(self.db, 'version'):
self._version = self.db.version(self.jid)
for jid in self.db.entries(self.jid):
self.add(jid)
@property
def version(self):
"""Retrieve the roster's version ID."""
if self.db and hasattr(self.db, 'version'):
self._version = self.db.version(self.jid)
return self._version
@version.setter
def version(self, version):
"""Set the roster's version ID."""
self._version = version
if self.db and hasattr(self.db, 'set_version'):
self.db.set_version(self.jid, version)
def __getitem__(self, key):
"""
Return the roster item for a subscribed JID.
@ -75,6 +92,17 @@ class RosterNode(object):
self.add(key, save=True)
return self._jids[key]
def __delitem__(self, key):
"""
Remove a roster item from the local storage.
To remove an item from the server, use the remove() method.
"""
if isinstance(key, JID):
key = key.bare
if key in self._jids:
del self._jids[key]
def __len__(self):
"""Return the number of JIDs referenced by the roster."""
return len(self._jids)

View File

@ -36,7 +36,30 @@ class Roster(ElementBase):
namespace = 'jabber:iq:roster'
name = 'query'
plugin_attrib = 'roster'
interfaces = set(('items',))
interfaces = set(('items', 'ver'))
def get_ver(self):
"""
Ensure handling an empty ver attribute propery.
The ver attribute is special in that the presence of the
attribute with an empty value is important for boostrapping
roster versioning.
"""
return self.xml.attrib.get('ver', None)
def set_ver(self, ver):
"""
Ensure handling an empty ver attribute propery.
The ver attribute is special in that the presence of the
attribute with an empty value is important for boostrapping
roster versioning.
"""
if ver is not None:
self.xml.attrib['ver'] = ver
else:
del self.xml.attrib['ver']
def set_items(self, items):
"""