# -*- test-case-name: wokkel.test.test_xmppim -*- # # Copyright (c) 2003-2008 Ralph Meijer # See LICENSE for details. """ XMPP IM protocol support. This module provides generic implementations for the protocols defined in U{RFC 3921} (XMPP IM). All of it should eventually move to Twisted. """ from twisted.words.protocols.jabber.jid import JID from twisted.words.xish import domish from wokkel.compat import IQ from wokkel.subprotocols import XMPPHandler NS_XML = 'http://www.w3.org/XML/1998/namespace' NS_ROSTER = 'jabber:iq:roster' class Presence(domish.Element): def __init__(self, to=None, type=None): domish.Element.__init__(self, (None, "presence")) if type: self["type"] = type if to is not None: self["to"] = to.full() class AvailablePresence(Presence): def __init__(self, to=None, show=None, statuses=None, priority=0): Presence.__init__(self, to, type=None) if show in ['away', 'xa', 'chat', 'dnd']: self.addElement('show', content=show) if statuses is not None: for lang, status in statuses.iteritems(): s = self.addElement('status', content=status) if lang: s[(NS_XML, "lang")] = lang if priority != 0: self.addElement('priority', content=unicode(int(priority))) class UnavailablePresence(Presence): def __init__(self, to=None, statuses=None): Presence.__init__(self, to, type='unavailable') if statuses is not None: for lang, status in statuses.iteritems(): s = self.addElement('status', content=status) if lang: s[(NS_XML, "lang")] = lang class PresenceClientProtocol(XMPPHandler): def connectionInitialized(self): self.xmlstream.addObserver('/presence', self._onPresence) def _getStatuses(self, presence): statuses = {} for element in presence.elements(): if element.name == 'status': lang = element.getAttribute((NS_XML, 'lang')) text = unicode(element) statuses[lang] = text return statuses def _onPresence(self, presence): type = presence.getAttribute("type", "available") try: handler = getattr(self, '_onPresence%s' % (type.capitalize())) except AttributeError: return else: handler(presence) def _onPresenceAvailable(self, presence): entity = JID(presence["from"]) show = unicode(presence.show or '') if show not in ['away', 'xa', 'chat', 'dnd']: show = None statuses = self._getStatuses(presence) try: priority = int(unicode(presence.priority or '')) or 0 except ValueError: priority = 0 self.availableReceived(entity, show, statuses, priority) def _onPresenceUnavailable(self, presence): entity = JID(presence["from"]) statuses = self._getStatuses(presence) self.unavailableReceived(entity, statuses) def _onPresenceSubscribed(self, presence): self.subscribedReceived(JID(presence["from"])) def _onPresenceUnsubscribed(self, presence): self.unsubscribedReceived(JID(presence["from"])) def _onPresenceSubscribe(self, presence): self.subscribeReceived(JID(presence["from"])) def _onPresenceUnsubscribe(self, presence): self.unsubscribeReceived(JID(presence["from"])) def availableReceived(self, entity, show=None, statuses=None, priority=0): """ Available presence was received. @param entity: entity from which the presence was received. @type entity: {JID} @param show: detailed presence information. One of C{'away'}, C{'xa'}, C{'chat'}, C{'dnd'} or C{None}. @type show: C{str} or C{NoneType} @param statuses: dictionary of natural language descriptions of the availability status, keyed by the language descriptor. A status without a language specified, is keyed with C{None}. @type statuses: C{dict} @param priority: priority level of the resource. @type priority: C{int} """ def unavailableReceived(self, entity, statuses=None): """ Unavailable presence was received. @param entity: entity from which the presence was received. @type entity: {JID} @param statuses: dictionary of natural language descriptions of the availability status, keyed by the language descriptor. A status without a language specified, is keyed with C{None}. @type statuses: C{dict} """ def subscribedReceived(self, entity): """ Subscription approval confirmation was received. @param entity: entity from which the confirmation was received. @type entity: {JID} """ def unsubscribedReceived(self, entity): """ Unsubscription confirmation was received. @param entity: entity from which the confirmation was received. @type entity: {JID} """ def subscribeReceived(self, entity): """ Subscription request was received. @param entity: entity from which the request was received. @type entity: {JID} """ def unsubscribeReceived(self, entity): """ Unsubscription request was received. @param entity: entity from which the request was received. @type entity: {JID} """ def available(self, entity=None, show=None, statuses=None, priority=0): """ Send available presence. @param entity: optional entity to which the presence should be sent. @type entity: {JID} @param show: optional detailed presence information. One of C{'away'}, C{'xa'}, C{'chat'}, C{'dnd'}. @type show: C{str} @param statuses: dictionary of natural language descriptions of the availability status, keyed by the language descriptor. A status without a language specified, is keyed with C{None}. @type statuses: C{dict} @param priority: priority level of the resource. @type priority: C{int} """ self.send(AvailablePresence(entity, show, statuses, priority)) def unavailable(self, entity=None, statuses=None): """ Send unavailable presence. @param entity: optional entity to which the presence should be sent. @type entity: {JID} @param statuses: dictionary of natural language descriptions of the availability status, keyed by the language descriptor. A status without a language specified, is keyed with C{None}. @type statuses: C{dict} """ self.send(UnavailablePresence(entity, statuses)) def subscribe(self, entity): """ Send subscription request @param entity: entity to subscribe to. @type entity: {JID} """ self.send(Presence(to=entity, type='subscribe')) def unsubscribe(self, entity): """ Send unsubscription request @param entity: entity to unsubscribe from. @type entity: {JID} """ self.send(Presence(to=entity, type='unsubscribe')) def subscribed(self, entity): """ Send subscription confirmation. @param entity: entity that subscribed. @type entity: {JID} """ self.send(Presence(to=entity, type='subscribed')) def unsubscribed(self, entity): """ Send unsubscription confirmation. @param entity: entity that unsubscribed. @type entity: {JID} """ self.send(Presence(to=entity, type='unsubscribed')) class RosterItem(object): """ Roster item. This represents one contact from an XMPP contact list known as roster. @ivar jid: The JID of the contact. @type jid: L{JID} @ivar name: The optional associated nickname for this contact. @type name: C{unicode} @ivar subscriptionTo: Subscription state to contact's presence. If C{True}, the roster owner is subscribed to the presence information of the contact. @type subscriptionTo: C{bool} @ivar subscriptionFrom: Contact's subscription state. If C{True}, the contact is subscribed to the presence information of the roster owner. @type subscriptionTo: C{bool} @ivar ask: Whether subscription is pending. @type ask: C{bool} @ivar groups: Set of groups this contact is categorized in. Groups are represented by an opaque identifier of type C{unicode}. @type groups: C{set} """ def __init__(self, jid): self.jid = jid self.name = None self.subscriptionTo = False self.subscriptionFrom = False self.ask = None self.groups = set() class RosterClientProtocol(XMPPHandler): """ Client side XMPP roster protocol. """ def connectionInitialized(self): ROSTER_SET = "/iq[@type='set']/query[@xmlns='%s']" % NS_ROSTER self.xmlstream.addObserver(ROSTER_SET, self._onRosterSet) def _parseRosterItem(self, element): jid = JID(element['jid']) item = RosterItem(jid) item.name = element.getAttribute('name') subscription = element.getAttribute('subscription') item.subscriptionTo = subscription in ('to', 'both') item.subscriptionFrom = subscription in ('from', 'both') item.ask = element.getAttribute('ask') == 'subscribe' for subElement in domish.generateElementsQNamed(element.children, 'group', NS_ROSTER): item.groups.add(unicode(subElement)) return item def getRoster(self): """ Retrieve contact list. @return: Roster as a mapping from L{JID} to L{RosterItem}. @rtype: L{twisted.internet.defer.Deferred} """ def processRoster(result): roster = {} for element in domish.generateElementsQNamed(result.query.children, 'item', NS_ROSTER): item = self._parseRosterItem(element) roster[item.jid.userhost()] = item return roster iq = IQ(self.xmlstream, 'get') iq.addElement((NS_ROSTER, 'query')) d = iq.send() d.addCallback(processRoster) return d def removeItem(self, entity): """ Remove an item from the contact list. @param entity: The contact to remove the roster item for. @type entity: L{JID} @rtype: L{twisted.internet.defer.Deferred} """ iq = IQ(self.xmlstream, 'set') iq.addElement((NS_ROSTER, 'query')) item = iq.query.addElement('item') item['jid'] = entity.full() item['subscription'] = 'remove' return iq.send() def _onRosterSet(self, iq): if iq.handled or \ iq.hasAttribute('from') and iq['from'] != self.xmlstream: return iq.handled = True itemElement = iq.query.item if unicode(itemElement['subscription']) == 'remove': self.onRosterRemove(JID(itemElement['jid'])) else: item = self._parseRosterItem(iq.query.item) self.onRosterSet(item) def onRosterSet(self, item): """ Called when a roster push for a new or update item was received. @param item: The pushed roster item. @type item: L{RosterItem} """ def onRosterRemove(self, entity): """ Called when a roster push for the removal of an item was received. @param entity: The entity for which the roster item has been removed. @type entity: L{JID} """ class MessageProtocol(XMPPHandler): """ Generic XMPP subprotocol handler for incoming message stanzas. """ messageTypes = None, 'normal', 'chat', 'headline', 'groupchat' def connectionInitialized(self): self.xmlstream.addObserver("/message", self._onMessage) def _onMessage(self, message): if message.handled: return messageType = message.getAttribute("type") if messageType == 'error': return if messageType not in self.messageTypes: message["type"] = 'normal' self.onMessage(message) def onMessage(self, message): """ Called when a message stanza was received. """