summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--sleekxmpp/basexmpp.py187
-rw-r--r--sleekxmpp/clientxmpp.py16
-rw-r--r--sleekxmpp/componentxmpp.py11
-rw-r--r--sleekxmpp/roster.py768
-rw-r--r--sleekxmpp/stanza/roster.py2
-rw-r--r--sleekxmpp/test/sleektest.py26
-rw-r--r--tests/test_stanza_presence.py3
-rw-r--r--tests/test_stanza_roster.py4
-rw-r--r--tests/test_stream_presence.py34
-rw-r--r--tests/test_stream_roster.py39
10 files changed, 952 insertions, 138 deletions
diff --git a/sleekxmpp/basexmpp.py b/sleekxmpp/basexmpp.py
index 8347bfe0..7330b9c2 100644
--- a/sleekxmpp/basexmpp.py
+++ b/sleekxmpp/basexmpp.py
@@ -15,6 +15,7 @@ import logging
import sleekxmpp
from sleekxmpp import plugins
+import sleekxmpp.roster as roster
from sleekxmpp.stanza import Message, Presence, Iq, Error, StreamError
from sleekxmpp.stanza.roster import Roster
from sleekxmpp.stanza.nick import Nick
@@ -78,7 +79,7 @@ class BaseXMPP(XMLStream):
send_presence_subscribe -- Send a subscription request.
"""
- def __init__(self, default_ns='jabber:client'):
+ def __init__(self, jid='', default_ns='jabber:client'):
"""
Adapt an XML stream for use with XMPP.
@@ -93,12 +94,16 @@ class BaseXMPP(XMLStream):
self.default_ns = default_ns
self.stream_ns = 'http://etherx.jabber.org/streams'
- self.boundjid = JID("")
+ self.boundjid = JID(jid)
self.plugin = {}
self.plugin_config = {}
self.plugin_whitelist = []
- self.roster = {}
+
+ self.roster = roster.Roster(self)
+ self.roster.add(self.boundjid.bare)
+ self.client_roster = self.roster[self.boundjid.bare]
+
self.is_component = False
self.auto_authorize = True
self.auto_subscribe = True
@@ -119,10 +124,30 @@ class BaseXMPP(XMLStream):
MatchXPath("{%s}error" % self.stream_ns),
self._handle_stream_error))
- self.add_event_handler('presence_subscribe',
- self._handle_subscribe)
self.add_event_handler('disconnected',
self._handle_disconnected)
+ self.add_event_handler('presence_available',
+ self._handle_available)
+ self.add_event_handler('presence_dnd',
+ self._handle_available)
+ self.add_event_handler('presence_xa',
+ self._handle_available)
+ self.add_event_handler('presence_chat',
+ self._handle_available)
+ self.add_event_handler('presence_away',
+ self._handle_available)
+ self.add_event_handler('presence_unavailable',
+ self._handle_unavailable)
+ self.add_event_handler('presence_subscribe',
+ self._handle_subscribe)
+ self.add_event_handler('presence_subscribed',
+ self._handle_subscribed)
+ self.add_event_handler('presence_unsubscribe',
+ self._handle_unsubscribe)
+ self.add_event_handler('presence_unsubscribed',
+ self._handle_unsubscribed)
+ self.add_event_handler('roster_subscription_request',
+ self._handle_new_subscription)
# Set up the XML stream with XMPP's root stanzas.
self.register_stanza(Message)
@@ -568,7 +593,7 @@ class BaseXMPP(XMLStream):
def _handle_disconnected(self, event):
"""When disconnected, reset the roster"""
- self.roster = {}
+ self.roster.reset()
def _handle_stream_error(self, error):
self.event('stream_error', error)
@@ -577,12 +602,72 @@ class BaseXMPP(XMLStream):
"""Process incoming message stanzas."""
self.event('message', msg)
+ def _handle_available(self, presence):
+ pto = presence['to'].bare
+ pfrom = presence['from'].bare
+ self.roster[pto][pfrom].handle_available(presence)
+
+ def _handle_unavailable(self, presence):
+ pto = presence['to'].bare
+ pfrom = presence['from'].bare
+ self.roster[pto][pfrom].handle_unavailable(presence)
+
+ def _handle_new_subscription(self, stanza):
+ """
+ Attempt to automatically handle subscription requests.
+
+ Subscriptions will be approved if the request is from
+ a whitelisted JID, of self.auto_authorize is True. They
+ will be rejected if self.auto_authorize is False. Setting
+ self.auto_authorize to None will disable automatic
+ subscription handling (except for whitelisted JIDs).
+
+ If a subscription is accepted, a request for a mutual
+ subscription will be sent if self.auto_subscribe is True.
+ """
+ roster = self.roster[stanza['to'].bare]
+ item = self.roster[stanza['to'].bare][stanza['from'].bare]
+ if item['whitelisted']:
+ item.authorize()
+ elif roster.auto_authorize:
+ item.authorize()
+ if roster.auto_subscribe:
+ item.subscribe()
+ elif roster.auto_authorize == False:
+ item.unauthorize()
+
+ def _handle_removed_subscription(self, presence):
+ pto = presence['to'].bare
+ pfrom = presence['from'].bare
+ self.roster[pto][pfrom].unauthorize()
+
+ def _handle_subscribe(self, presence):
+ pto = presence['to'].bare
+ pfrom = presence['from'].bare
+ self.roster[pto][pfrom].handle_subscribe(presence)
+
+ def _handle_subscribed(self, presence):
+ pto = presence['to'].bare
+ pfrom = presence['from'].bare
+ self.roster[pto][pfrom].handle_subscribed(presence)
+
+ def _handle_unsubscribe(self, presence):
+ pto = presence['to'].bare
+ pfrom = presence['from'].bare
+ self.roster[pto][pfrom].handle_unsubscribe(presence)
+
+ def _handle_unsubscribed(self, presence):
+ pto = presence['to'].bare
+ pfrom = presence['from'].bare
+ self.roster[pto][pfrom].handle_unsubscribed(presence)
+
def _handle_presence(self, presence):
"""
Process incoming presence stanzas.
Update the roster with presence information.
"""
+ logging.debug(presence['type'])
self.event("presence_%s" % presence['type'], presence)
# Check for changes in subscription state.
@@ -593,97 +678,7 @@ class BaseXMPP(XMLStream):
elif not presence['type'] in ('available', 'unavailable') and \
not presence['type'] in presence.showtypes:
return
-
- # Strip the information from the stanza.
- jid = presence['from'].bare
- resource = presence['from'].resource
- show = presence['type']
- status = presence['status']
- priority = presence['priority']
-
- was_offline = False
- got_online = False
- old_roster = self.roster.get(jid, {}).get(resource, {})
-
- # Create a new roster entry if needed.
- if not jid in self.roster:
- self.roster[jid] = {'groups': [],
- 'name': '',
- 'subscription': 'none',
- 'presence': {},
- 'in_roster': False}
-
- # Alias to simplify some references.
- connections = self.roster[jid]['presence']
-
- # Determine if the user has just come online.
- if not resource in connections:
- if show == 'available' or show in presence.showtypes:
- got_online = True
- was_offline = True
- connections[resource] = {}
-
- if connections[resource].get('show', 'unavailable') == 'unavailable':
- was_offline = True
-
- # Update the roster's state for this JID's resource.
- connections[resource] = {'show': show,
- 'status': status,
- 'priority': priority}
-
- name = self.roster[jid].get('name', '')
-
- # Remove unneeded state information after a resource
- # disconnects. Determine if this was the last connection
- # for the JID.
- if show == 'unavailable':
- log.debug("%s %s got offline" % (jid, resource))
- del connections[resource]
-
- if not connections and not self.roster[jid]['in_roster']:
- del self.roster[jid]
- if not was_offline:
- self.event("got_offline", presence)
- else:
- return False
-
- name = '(%s) ' % name if name else ''
-
- # Presence state has changed.
self.event("changed_status", presence)
- if got_online:
- self.event("got_online", presence)
- log.debug("STATUS: %s%s/%s[%s]: %s" % (name, jid, resource,
- show, status))
-
- def _handle_subscribe(self, presence):
- """
- Automatically managage subscription requests.
-
- Subscription behavior is controlled by the settings
- self.auto_authorize and self.auto_subscribe.
-
- auto_auth auto_sub Result:
- True True Create bi-directional subsriptions.
- True False Create only directed subscriptions.
- False * Decline all subscriptions.
- None * Disable automatic handling and use
- a custom handler.
- """
- presence.reply()
- presence['to'] = presence['to'].bare
-
- # We are using trinary logic, so conditions have to be
- # more explicit than usual.
- if self.auto_authorize == True:
- presence['type'] = 'subscribed'
- presence.send()
- if self.auto_subscribe:
- presence['type'] = 'subscribe'
- presence.send()
- elif self.auto_authorize == False:
- presence['type'] = 'unsubscribed'
- presence.send()
# Restore the old, lowercased name for backwards compatibility.
basexmpp = BaseXMPP
diff --git a/sleekxmpp/clientxmpp.py b/sleekxmpp/clientxmpp.py
index c518a4ce..34d8008b 100644
--- a/sleekxmpp/clientxmpp.py
+++ b/sleekxmpp/clientxmpp.py
@@ -66,7 +66,7 @@ class ClientXMPP(BaseXMPP):
when calling register_plugins.
escape_quotes -- Deprecated.
"""
- BaseXMPP.__init__(self, 'jabber:client')
+ BaseXMPP.__init__(self, jid, 'jabber:client')
self.set_jid(jid)
self.password = password
@@ -459,13 +459,13 @@ class ClientXMPP(BaseXMPP):
"""
if iq['type'] == 'set' or (iq['type'] == 'result' and request):
for jid in iq['roster']['items']:
- if not jid in self.roster:
- self.roster[jid] = {'groups': [],
- 'name': '',
- 'subscription': 'none',
- 'presence': {},
- 'in_roster': True}
- self.roster[jid].update(iq['roster']['items'][jid])
+ 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')
self.event('roster_received', iq)
self.event("roster_update", iq)
diff --git a/sleekxmpp/componentxmpp.py b/sleekxmpp/componentxmpp.py
index ae58c5f2..121e7c85 100644
--- a/sleekxmpp/componentxmpp.py
+++ b/sleekxmpp/componentxmpp.py
@@ -58,7 +58,7 @@ class ComponentXMPP(BaseXMPP):
default_ns = 'jabber:client'
else:
default_ns = 'jabber:component:accept'
- BaseXMPP.__init__(self, default_ns)
+ BaseXMPP.__init__(self, jid, default_ns)
self.auto_authorize = None
self.stream_header = "<stream:stream %s %s to='%s'>" % (
@@ -68,8 +68,8 @@ class ComponentXMPP(BaseXMPP):
self.stream_footer = "</stream:stream>"
self.server_host = host
self.server_port = port
- self.set_jid(jid)
self.secret = secret
+
self.plugin_config = plugin_config
self.plugin_whitelist = plugin_whitelist
self.is_component = True
@@ -78,6 +78,8 @@ class ComponentXMPP(BaseXMPP):
Callback('Handshake',
MatchXPath('{jabber:component:accept}handshake'),
self._handle_handshake))
+ self.add_event_handler('presence_probe',
+ self._handle_probe)
def connect(self):
"""
@@ -139,3 +141,8 @@ class ComponentXMPP(BaseXMPP):
xml -- The reply handshake stanza.
"""
self.event("session_start")
+
+ def _handle_probe(self, presence):
+ pto = presence['to'].bare
+ pfrom = presence['from'].bare
+ self.roster[pto][pfrom].handle_probe(presence)
diff --git a/sleekxmpp/roster.py b/sleekxmpp/roster.py
new file mode 100644
index 00000000..1972a92e
--- /dev/null
+++ b/sleekxmpp/roster.py
@@ -0,0 +1,768 @@
+"""
+ SleekXMPP: The Sleek XMPP Library
+ Copyright (C) 2010 Nathanael C. Fritz
+ This file is part of SleekXMPP.
+
+ See the file LICENSE for copying permission.
+"""
+
+import logging
+
+
+class Roster(object):
+
+ """
+ SleekXMPP's roster manager.
+
+ The roster is divided into "nodes", where each node is responsible
+ for a single JID. While the distinction is not strictly necessary
+ for client connections, it is a necessity for components that use
+ multiple JIDs.
+
+ Rosters may be stored and persisted in an external datastore. An
+ interface object to the datastore that loads and saves roster items may
+ be provided. See the documentation for the RosterItem class for the
+ methods that the datastore interface object must provide.
+
+ Attributes:
+ xmpp -- The main SleekXMPP instance.
+ db -- Optional interface object to an external datastore.
+ auto_authorize -- Default auto_authorize value for new roster nodes.
+ Defaults to True.
+ auto_subscribe -- Default auto_subscribe value for new roster nodes.
+ Defaults to True.
+
+ Methods:
+ add -- Create a new roster node for a JID.
+ """
+
+ def __init__(self, xmpp, db=None):
+ """
+ Create a new roster.
+
+ Arguments:
+ xmpp -- The main SleekXMPP instance.
+ db -- Optional interface object to a datastore.
+ """
+ self.xmpp = xmpp
+ self.db = db
+ self.auto_authorize = True
+ self.auto_subscribe = True
+ self._rosters = {}
+
+ if self.db:
+ for node in self.db.entries(None, {}):
+ self.add(node)
+
+ def __getitem__(self, key):
+ """
+ Return the roster node for a JID.
+
+ A new roster node will be created if one
+ does not already exist.
+
+ Arguments:
+ key -- Return the roster for this JID.
+ """
+ if not isinstance(key, str):
+ key = key.bare
+ if key not in self._rosters:
+ self.add(key)
+ self._rosters[key].auto_authorize = self.auto_authorize
+ self._rosters[key].auto_subscribe = self.auto_subscribe
+ return self._rosters[key]
+
+ def keys(self):
+ """Return the JIDs managed by the roster."""
+ return self._rosters.keys()
+
+ def __iter__(self):
+ """Iterate over the roster nodes."""
+ return self._rosters.__iter__()
+
+ def add(self, node):
+ """
+ Add a new roster node for the given JID.
+
+ Arguments:
+ node -- The JID for the new roster node.
+ """
+ if not isinstance(node, str):
+ node = node.bare
+ if node not in self._rosters:
+ self._rosters[node] = RosterNode(self.xmpp, node, self.db)
+
+ def set_backend(self, db=None):
+ """
+ Set the datastore interface object for the roster.
+
+ Arguments:
+ db -- The new datastore interface.
+ """
+ self.db = db
+ for node in self.db.entries(None, {}):
+ self.add(node)
+ for node in self._rosters:
+ self._rosters[node].set_backend(db)
+
+ def reset(self):
+ """
+ Reset the state of the roster to forget any current
+ presence information. Useful after a disconnection occurs.
+ """
+ for node in self:
+ self[node].reset()
+
+
+class RosterNode(object):
+
+ """
+ A roster node is a roster for a single JID.
+
+ Attributes:
+ xmpp -- The main SleekXMPP instance.
+ jid -- The JID that owns the roster node.
+ db -- Optional interface to an external datastore.
+ auto_authorize -- Determines how authorizations are handled:
+ True -- Accept all subscriptions.
+ False -- Reject all subscriptions.
+ None -- Subscriptions must be
+ manually authorized.
+ Defaults to True.
+ auto_subscribe -- Determines if bi-directional subscriptions
+ are created after automatically authrorizing
+ a subscription request.
+ Defaults to True
+
+ Methods:
+ add -- Add a JID to the roster.
+ update -- Update a JID's subscription information.
+ subscribe -- Subscribe to a JID.
+ unsubscribe -- Unsubscribe from a JID.
+ remove -- Remove a JID from the roster.
+ presence -- Return presence information for a JID's resources.
+ """
+
+ def __init__(self, xmpp, jid, db=None):
+ """
+ Create a roster node for a JID.
+
+ Arguments:
+ xmpp -- The main SleekXMPP instance.
+ jid -- The JID that owns the roster.
+ db -- Optional interface to an external datastore.
+ """
+ self.xmpp = xmpp
+ self.jid = jid
+ self.db = db
+ self.auto_authorize = True
+ self.auto_subscribe = True
+ self._jids = {}
+
+ if self.db:
+ for jid in self.db.entries(self.jid):
+ self.add(jid)
+
+ def __getitem__(self, key):
+ """
+ Return the roster item for a subscribed JID.
+
+ A new item entry will be created if one does not already exist.
+ """
+ if not isinstance(key, str):
+ key = key.bare
+ if key not in self._jids:
+ self.add(key, save=True)
+ return self._jids[key]
+
+ def keys(self):
+ """Return a list of all subscribed JIDs."""
+ return self._jids.keys()
+
+ def has_jid(self, jid):
+ """Returns whether the roster has a JID."""
+ return jid in self._jids
+
+ def __iter__(self):
+ """Iterate over the roster items."""
+ return self._jids.__iter__()
+
+ def set_backend(self, db=None):
+ """
+ Set the datastore interface object for the roster node.
+
+ Arguments:
+ db -- The new datastore interface.
+ """
+ self.db = db
+ for jid in self.db.entries(self.jid):
+ self.add(jid)
+ for jid in self._jids:
+ self._jids[jid].set_backend(db)
+
+ def add(self, jid, name='', groups=None, afrom=False, ato=False,
+ pending_in=False, pending_out=False, whitelisted=False,
+ save=False):
+ """
+ Add a new roster item entry.
+
+ Arguments:
+ jid -- The JID for the roster item.
+ name -- An alias for the JID.
+ groups -- A list of group names.
+ afrom -- Indicates if the JID has a subscription state
+ of 'from'. Defaults to False.
+ ato -- Indicates if the JID has a subscription state
+ of 'to'. Defaults to False.
+ pending_in -- Indicates if the JID has sent a subscription
+ request to this connection's JID.
+ Defaults to False.
+ pending_out -- Indicates if a subscription request has been sent
+ to this JID.
+ Defaults to False.
+ whitelisted -- Indicates if a subscription request from this JID
+ should be automatically authorized.
+ Defaults to False.
+ save -- Indicates if the item should be persisted
+ immediately to an external datastore,
+ if one is used.
+ Defaults to False.
+ """
+ if not isinstance(jid, str):
+ key = jid.bare
+ state = {'name': name,
+ 'groups': groups or [],
+ 'from': afrom,
+ 'to': ato,
+ 'pending_in': pending_in,
+ 'pending_out': pending_out,
+ 'whitelisted': whitelisted,
+ 'subscription': 'none'}
+ self._jids[jid] = RosterItem(self.xmpp, jid, self.jid,
+ state=state, db=self.db)
+ if save:
+ self._jids[jid].save()
+
+ def subscribe(self, jid):
+ """
+ Subscribe to the given JID.
+
+ Arguments:
+ jid -- The JID to subscribe to.
+ """
+ self[jid].subscribe()
+
+ def unsubscribe(self, jid):
+ """
+ Unsubscribe from the given JID.
+
+ Arguments:
+ jid -- The JID to unsubscribe from.
+ """
+ self[jid].unsubscribe()
+
+ def remove(self, jid):
+ """
+ Remove a JID from the roster.
+
+ Arguments:
+ jid -- The JID to remove.
+ """
+ self[jid].remove()
+ if not self.xmpp.is_component:
+ self.update(jid, subscription='remove')
+
+ def update(self, jid, name=None, subscription=None, groups=[]):
+ """
+ Update a JID's subscription information.
+
+ Arguments:
+ jid -- The JID to update.
+ name -- Optional alias for the JID.
+ subscription -- The subscription state. May be one of: 'to',
+ 'from', 'both', 'none', or 'remove'.
+ groups -- A list of group names.
+ """
+ self[jid]['name'] = name
+ self[jid]['groups'] = group
+ self[jid].save()
+
+ if not self.xmpp.is_component:
+ iq = self.Iq()
+ iq['type'] = 'set'
+ iq['roster']['items'] = {jid: {'name': name,
+ 'subscription': subscription,
+ 'groups': groups}}
+ response = iq.send()
+ return response and response['type'] == 'result'
+
+ def presence(self, jid, resource=None):
+ """
+ Retrieve the presence information of a JID.
+
+ May return either all online resources' status, or
+ a single resource's status.
+
+ Arguments:
+ jid -- The JID to lookup.
+ resource -- Optional resource for returning
+ only the status of a single connection.
+ """
+ if resource is None:
+ return self[jid].resources
+
+ default_presence = {'status': '',
+ 'priority': 0,
+ 'show': ''}
+ return self[jid].resources.get(resource,
+ default_presence)
+
+ def reset(self):
+ """
+ Reset the state of the roster to forget any current
+ presence information. Useful after a disconnection occurs.
+ """
+ for jid in self:
+ self[jid].reset()
+
+
+
+class RosterItem(object):
+
+ """
+ A RosterItem is a single entry in a roster node, and tracks
+ the subscription state and user annotations of a single JID.
+
+ Roster items may use an external datastore to persist roster data
+ across sessions. Client applications will not need to use this
+ functionality, but is intended for components that do not have their
+ roster persisted automatically by the XMPP server.
+
+ Roster items provide many methods for handling incoming presence
+ stanzas that ensure that response stanzas are sent according to
+ RFC 3921.
+
+ The external datastore is accessed through a provided interface
+ object which is stored in self.db. The interface object MUST
+ provide two methods: load and save, both of which are responsible
+ for working with a single roster item. A private dictionary,
+ self._db_state, is used to store any metadata needed by the
+ interface, such as the row ID of a roster item, etc.
+
+ Interface for self.db.load:
+ load(owner_jid, jid, db_state):
+ owner_jid -- The JID that owns the roster.
+ jid -- The JID of the roster item.
+ db_state -- A dictionary containing any data saved
+ by the interface object after a save()
+ call. Will typically have the equivalent
+ of a 'row_id' value.
+
+ Interface for self.db.save:
+ save(owner_jid, jid, item_state, db_state):
+ owner_jid -- The JID that owns the roster.
+ jid -- The JID of the roster item.
+ item_state -- A dictionary containing the fields:
+ 'from', 'to', 'pending_in', 'pending_out',
+ 'whitelisted', 'subscription', 'name',
+ and 'groups'.
+ db_state -- A dictionary provided for persisting
+ datastore specific information. Typically,
+ a value equivalent to 'row_id' will be
+ stored here.
+
+ State Fields:
+ from -- Indicates if a subscription of type 'from'
+ has been authorized.
+ to -- Indicates if a subscription of type 'to' has
+ been authorized.
+ pending_in -- Indicates if a subscription request has been
+ received from this JID and it has not been
+ authorized yet.
+ pending_out -- Indicates if a subscription request has been sent
+ to this JID and it has not been accepted yet.
+ subscription -- Returns one of: 'to', 'from', 'both', or 'none'
+ based on the states of from, to, pending_in,
+ and pending_out. Assignment to this value does
+ not affect the states of the other values.
+ whitelisted -- Indicates if a subscription request from this
+ JID should be automatically accepted.
+ name -- A user supplied alias for the JID.
+ groups -- A list of group names for the JID.
+
+ Attributes:
+ xmpp -- The main SleekXMPP instance.
+ owner -- The JID that owns the roster.
+ jid -- The JID for the roster item.
+ db -- Optional datastore interface object.
+ last_status -- The last presence sent to this JID.
+ resources -- A dictionary of online resources for this JID.
+ Will contain the fields 'show', 'status',
+ and 'priority'.
+
+ Methods:
+ load -- Retrieve the roster item from an
+ external datastore, if one was provided.
+ save -- Save the roster item to an external
+ datastore, if one was provided.
+ remove -- Remove a subscription to the JID and revoke
+ its whitelisted status.
+ subscribe -- Subscribe to the JID.
+ authorize -- Accept a subscription from the JID.
+ unauthorize -- Deny a subscription from the JID.
+ unsubscribe -- Unsubscribe from the JID.
+ send_presence -- Send a directed presence to the JID.
+ send_last_presence -- Resend the last sent presence.
+ handle_available -- Update the JID's resource information.
+ handle_unavailable -- Update the JID's resource information.
+ handle_subscribe -- Handle a subscription request.
+ handle_subscribed -- Handle a notice that a subscription request
+ was authorized by the JID.
+ handle_unsubscribe -- Handle an unsubscribe request.
+ handle_unsubscribed -- Handle a notice that a subscription was
+ removed by the JID.
+ handle_probe -- Handle a presence probe query.
+ """
+
+ def __init__(self, xmpp, jid, owner=None,
+ state=None, db=None):
+ """
+ Create a new roster item.
+
+ Arguments:
+ xmpp -- The main SleekXMPP instance.
+ jid -- The item's JID.
+ owner -- The roster owner's JID. Defaults
+ so self.xmpp.boundjid.bare.
+ state -- A dictionary of initial state values.
+ db -- An optional interface to an external datastore.
+ """
+ self.xmpp = xmpp
+ self.jid = jid
+ self.owner = owner or self.xmpp.boundjid.bare
+ self.last_status = None
+ self.resources = {}
+ self.db = db
+ self._state = state or {
+ 'from': False,
+ 'to': False,
+ 'pending_in': False,
+ 'pending_out': False,
+ 'whitelisted': False,
+ 'subscription': 'none',
+ 'name': '',
+ 'groups': []}
+ self._db_state = {}
+ self.load()
+
+ def set_backend(self, db=None):
+ """
+ Set the datastore interface object for the roster item.
+
+ Arguments:
+ db -- The new datastore interface.
+ """
+ self.db = db
+ self.load()
+
+ def load(self):
+ """
+ Load the item's state information from an external datastore,
+ if one has been provided.
+ """
+ if self.db:
+ item = self.db.load(self.owner, self.jid,
+ self._db_state)
+ if item:
+ self['name'] = item['name']
+ self['groups'] = item['groups']
+ self['from'] = item['from']
+ self['to'] = item['to']
+ self['whitelisted'] = item['whitelisted']
+ self['pending_out'] = item['pending_out']
+ self['pending_in'] = item['pending_in']
+ self['subscription'] = self._subscription()
+ return self._state
+ return None
+
+ def save(self):
+ """
+ Save the item's state information to an external datastore,
+ if one has been provided.
+ """
+ if self.db:
+ self.db.save(self.owner, self.jid,
+ self._state, self._db_state)
+
+ def __getitem__(self, key):
+ """Return a state field's value."""
+ if key in self._state:
+ if key == 'subscription':
+ return self._subscription()
+ return self._state[key]
+ else:
+ raise KeyError
+
+ def __setitem__(self, key, value):
+ """
+ Set the value of a state field.
+
+ For boolean states, the values True, 'true', '1', 'on',
+ and 'yes' are accepted as True; all others are False.
+
+ Arguments:
+ key -- The state field to modify.
+ value -- The new value of the state field.
+ """
+ if key in self._state:
+ if key in ['name', 'subscription', 'groups']:
+ self._state[key] = value
+ else:
+ value = str(value).lower()
+ self._state[key] = value in ('true', '1', 'on', 'yes')
+ else:
+ raise KeyError
+
+ def _subscription(self):
+ """Return the proper subscription type based on current state."""
+ if self['to'] and self['from']:
+ return 'both'
+ elif self['from']:
+ return 'from'
+ elif self['to']:
+ return 'to'
+ else:
+ return 'none'
+
+ def remove(self):
+ """
+ Remove a JID's whitelisted status and unsubscribe if a
+ subscription exists.
+ """
+ if self['to']:
+ p = self.xmpp.Presence()
+ p['to'] = self.jid
+ p['type'] = ['unsubscribe']
+ if self.xmpp.is_component:
+ p['from'] = self.owner
+ p.send()
+ self['to'] = False
+ self['whitelisted'] = False
+ self.save()
+
+ def subscribe(self):
+ """Send a subscription request to the JID."""
+ p = self.xmpp.Presence()
+ p['to'] = self.jid
+ p['type'] = 'subscribe'
+ if self.xmpp.is_component:
+ p['from'] = self.owner
+ self['pending_out'] = True
+ self.save()
+ p.send()
+
+ def authorize(self):
+ """Authorize a received subscription request from the JID."""
+ self['from'] = True
+ self['pending_in'] = False
+ self.save()
+ self._subscribed()
+ self.send_last_presence()
+
+ def unauthorize(self):
+ """Deny a received subscription request from the JID."""
+ self['from'] = False
+ self['pending_in'] = False
+ self.save()
+ self._unsubscribed()
+ p = self.xmpp.Presence()
+ p['to'] = self.jid
+ p['type'] = 'unavailable'
+ if self.xmpp.is_component:
+ p['from'] = self.owner
+ p.send()
+
+ def _subscribed(self):
+ """Handle acknowledging a subscription."""
+ p = self.xmpp.Presence()
+ p['to'] = self.jid
+ p['type'] = 'subscribed'
+ if self.xmpp.is_component:
+ p['from'] = self.owner
+ p.send()
+
+ def unsubscribe(self):
+ """Unsubscribe from the JID."""
+ p = self.xmpp.Presence()
+ p['to'] = self.jid
+ p['type'] = 'unsubscribe'
+ if self.xmpp.is_component:
+ p['from'] = self.owner
+ self.save()
+ p.send()
+
+ def _unsubscribed(self):
+ """Handle acknowledging an unsubscribe request."""
+ p = self.xmpp.Presence()
+ p['to'] = self.jid
+ p['type'] = 'unsubscribed'
+ if self.xmpp.is_component:
+ p['from'] = self.owner
+ p.send()
+
+ def send_presence(self, ptype='available', status=None):
+ p = self.xmpp.Presence()
+ p['to'] = self.jid
+ p['type'] = ptype
+ p['status'] = status
+ if self.xmpp.is_component:
+ p['from'] = self.owner
+ self.last_status = p
+ p.send()
+
+ def send_last_presence(self):
+ if self.last_status is None:
+ self.send_presence()
+ else:
+ self.last_status.send()
+
+ def handle_available(self, presence):
+ resource = presence['from'].resource
+ data = {'status': presence['status'],
+ 'show': presence['show'],
+ 'priority': presence['priority']}
+ if not self.resources:
+ self.xmpp.event('got_online', presence)
+ if resource not in self.resources:
+ self.resources[resource] = {}
+ self.resources[resource].update(data)
+
+ def handle_unavailable(self, presence):
+ resource = presence['from'].resource
+ if not self.resources:
+ return
+ if resource in self.resources:
+ del self.resources[resource]
+ if not self.resources:
+ self.xmpp.event('got_offline', presence)
+
+ def handle_subscribe(self, presence):
+ """
+ +------------------------------------------------------------------+
+ | EXISTING STATE | DELIVER? | NEW STATE |
+ +------------------------------------------------------------------+
+ | "None" | yes | "None + Pending In" |
+ | "None + Pending Out" | yes | "None + Pending Out/In" |
+ | "None + Pending In" | no | no state change |
+ | "None + Pending Out/In" | no | no state change |
+ | "To" | yes | "To + Pending In" |
+ | "To + Pending In" | no | no state change |
+ | "From" | no * | no state change |
+ | "From + Pending Out" | no * | no state change |
+ | "Both" | no * | no state change |
+ +------------------------------------------------------------------+
+ """
+ if self.xmpp.is_component:
+ if not self['from'] and not self['pending_in']:
+ self['pending_in'] = True
+ self.xmpp.event('roster_subscription_request', presence)
+ elif self['from']:
+ self._subscribed()
+ self.save()
+ else:
+ #server shouldn't send an invalid subscription request
+ self.xmpp.event('roster_subscription_request', presence)
+
+ def handle_subscribed(self, presence):
+ """
+ +------------------------------------------------------------------+
+ | EXISTING STATE | DELIVER? | NEW STATE |
+ +------------------------------------------------------------------+
+ | "None" | no | no state change |
+ | "None + Pending Out" | yes | "To" |
+ | "None + Pending In" | no | no state change |
+ | "None + Pending Out/In" | yes | "To + Pending In" |
+ | "To" | no | no state change |
+ | "To + Pending In" | no | no state change |
+ | "From" | no | no state change |
+ | "From + Pending Out" | yes | "Both" |
+ | "Both" | no | no state change |
+ +------------------------------------------------------------------+
+ """
+ if self.xmpp.is_component:
+ if not self['to'] and self['pending_out']:
+ self['pending_out'] = False
+ self['to'] = True
+ self.xmpp.event('roster_subscription_authorized', presence)
+ self.save()
+ else:
+ self.xmpp.event('roster_subscription_authorized', presence)
+
+ def handle_unsubscribe(self, presence):
+ """
+ +------------------------------------------------------------------+
+ | EXISTING STATE | DELIVER? | NEW STATE |
+ +------------------------------------------------------------------+
+ | "None" | no | no state change |
+ | "None + Pending Out" | no | no state change |
+ | "None + Pending In" | yes * | "None" |
+ | "None + Pending Out/In" | yes * | "None + Pending Out" |
+ | "To" | no | no state change |
+ | "To + Pending In" | yes * | "To" |
+ | "From" | yes * | "None" |
+ | "From + Pending Out" | yes * | "None + Pending Out |
+ | "Both" | yes * | "To" |
+ +------------------------------------------------------------------+
+ """
+ if self.xmpp.is_component:
+ if not self['from'] and self['pending_in']:
+ self['pending_in'] = False
+ self._unsubscribed()
+ elif self['from']:
+ self['from'] = False
+ self._unsubscribed()
+ self.xmpp.event('roster_subscription_remove', presence)
+ self.save()
+ else:
+ self.xmpp.event('roster_subscription_remove', presence)
+
+ def handle_unsubscribed(self, presence):
+ """
+ +------------------------------------------------------------------+
+ | EXISTING STATE | DELIVER? | NEW STATE |
+ +------------------------------------------------------------------+
+ | "None" | no | no state change |
+ | "None + Pending Out" | yes | "None" |
+ | "None + Pending In" | no | no state change |
+ | "None + Pending Out/In" | yes | "None + Pending In" |
+ | "To" | yes | "None" |
+ | "To + Pending In" | yes | "None + Pending In" |
+ | "From" | no | no state change |
+ | "From + Pending Out" | yes | "From" |
+ | "Both" | yes | "From" |
+ +------------------------------------------------------------------
+ """
+ if self.xmpp.is_component:
+ if not self['to'] and self['pending_out']:
+ self['pending_out'] = False
+ elif self['to'] and not self['pending_out']:
+ self['to'] = False
+ self.xmpp.event('roster_subscription_removed', presence)
+ self.save()
+ else:
+ self.xmpp.event('roster_subscription_removed', presence)
+
+ def handle_probe(self, presence):
+ if self['to']:
+ self.send_last_presence()
+ if self['pending_out']:
+ self.subscribe()
+ if not self['to']:
+ self._unsubscribed()
+
+ def reset(self):
+ """
+ Forgot current resource presence information as part of
+ a roster reset request.
+ """
+ self.resources = {}
diff --git a/sleekxmpp/stanza/roster.py b/sleekxmpp/stanza/roster.py
index afe75516..3fcdbebc 100644
--- a/sleekxmpp/stanza/roster.py
+++ b/sleekxmpp/stanza/roster.py
@@ -89,6 +89,8 @@ class Roster(ElementBase):
item = {}
item['name'] = itemxml.get('name', '')
item['subscription'] = itemxml.get('subscription', '')
+ item['ask'] = itemxml.get('ask', '')
+ item['approved'] = itemxml.get('approved', '')
item['groups'] = []
groupsxml = itemxml.findall('{jabber:iq:roster}group')
if groupsxml is not None:
diff --git a/sleekxmpp/test/sleektest.py b/sleekxmpp/test/sleektest.py
index fd47a87e..e478e3a7 100644
--- a/sleekxmpp/test/sleektest.py
+++ b/sleekxmpp/test/sleektest.py
@@ -149,6 +149,32 @@ class SleekTest(unittest.TestCase):
self.assertEqual(str(jid), string,
"String does not match: %s" % str(jid))
+ def check_roster(self, owner, jid, name=None, subscription=None,
+ afrom=None, ato=None, pending_out=None, pending_in=None,
+ groups=None):
+ roster = self.xmpp.roster[owner][jid]
+ if name is not None:
+ self.assertEqual(roster['name'], name,
+ "Incorrect name value: %s" % roster['name'])
+ if subscription is not None:
+ self.assertEqual(roster['subscription'], subscription,
+ "Incorrect subscription: %s" % roster['subscription'])
+ if afrom is not None:
+ self.assertEqual(roster['from'], afrom,
+ "Incorrect from state: %s" % roster['from'])
+ if ato is not None:
+ self.assertEqual(roster['to'], ato,
+ "Incorrect to state: %s" % roster['to'])
+ if pending_out is not None:
+ self.assertEqual(roster['pending_out'], pending_out,
+ "Incorrect pending_out state: %s" % roster['pending_out'])
+ if pending_in is not None:
+ self.assertEqual(roster['pending_in'], pending_out,
+ "Incorrect pending_in state: %s" % roster['pending_in'])
+ if groups is not None:
+ self.assertEqual(roster['groups'], groups,
+ "Incorrect groups: %s" % roster['groups'])
+
# ------------------------------------------------------------------
# Methods for comparing stanza objects to XML strings
diff --git a/tests/test_stanza_presence.py b/tests/test_stanza_presence.py
index f9305a31..2ec43b65 100644
--- a/tests/test_stanza_presence.py
+++ b/tests/test_stanza_presence.py
@@ -49,7 +49,8 @@ class TestPresenceStanzas(SleekTest):
self.failUnless(happened == [],
"changed_status event triggered for extra unavailable presence")
- self.failUnless(c.roster == {},
+ roster = c.roster['crap@wherever']
+ self.failUnless(roster['bill@chadmore.com'].resources == {},
"Roster updated for superfulous unavailable presence")
def testNickPlugin(self):
diff --git a/tests/test_stanza_roster.py b/tests/test_stanza_roster.py
index cd3e607c..8ec2d32b 100644
--- a/tests/test_stanza_roster.py
+++ b/tests/test_stanza_roster.py
@@ -48,10 +48,14 @@ class TestRosterStanzas(SleekTest):
'user@example.com': {
'name': 'User',
'subscription': 'both',
+ 'ask': '',
+ 'approved': '',
'groups': ['Friends', 'Coworkers']},
'otheruser@example.com': {
'name': 'Other User',
'subscription': 'both',
+ 'ask': '',
+ 'approved': '',
'groups': []}}
debug = "Roster items don't match after retrieval."
debug += "\nReturned: %s" % str(iq['roster']['items'])
diff --git a/tests/test_stream_presence.py b/tests/test_stream_presence.py
index 1d5caa98..3e0933d7 100644
--- a/tests/test_stream_presence.py
+++ b/tests/test_stream_presence.py
@@ -30,7 +30,9 @@ class TestStreamPresence(SleekTest):
self.xmpp.add_event_handler('presence_unavailable', unavailable)
self.recv("""
- <presence type="unavailable" from="otheruser@localhost" />
+ <presence type="unavailable"
+ from="otheruser@localhost"
+ to="tester@localhost"/>
""")
# Give event queue time to process.
@@ -68,12 +70,14 @@ class TestStreamPresence(SleekTest):
# Contact comes online.
self.recv("""
- <presence from="otheruser@localhost/foobar" />
+ <presence from="otheruser@localhost/foobar"
+ to="tester@localhost" />
""")
# Contact goes offline, should trigger got_offline.
self.recv("""
<presence from="otheruser@localhost/foobar"
+ to="tester@localhost"
type="unavailable" />
""")
@@ -99,7 +103,8 @@ class TestStreamPresence(SleekTest):
self.xmpp.add_event_handler('got_online', got_online)
self.recv("""
- <presence from="user@localhost" />
+ <presence from="user@localhost"
+ to="tester@localhost" />
""")
# Give event queue time to process.
@@ -136,15 +141,23 @@ class TestStreamPresence(SleekTest):
self.xmpp.auto_subscribe = True
self.recv("""
- <presence from="user@localhost" type="subscribe" />
+ <presence from="user@localhost"
+ to="tester@localhost"
+ type="subscribe" />
+ """)
+
+ self.send("""
+ <presence to="user@localhost"
+ type="subscribed" />
""")
self.send("""
- <presence to="user@localhost" type="subscribed" />
+ <presence to="user@localhost" />
""")
self.send("""
- <presence to="user@localhost" type="subscribe" />
+ <presence to="user@localhost"
+ type="subscribe" />
""")
expected = set(('presence_subscribe', 'changed_subscription'))
@@ -170,14 +183,17 @@ class TestStreamPresence(SleekTest):
presence_subscribe)
# With this setting we should reject all subscriptions.
- self.xmpp.auto_authorize = False
+ self.xmpp.roster['tester@localhost'].auto_authorize = False
self.recv("""
- <presence from="user@localhost" type="subscribe" />
+ <presence from="user@localhost"
+ to="tester@localhost"
+ type="subscribe" />
""")
self.send("""
- <presence to="user@localhost" type="unsubscribed" />
+ <presence to="user@localhost"
+ type="unsubscribed" />
""")
expected = set(('presence_subscribe', 'changed_subscription'))
diff --git a/tests/test_stream_roster.py b/tests/test_stream_roster.py
index 731d1145..28d07b51 100644
--- a/tests/test_stream_roster.py
+++ b/tests/test_stream_roster.py
@@ -13,8 +13,7 @@ class TestStreamRoster(SleekTest):
def testGetRoster(self):
"""Test handling roster requests."""
- self.stream_start(mode='client')
- self.failUnless(self.xmpp.roster == {}, "Initial roster not empty.")
+ self.stream_start(mode='client', jid='tester@localhost')
events = []
@@ -33,11 +32,12 @@ class TestStreamRoster(SleekTest):
</iq>
""")
self.recv("""
- <iq type="result" id="1">
+ <iq to='tester@localhost' type="result" id="1">
<query xmlns="jabber:iq:roster">
<item jid="user@localhost"
name="User"
- subscription="both">
+ subscription="from"
+ ask="subscribe">
<group>Friends</group>
<group>Examples</group>
</item>
@@ -48,23 +48,22 @@ class TestStreamRoster(SleekTest):
# Wait for get_roster to return.
t.join()
+ self.check_roster('tester@localhost', 'user@localhost',
+ name='User',
+ subscription='from',
+ afrom=True,
+ pending_out=True,
+ groups=['Friends', 'Examples'])
+
# Give the event queue time to process.
time.sleep(.1)
- roster = {'user@localhost': {'name': 'User',
- 'subscription': 'both',
- 'groups': ['Friends', 'Examples'],
- 'presence': {},
- 'in_roster': True}}
- self.failUnless(self.xmpp.roster == roster,
- "Unexpected roster values: %s" % self.xmpp.roster)
self.failUnless('roster_received' in events,
"Roster received event not triggered: %s" % events)
def testRosterSet(self):
"""Test handling pushed roster updates."""
self.stream_start(mode='client')
- self.failUnless(self.xmpp.roster == {}, "Initial roster not empty.")
events = []
def roster_update(e):
@@ -72,9 +71,8 @@ class TestStreamRoster(SleekTest):
self.xmpp.add_event_handler('roster_update', roster_update)
-
self.recv("""
- <iq type="set" id="1">
+ <iq to='tester@localhost' type="set" id="1">
<query xmlns="jabber:iq:roster">
<item jid="user@localhost"
name="User"
@@ -91,16 +89,14 @@ class TestStreamRoster(SleekTest):
</iq>
""")
+ self.check_roster('tester@localhost', 'user@localhost',
+ name='User',
+ subscription='both',
+ groups=['Friends', 'Examples'])
+
# Give the event queue time to process.
time.sleep(.1)
- roster = {'user@localhost': {'name': 'User',
- 'subscription': 'both',
- 'groups': ['Friends', 'Examples'],
- 'presence': {},
- 'in_roster': True}}
- self.failUnless(self.xmpp.roster == roster,
- "Unexpected roster values: %s" % self.xmpp.roster)
self.failUnless('roster_update' in events,
"Roster updated event not triggered: %s" % events)
@@ -163,5 +159,4 @@ class TestStreamRoster(SleekTest):
"Roster timeout event not triggered: %s." % events)
-
suite = unittest.TestLoader().loadTestsFromTestCase(TestStreamRoster)