summaryrefslogtreecommitdiff
path: root/slixmpp/roster
diff options
context:
space:
mode:
authorFlorent Le Coz <louiz@louiz.org>2014-07-17 14:19:04 +0200
committerFlorent Le Coz <louiz@louiz.org>2014-07-17 14:19:04 +0200
commit5ab77c745270d7d5c016c1dc7ef2a82533a4b16e (patch)
tree259377cc666f8b9c7954fc4e7b8f7a912bcfe101 /slixmpp/roster
parente5582694c07236e6830c20361840360a1dde37f3 (diff)
downloadslixmpp-5ab77c745270d7d5c016c1dc7ef2a82533a4b16e.tar.gz
slixmpp-5ab77c745270d7d5c016c1dc7ef2a82533a4b16e.tar.bz2
slixmpp-5ab77c745270d7d5c016c1dc7ef2a82533a4b16e.tar.xz
slixmpp-5ab77c745270d7d5c016c1dc7ef2a82533a4b16e.zip
Rename to slixmpp
Diffstat (limited to 'slixmpp/roster')
-rw-r--r--slixmpp/roster/__init__.py11
-rw-r--r--slixmpp/roster/item.py497
-rw-r--r--slixmpp/roster/multi.py224
-rw-r--r--slixmpp/roster/single.py337
4 files changed, 1069 insertions, 0 deletions
diff --git a/slixmpp/roster/__init__.py b/slixmpp/roster/__init__.py
new file mode 100644
index 00000000..9a9b69a1
--- /dev/null
+++ b/slixmpp/roster/__init__.py
@@ -0,0 +1,11 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2010 Nathanael C. Fritz
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+from slixmpp.roster.item import RosterItem
+from slixmpp.roster.single import RosterNode
+from slixmpp.roster.multi import Roster
diff --git a/slixmpp/roster/item.py b/slixmpp/roster/item.py
new file mode 100644
index 00000000..c1eb574a
--- /dev/null
+++ b/slixmpp/roster/item.py
@@ -0,0 +1,497 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2010 Nathanael C. Fritz
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+
+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 Slixmpp 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, roster=None):
+ """
+ Create a new roster item.
+
+ Arguments:
+ xmpp -- The main Slixmpp 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.
+ roster -- The roster object containing this entry.
+ """
+ self.xmpp = xmpp
+ self.jid = jid
+ self.owner = owner or self.xmpp.boundjid.bare
+ self.last_status = None
+ self.resources = {}
+ self.roster = roster
+ 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, save=True):
+ """
+ Set the datastore interface object for the roster item.
+
+ Arguments:
+ db -- The new datastore interface.
+ save -- If True, save the existing state to the new
+ backend datastore. Defaults to True.
+ """
+ self.db = db
+ if save:
+ self.save()
+ 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, 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:
+ 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, **kwargs):
+ """
+ Create, initialize, and send a Presence stanza.
+
+ If no recipient is specified, send the presence immediately.
+ Otherwise, forward the send request to the recipient's roster
+ entry for processing.
+
+ Arguments:
+ pshow -- The presence's show value.
+ pstatus -- The presence's status message.
+ ppriority -- This connections' priority.
+ pto -- The recipient of a directed presence.
+ pfrom -- The sender of a directed presence, which should
+ be the owner JID plus resource.
+ ptype -- The type of presence, such as 'subscribe'.
+ pnick -- Optional nickname of the presence's sender.
+ """
+ if self.xmpp.is_component and not kwargs.get('pfrom', ''):
+ kwargs['pfrom'] = self.owner
+ if not kwargs.get('pto', ''):
+ kwargs['pto'] = self.jid
+ self.xmpp.send_presence(**kwargs)
+
+ def send_last_presence(self):
+ if self.last_status is None:
+ pres = self.roster.last_status
+ if pres is None:
+ self.send_presence()
+ else:
+ pres['to'] = self.jid
+ if self.xmpp.is_component:
+ pres['from'] = self.owner
+ else:
+ del pres['from']
+ pres.send()
+ else:
+ self.last_status.send()
+
+ def handle_available(self, presence):
+ resource = presence['from'].resource
+ data = {'status': presence['status'],
+ 'show': presence['show'],
+ 'priority': presence['priority']}
+ got_online = not self.resources
+ if resource not in self.resources:
+ self.resources[resource] = {}
+ old_status = self.resources[resource].get('status', '')
+ old_show = self.resources[resource].get('show', None)
+ self.resources[resource].update(data)
+ if got_online:
+ self.xmpp.event('got_online', presence)
+ if old_show != presence['show'] or old_status != presence['status']:
+ self.xmpp.event('changed_status', presence)
+
+ def handle_unavailable(self, presence):
+ resource = presence['from'].resource
+ if not self.resources:
+ return
+ if resource in self.resources:
+ del self.resources[resource]
+ self.xmpp.event('changed_status', presence)
+ 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['from']:
+ self.send_last_presence()
+ if self['pending_out']:
+ self.subscribe()
+ if not self['from']:
+ self._unsubscribed()
+
+ def reset(self):
+ """
+ Forgot current resource presence information as part of
+ a roster reset request.
+ """
+ self.resources = {}
+
+ def __repr__(self):
+ return repr(self._state)
diff --git a/slixmpp/roster/multi.py b/slixmpp/roster/multi.py
new file mode 100644
index 00000000..e1a44a08
--- /dev/null
+++ b/slixmpp/roster/multi.py
@@ -0,0 +1,224 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2010 Nathanael C. Fritz
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+from slixmpp.stanza import Presence
+from slixmpp.xmlstream import JID
+from slixmpp.roster import RosterNode
+
+
+class Roster(object):
+
+ """
+ Slixmpp'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 Slixmpp 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.
+ send_presence -- Shortcut for sending a presence stanza.
+ """
+
+ def __init__(self, xmpp, db=None):
+ """
+ Create a new roster.
+
+ Arguments:
+ xmpp -- The main Slixmpp 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)
+
+ self.xmpp.add_filter('out', self._save_last_status)
+
+ def _save_last_status(self, stanza):
+
+ if isinstance(stanza, Presence):
+ sfrom = stanza['from'].full
+ sto = stanza['to'].full
+
+ if not sfrom:
+ sfrom = self.xmpp.boundjid
+
+ if stanza['type'] in stanza.showtypes or \
+ stanza['type'] in ('available', 'unavailable'):
+ if sto:
+ self[sfrom][sto].last_status = stanza
+ else:
+ self[sfrom].last_status = stanza
+ with self[sfrom]._last_status_lock:
+ for jid in self[sfrom]:
+ self[sfrom][jid].last_status = None
+
+ if not self.xmpp.sentpresence:
+ self.xmpp.event('sent_presence')
+ self.xmpp.sentpresence = True
+
+ return stanza
+
+ 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 key is None:
+ key = self.xmpp.boundjid
+ if not isinstance(key, JID):
+ key = JID(key)
+ 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, JID):
+ node = JID(node)
+
+ node = node.bare
+ if node not in self._rosters:
+ self._rosters[node] = RosterNode(self.xmpp, node, self.db)
+
+ def set_backend(self, db=None, save=True):
+ """
+ Set the datastore interface object for the roster.
+
+ Arguments:
+ db -- The new datastore interface.
+ save -- If True, save the existing state to the new
+ backend datastore. Defaults to True.
+ """
+ self.db = db
+ existing_entries = set(self._rosters)
+ new_entries = set(self.db.entries(None, {}))
+
+ for node in existing_entries:
+ self._rosters[node].set_backend(db, save)
+ for node in new_entries - existing_entries:
+ self.add(node)
+
+ 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()
+
+ def send_presence(self, **kwargs):
+ """
+ Create, initialize, and send a Presence stanza.
+
+ If no recipient is specified, send the presence immediately.
+ Otherwise, forward the send request to the recipient's roster
+ entry for processing.
+
+ Arguments:
+ pshow -- The presence's show value.
+ pstatus -- The presence's status message.
+ ppriority -- This connections' priority.
+ pto -- The recipient of a directed presence.
+ pfrom -- The sender of a directed presence, which should
+ be the owner JID plus resource.
+ ptype -- The type of presence, such as 'subscribe'.
+ pnick -- Optional nickname of the presence's sender.
+ """
+ if self.xmpp.is_component and not kwargs.get('pfrom', ''):
+ kwargs['pfrom'] = self.jid
+ self.xmpp.send_presence(**kwargs)
+
+ @property
+ def auto_authorize(self):
+ """
+ Auto accept or deny subscription requests.
+
+ If True, auto accept subscription requests.
+ If False, auto deny subscription requests.
+ If None, don't automatically respond.
+ """
+ return self._auto_authorize
+
+ @auto_authorize.setter
+ def auto_authorize(self, value):
+ """
+ Auto accept or deny subscription requests.
+
+ If True, auto accept subscription requests.
+ If False, auto deny subscription requests.
+ If None, don't automatically respond.
+ """
+ self._auto_authorize = value
+ for node in self._rosters:
+ self._rosters[node].auto_authorize = value
+
+ @property
+ def auto_subscribe(self):
+ """
+ Auto send requests for mutual subscriptions.
+
+ If True, auto send mutual subscription requests.
+ """
+ return self._auto_subscribe
+
+ @auto_subscribe.setter
+ def auto_subscribe(self, value):
+ """
+ Auto send requests for mutual subscriptions.
+
+ If True, auto send mutual subscription requests.
+ """
+ self._auto_subscribe = value
+ for node in self._rosters:
+ self._rosters[node].auto_subscribe = value
+
+ def __repr__(self):
+ return repr(self._rosters)
diff --git a/slixmpp/roster/single.py b/slixmpp/roster/single.py
new file mode 100644
index 00000000..07a1f188
--- /dev/null
+++ b/slixmpp/roster/single.py
@@ -0,0 +1,337 @@
+"""
+ Slixmpp: The Slick XMPP Library
+ Copyright (C) 2010 Nathanael C. Fritz
+ This file is part of Slixmpp.
+
+ See the file LICENSE for copying permission.
+"""
+
+import threading
+
+from slixmpp.xmlstream import JID
+from slixmpp.roster import RosterItem
+
+
+class RosterNode(object):
+
+ """
+ A roster node is a roster for a single JID.
+
+ Attributes:
+ xmpp -- The main Slixmpp 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
+ last_status -- The last sent presence status that was broadcast
+ to all contact JIDs.
+
+ 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.
+ send_presence -- Shortcut for sending a presence stanza.
+ """
+
+ def __init__(self, xmpp, jid, db=None):
+ """
+ Create a roster node for a JID.
+
+ Arguments:
+ xmpp -- The main Slixmpp 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.last_status = None
+ self._version = ''
+ self._jids = {}
+ self._last_status_lock = threading.Lock()
+
+ 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.
+
+ A new item entry will be created if one does not already exist.
+ """
+ if key is None:
+ key = JID('')
+ if not isinstance(key, JID):
+ key = JID(key)
+ key = key.bare
+ if key not in self._jids:
+ 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 key is None:
+ key = JID('')
+ if not isinstance(key, JID):
+ key = JID(key)
+ 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)
+
+ 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 groups(self):
+ """Return a dictionary mapping group names to JIDs."""
+ result = {}
+ for jid in self._jids:
+ groups = self._jids[jid]['groups']
+ if not groups:
+ if '' not in result:
+ result[''] = []
+ result[''].append(jid)
+ for group in groups:
+ if group not in result:
+ result[group] = []
+ result[group].append(jid)
+ return result
+
+ def __iter__(self):
+ """Iterate over the roster items."""
+ return self._jids.__iter__()
+
+ def set_backend(self, db=None, save=True):
+ """
+ Set the datastore interface object for the roster node.
+
+ Arguments:
+ db -- The new datastore interface.
+ save -- If True, save the existing state to the new
+ backend datastore. Defaults to True.
+ """
+ self.db = db
+ existing_entries = set(self._jids)
+ new_entries = set(self.db.entries(self.jid, {}))
+
+ for jid in existing_entries:
+ self._jids[jid].set_backend(db, save)
+ for jid in new_entries - existing_entries:
+ self.add(jid)
+
+ 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 isinstance(jid, JID):
+ key = jid.bare
+ else:
+ key = jid
+
+ state = {'name': name,
+ 'groups': groups or [],
+ 'from': afrom,
+ 'to': ato,
+ 'pending_in': pending_in,
+ 'pending_out': pending_out,
+ 'whitelisted': whitelisted,
+ 'subscription': 'none'}
+ self._jids[key] = RosterItem(self.xmpp, jid, self.jid,
+ state=state, db=self.db,
+ roster=self)
+ if save:
+ self._jids[key].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:
+ return self.update(jid, subscription='remove')
+
+ def update(self, jid, name=None, subscription=None, groups=[],
+ block=True, timeout=None, callback=None):
+ """
+ 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.
+ block -- Specify if the roster request will block
+ until a response is received, or a timeout
+ occurs. Defaults to True.
+ timeout -- The length of time (in seconds) to wait
+ for a response before continuing if blocking
+ is used. Defaults to self.response_timeout.
+ callback -- Optional reference to a stream handler function.
+ Will be executed when the roster is received.
+ Implies block=False.
+ """
+ self[jid]['name'] = name
+ self[jid]['groups'] = groups
+ self[jid].save()
+
+ if not self.xmpp.is_component:
+ iq = self.xmpp.Iq()
+ iq['type'] = 'set'
+ iq['roster']['items'] = {jid: {'name': name,
+ 'subscription': subscription,
+ 'groups': groups}}
+
+ return iq.send(block, timeout, callback)
+
+ 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()
+
+ def send_presence(self, **kwargs):
+ """
+ Create, initialize, and send a Presence stanza.
+
+ If no recipient is specified, send the presence immediately.
+ Otherwise, forward the send request to the recipient's roster
+ entry for processing.
+
+ Arguments:
+ pshow -- The presence's show value.
+ pstatus -- The presence's status message.
+ ppriority -- This connections' priority.
+ pto -- The recipient of a directed presence.
+ pfrom -- The sender of a directed presence, which should
+ be the owner JID plus resource.
+ ptype -- The type of presence, such as 'subscribe'.
+ pnick -- Optional nickname of the presence's sender.
+ """
+ if self.xmpp.is_component and not kwargs.get('pfrom', ''):
+ kwargs['pfrom'] = self.jid
+ self.xmpp.send_presence(**kwargs)
+
+ def send_last_presence(self):
+ if self.last_status is None:
+ self.send_presence()
+ else:
+ pres = self.last_status
+ if self.xmpp.is_component:
+ pres['from'] = self.jid
+ else:
+ del pres['from']
+ pres.send()
+
+ def __repr__(self):
+ return repr(self._jids)