Roster Management

Lance Stout edited this page Aug 18, 2011 · 7 revisions

Roster Management

Background

In XMPP, the presence portion of the protocol is expressed through presence subscriptions between entities. The list of all of these subscriptions is called the roster, and you may think of it as a buddy list or contact list. Manually managing the roster is usually unnecessary since it is done by the server automatically when presence subscribe or unsubscribe requests are received and accepted. However, roster entries may be placed into groups or even removed. In those cases, manual management is needed.

Note: See Stanzas: Presence for more information on adding and removing subscriptions.

If you are using SleekXMPP Beta6.1 or below, see Roster Management Pre RC1.

The Roster

SleekXMPP builds and maintains a local roster based on roster items pushed by the XMPP server. For clients, getting an initial copy of the roster is done using self.get_roster() (where self is a ClientXMPP object), and can then later be accessed with self.client_roster.

Components may also have a roster, but it is up to the component to manage it, not the server. A component's roster can be accessed with self.roster. The difference for components is that the component may be using multiple JIDs, each of which may have its own set of subscriptions. Thus self.roster is a collection of rosters for individual JIDs. For example, self.roster[self.boundjid.bare] is the roster for the JID that was used to connect to the server. Clients use the same implementation, and self.client_roster is just a shortcut to self.roster[self.boundjid.bare].

The structure for an individual roster item (for OWNER_JID's roster) is:

  • self.roster[OWNER_JID]['user@server']['name'] (or self.client_roster['user@server']['name'])

    The alias assigned to the roster entry to make it easier to identify the owner of the JID. For example, the name of the person using the subscribed JID.

  • self.roster[OWNER_JID]['user@server']['subscription'] (or self.client_roster['user@server']]['subscription'])

    The subscription type defines the directions in which presence updates are broadcast. If the subscription type is 'to', then the SleekXMPP agent will receive presence updates from user@server, but user@server will not receive presence updates from the JID used by the SleekXMPP agent. The direction is reversed if the subscription type is 'from'. Only if the type is 'both' will presence updates be mutually exchanged. No presence updates are broadcast when the type is 'none'. An additional type, 'remove', is used for deleting a subscription and is described below.

    Note: If you are using the roster for deciding on message broadcasting recipients, be sure to ignore entries with a subscription type of 'none'.

  • self.roster[OWNER_JID]['user@server']['groups'] (or self.client_roster['user@server']['groups'])

    A list of groups that user@server has been placed into for ease of use and organization.

  • self.roster[OWNER_JID]['user@server'].resources (or self.client_roster['user@server'].resources)

    A dictionary of all of the connected resources associated with the given bare JID, keyed by the resource identifier. Each of these dictionaries will contain the keys show, status, and priority which will contain the associated information from the last presence stanza received from the particular connection.

See the inline documentation for sleekxmpp.roster for more information. You can also persist the roster using an external database; see sleekxmpp.roster.item for details. As an example, here is a plugin for hooking the roster to Redis: [Redis Roster](https://github.com/legastero/SleekRedis/blob/master/redis_roster.py).

Updating an Entry

Updating a roster item can be done at any time, but is usually done after a subscription has been established.

# Where self is a ClientXMPP object
self.update_roster('romeo@montague.lit', name='Romeo', groups=['Montagues'])

The subscription type can be set as well using the subscription keyword parameter. Some servers will issue the appropriate presence stanzas needed to obtain the requested subscription state, but that behavior should not be assumed.

Deleting an Entry

Even after modifying a subscription to have a type of 'none', the XMPP server may still keep a record of it and include it in requests for the roster. For clients that will be adding and removing subscriptions regularly and in large numbers (such as a public bot), then removing obsolete entries is needed. Updating a roster item to have a subscription type of 'remove' will instruct the server to delete the entry from its data store and not include it in future roster requests.

# Where self is a ClientXMPP object
self.del_roster_item('rosaline@capulet.lit')
# -or-
self.update_roster('rosaline@capulet.lit', subscription='remove')

Subscription Management

Adding or removing a subscription is typically a four-step process:

  1. Send a presence stanza with type 'subscribe' or 'unsubscribe'
  2. Receive a presence stanza of type 'subscribed' or 'unsubscribed'
  3. Receive a presence stanza of type 'subscribe' or 'unsubscribe'
  4. Send a presence stanza of type 'subscribed' or 'unsubscribed'

If your program will be accepting subscriptions instead of initiating them, reverse the send and receive steps above.

Sending and replying to a presence subscription can be done using xmpp.send_presence or xmpp.send_presence_subscription. In both cases, the ptype keyword parameter must be set. If adding a nick element to the subscription request is desired, xmpp.send_presence_subscription must be used.

# Where xmpp is a SleekXMPP object
xmpp.send_presence(pto='user@example.com', ptype='subscribe')
# -or-
xmpp.send_presence_subscription(pto='user@example.com', 
                                ptype='subscribe', 
                                pnick='Sleek')

Automatic Subscription Management

SleekXMPP already provides basic subscription management, but the automatic handling is to either accept or reject all subscription requests.

The value xmpp.auto_authorize controls how the agent responds to a 'subscribe' request. If True, then a 'subscribed' response will be sent. If False, then an 'unsubscribed' response will be sent to decline the subscription. Setting xmpp.auto_authorize to None will disable automatic management.

NOTE: By default, xmpp.auto_authorize is set to True.

The value xmpp.auto_subscribe is used when xmpp.auto_authorize is set to True. If xmpp.auto_subscribe is True then a 'subscribe' request will be sent after accepting a subscription request.

  • Accept and create bidirectional subscription requests:
xmpp.auto_authorize = True
xmpp.auto_subscribe = True
  • Accept only one-directional subscription requests:
xmpp.auto_authorize = True
xmpp.auto_subscribe = False
  • Decline all subscription requests:
xmpp.auto_authorize = False
  • Use a custom subscription policy:
xmpp.auto_authorize = None

Custom Subscription Management

To create custom subscription management, the events 'presence_subscribe', 'presence_subscribed', 'presence_unsubscribe', and 'presence_unsubscribed' must be listened for by event handlers.

Note: If your application will both send and receive subscription requests, you will need to keep a roster data structure that knows the status of all pending requests (the ClientXMPP implementation has such a roster object; the ComponentXMPP implementation does not unless you use the experimental roster branch). Otherwise, you can create an infinite loop of two bots subscribing to the other's presence. If you only test with a human-facing client, most client programs will detect that situation and stop the cycle without reporting an error to you.

As an example, here is a sample snippet for custom subscription handling. However, note that if you are using a server component, it is a good idea to also set the pfrom parameter when sending presence subscriptions. Doing so allows you to support different subscription states for different JIDs for the component, such as user1@component.example.com and user2@component.example.com.

# Where self is a SleekXMPP object, and self.backend is some arbitrary
# object that you create depending on the needs of your application.

# self.add_event_handler('presence_subscribe', self.subscribe)
# self.add_event_handler('presence_subscribed', self.subscribed)
# The unsubscribe and unsubscribed handlers will be similar.

# If a component is being used, be sure to set the pfrom parameter
# when sending presences if you are using multiple JIDs for the component, 
# such as user1@component.example.com and user2@component.example.com.

def subscribe(self, presence):
    # If the subscription request is rejected.
    if not self.backend.allow_subscription(presence['from']):
        self.send_presence(pto=presence['from'], 
                           ptype='unsubscribed')
        return
     
    # If the subscription request is accepted.
    self.send_presence(pto=presence['from'],
                       ptype='subscribed')

    # Save the fact that a subscription has been accepted, somehow. Here
    # we use a backend object that has a roster.
    self.backend.roster.subscribe(presence['from'])

    # If a bi-directional subscription does not exist, create one.
    if not self.backend.roster.sub_from(presence['from']):
        self.send_presence(pto=presence['from'],
                           ptype='subscribe')

def subscribed(self, presence):
    # Store the new subscription state, somehow. Here we use a backend object.
    self.backend.roster.subscribed(presence['from'])

    # Send a new presence update to the subscriber.
    self.send_presence(pto=presence['from'])