diff options
-rw-r--r-- | sleekxmpp/clientxmpp.py | 13 | ||||
-rw-r--r-- | sleekxmpp/features/__init__.py | 2 | ||||
-rw-r--r-- | sleekxmpp/features/feature_rosterver/__init__.py | 10 | ||||
-rw-r--r-- | sleekxmpp/features/feature_rosterver/rosterver.py | 42 | ||||
-rw-r--r-- | sleekxmpp/features/feature_rosterver/stanza.py | 17 | ||||
-rw-r--r-- | sleekxmpp/roster/item.py | 12 | ||||
-rw-r--r-- | sleekxmpp/roster/single.py | 28 | ||||
-rw-r--r-- | sleekxmpp/stanza/roster.py | 25 |
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): """ |