summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLance Stout <lancestout@gmail.com>2012-03-07 11:44:07 -0800
committerLance Stout <lancestout@gmail.com>2012-03-07 14:55:27 -0800
commita71823dc040062c02a2036db7473845ca07b52f3 (patch)
treec3e97566ec63cd936203684321e1cce1df73fb74
parentd41ada6b66e4fb1b447c2f6aa4f0e227429afbbf (diff)
downloadslixmpp-a71823dc040062c02a2036db7473845ca07b52f3.tar.gz
slixmpp-a71823dc040062c02a2036db7473845ca07b52f3.tar.bz2
slixmpp-a71823dc040062c02a2036db7473845ca07b52f3.tar.xz
slixmpp-a71823dc040062c02a2036db7473845ca07b52f3.zip
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.
-rw-r--r--sleekxmpp/clientxmpp.py13
-rw-r--r--sleekxmpp/features/__init__.py2
-rw-r--r--sleekxmpp/features/feature_rosterver/__init__.py10
-rw-r--r--sleekxmpp/features/feature_rosterver/rosterver.py42
-rw-r--r--sleekxmpp/features/feature_rosterver/stanza.py17
-rw-r--r--sleekxmpp/roster/item.py12
-rw-r--r--sleekxmpp/roster/single.py28
-rw-r--r--sleekxmpp/stanza/roster.py25
8 files changed, 145 insertions, 4 deletions
diff --git a/sleekxmpp/clientxmpp.py b/sleekxmpp/clientxmpp.py
index 458af2b7..590192db 100644
--- a/sleekxmpp/clientxmpp.py
+++ b/sleekxmpp/clientxmpp.py
@@ -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)
diff --git a/sleekxmpp/features/__init__.py b/sleekxmpp/features/__init__.py
index 5bfe173d..f84961d6 100644
--- a/sleekxmpp/features/__init__.py
+++ b/sleekxmpp/features/__init__.py
@@ -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']
diff --git a/sleekxmpp/features/feature_rosterver/__init__.py b/sleekxmpp/features/feature_rosterver/__init__.py
new file mode 100644
index 00000000..c83dd01c
--- /dev/null
+++ b/sleekxmpp/features/feature_rosterver/__init__.py
@@ -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
diff --git a/sleekxmpp/features/feature_rosterver/rosterver.py b/sleekxmpp/features/feature_rosterver/rosterver.py
new file mode 100644
index 00000000..059d20e1
--- /dev/null
+++ b/sleekxmpp/features/feature_rosterver/rosterver.py
@@ -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')
diff --git a/sleekxmpp/features/feature_rosterver/stanza.py b/sleekxmpp/features/feature_rosterver/stanza.py
new file mode 100644
index 00000000..025872fa
--- /dev/null
+++ b/sleekxmpp/features/feature_rosterver/stanza.py
@@ -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'
diff --git a/sleekxmpp/roster/item.py b/sleekxmpp/roster/item.py
index eb2f64b3..9cb278a4 100644
--- a/sleekxmpp/roster/item.py
+++ b/sleekxmpp/roster/item.py
@@ -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:
diff --git a/sleekxmpp/roster/single.py b/sleekxmpp/roster/single.py
index 159eb07d..518afebe 100644
--- a/sleekxmpp/roster/single.py
+++ b/sleekxmpp/roster/single.py
@@ -57,11 +57,28 @@ 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):
"""
@@ -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)
diff --git a/sleekxmpp/stanza/roster.py b/sleekxmpp/stanza/roster.py
index 0b34d03b..4788ba72 100644
--- a/sleekxmpp/stanza/roster.py
+++ b/sleekxmpp/stanza/roster.py
@@ -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):
"""