diff options
-rw-r--r-- | sleekxmpp/basexmpp.py | 186 | ||||
-rw-r--r-- | sleekxmpp/clientxmpp.py | 16 | ||||
-rw-r--r-- | sleekxmpp/componentxmpp.py | 11 | ||||
-rw-r--r-- | sleekxmpp/roster.py | 770 | ||||
-rw-r--r-- | sleekxmpp/stanza/roster.py | 2 | ||||
-rw-r--r-- | sleekxmpp/test/sleektest.py | 26 | ||||
-rw-r--r-- | tests/test_stanza_presence.py | 3 | ||||
-rw-r--r-- | tests/test_stanza_roster.py | 4 | ||||
-rw-r--r-- | tests/test_stream_presence.py | 34 | ||||
-rw-r--r-- | tests/test_stream_roster.py | 71 |
10 files changed, 967 insertions, 156 deletions
diff --git a/sleekxmpp/basexmpp.py b/sleekxmpp/basexmpp.py index 3992a4f9..91b419ba 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. @@ -594,96 +679,7 @@ class BaseXMPP(XMLStream): 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].get('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 fb5b2087..a7b24351 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 @@ -458,13 +458,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 f9e7da4d..4d17d725 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): """ @@ -140,3 +142,8 @@ class ComponentXMPP(BaseXMPP): """ self.session_started_event.set() 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..55af1e46 --- /dev/null +++ b/sleekxmpp/roster.py @@ -0,0 +1,770 @@ +""" + 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 + +from sleekxmpp.xmlstream import JID + + +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 isinstance(key, JID): + 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 isinstance(node, JID): + 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 isinstance(key, JID): + 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 isinstance(jid, JID): + 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 7802a9bc..19309fc5 100644 --- a/sleekxmpp/test/sleektest.py +++ b/sleekxmpp/test/sleektest.py @@ -150,6 +150,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 e1aa1766..69e5ca13 100644 --- a/tests/test_stream_roster.py +++ b/tests/test_stream_roster.py @@ -17,8 +17,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 = [] @@ -37,11 +36,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> @@ -52,23 +52,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): @@ -76,9 +75,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" @@ -95,16 +93,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) @@ -182,14 +178,13 @@ class TestStreamRoster(SleekTest): # Give the event queue time to process. time.sleep(.1) - roster = {'andré@foo': { - 'name': '', - 'subscription': 'both', - 'groups': ['Unicode'], - 'presence': {}, - 'in_roster': True}} - self.failUnless(self.xmpp.roster == roster, - "Unexpected roster values: %s" % self.xmpp.roster) + self.check_roster('tester@localhost', 'andré@foo', + subscription='both', + groups=['Unicode']) + + jids = list(self.xmpp.client_roster.keys()) + self.failUnless(jids == ['andré@foo'], + "Too many roster entries found: %s" % jids) self.recv(""" <presence to="tester@localhost" from="andré@foo/bar"> @@ -201,18 +196,12 @@ class TestStreamRoster(SleekTest): # Give the event queue time to process. time.sleep(.1) - roster = {'andré@foo': { - 'name': '', - 'subscription': 'both', - 'groups': ['Unicode'], - 'presence': { - 'bar':{'priority':0, - 'status':'Testing', - 'show':'away'}}, - 'in_roster': True}} - self.failUnless(self.xmpp.roster == roster, - "Unexpected roster values: %s" % self.xmpp.roster) - + result = self.xmpp.client_roster['andré@foo'].resources + expected = {'bar': {'status':'Testing', + 'show':'away', + 'priority':0}} + self.failUnless(result == expected, + "Unexpected roster values: %s" % result) suite = unittest.TestLoader().loadTestsFromTestCase(TestStreamRoster) |