source: wokkel/xmppim.py @ 2:47f1cb624f14

Last change on this file since 2:47f1cb624f14 was 2:47f1cb624f14, checked in by Ralph Meijer <ralphm@…>, 15 years ago

Add in pubsub client support, client helpers and generic XMPP subprotocol handlers.

File size: 12.2 KB
Line 
1# Copyright (c) 2003-2007 Ralph Meijer
2# See LICENSE for details.
3
4"""
5XMPP IM protocol support.
6
7This module provides generic implementations for the protocols defined in
8U{RFC 3921<http://www.xmpp.org/rfcs/rfc3921.html>} (XMPP IM).
9
10All of it should eventually move to Twisted.
11"""
12
13from twisted.words.protocols.jabber.jid import JID
14from twisted.words.protocols.jabber.xmlstream import IQ
15from twisted.words.xish import domish
16
17from wokkel.subprotocols import XMPPHandler
18
19NS_XML = 'http://www.w3.org/XML/1998/namespace'
20NS_ROSTER = 'jabber:iq:roster'
21
22class Presence(domish.Element):
23    def __init__(self, to=None, type=None):
24        domish.Element.__init__(self, (None, "presence"))
25        if type:
26            self["type"] = type
27
28        if to is not None:
29            self["to"] = to.full()
30
31class AvailablePresence(Presence):
32    def __init__(self, to=None, show=None, statuses=None, priority=0):
33        Presence.__init__(self, to, type=None)
34
35        if show in ['away', 'xa', 'chat', 'dnd']:
36            self.addElement('show', content=show)
37
38        if statuses is not None:
39            for lang, status in statuses.iteritems():
40                s = self.addElement('status', content=status)
41                if lang:
42                    s[(NS_XML, "lang")] = lang
43
44        if priority != 0:
45            self.addElement('priority', int(priority))
46
47class UnavailablePresence(Presence):
48    def __init__(self, to=None, statuses=None):
49        Presence.__init__(self, to, type='unavailable')
50
51        if statuses is not None:
52            for lang, status in statuses.iteritems():
53                s = self.addElement('status', content=status)
54                if lang:
55                    s[(NS_XML, "lang")] = lang
56
57class PresenceClientProtocol(XMPPHandler):
58
59    def connectionInitialized(self):
60        self.xmlstream.addObserver('/presence', self._onPresence)
61
62    def _getStatuses(self, presence):
63        statuses = {}
64        for element in presence.elements():
65            if element.name == 'status':
66                lang = element.getAttribute((NS_XML, 'lang'))
67                text = unicode(element)
68                statuses[lang] = text
69        return statuses
70
71    def _onPresence(self, presence):
72        type = presence.getAttribute("type", "available")
73        try:
74            handler = getattr(self, '_onPresence%s' % (type.capitalize()))
75        except AttributeError:
76            return
77        else:
78            handler(presence)
79
80    def _onPresenceAvailable(self, presence):
81        entity = JID(presence["from"])
82
83        show = unicode(presence.show or '')
84        if show not in ['away', 'xa', 'chat', 'dnd']:
85            show = None
86
87        statuses = self._getStatuses(presence)
88
89        try:
90            priority = int(unicode(presence.priority or '')) or 0
91        except ValueError:
92            priority = 0
93
94        self.availableReceived(entity, show, statuses, priority)
95
96    def _onPresenceUnavailable(self, presence):
97        entity = JID(presence["from"])
98
99        statuses = self._getStatuses(presence)
100
101        self.unavailableReceived(entity, statuses)
102
103    def _onPresenceSubscribed(self, presence):
104        self.subscribedReceived(JID(presence["from"]))
105
106    def _onPresenceUnsubscribed(self, presence):
107        self.unsubscribedReceived(JID(presence["from"]))
108
109    def _onPresenceSubscribe(self, presence):
110        self.subscribeReceived(JID(presence["from"]))
111
112    def _onPresenceUnsubscribe(self, presence):
113        self.unsubscribeReceived(JID(presence["from"]))
114
115    def availableReceived(self, entity, show=None, statuses=None, priority=0):
116        """
117        Available presence was received.
118
119        @param entity: entity from which the presence was received.
120        @type entity: {JID}
121        @param show: detailed presence information. One of C{'away'}, C{'xa'},
122                     C{'chat'}, C{'dnd'} or C{None}.
123        @type show: C{str} or C{NoneType}
124        @param statuses: dictionary of natural language descriptions of the
125                         availability status, keyed by the language
126                         descriptor. A status without a language
127                         specified, is keyed with C{None}.
128        @type statuses: C{dict}
129        @param priority: priority level of the resource.
130        @type priority: C{int}
131        """
132
133    def unavailableReceived(self, entity, statuses=None):
134        """
135        Unavailable presence was received.
136
137        @param entity: entity from which the presence was received.
138        @type entity: {JID}
139        @param statuses: dictionary of natural language descriptions of the
140                         availability status, keyed by the language
141                         descriptor. A status without a language
142                         specified, is keyed with C{None}.
143        @type statuses: C{dict}
144        """
145
146    def subscribedReceived(self, entity):
147        """
148        Subscription approval confirmation was received.
149
150        @param entity: entity from which the confirmation was received.
151        @type entity: {JID}
152        """
153
154    def unsubscribedReceived(self, entity):
155        """
156        Unsubscription confirmation was received.
157
158        @param entity: entity from which the confirmation was received.
159        @type entity: {JID}
160        """
161
162    def subscribeReceived(self, entity):
163        """
164        Subscription request was received.
165
166        @param entity: entity from which the request was received.
167        @type entity: {JID}
168        """
169
170    def unsubscribeReceived(self, entity):
171        """
172        Unsubscription request was received.
173
174        @param entity: entity from which the request was received.
175        @type entity: {JID}
176        """
177
178    def available(self, entity=None, show=None, statuses=None, priority=0):
179        """
180        Send available presence.
181
182        @param entity: optional entity to which the presence should be sent.
183        @type entity: {JID}
184        @param show: optional detailed presence information. One of C{'away'},
185                     C{'xa'}, C{'chat'}, C{'dnd'}.
186        @type show: C{str}
187        @param statuses: dictionary of natural language descriptions of the
188                         availability status, keyed by the language
189                         descriptor. A status without a language
190                         specified, is keyed with C{None}.
191        @type statuses: C{dict}
192        @param priority: priority level of the resource.
193        @type priority: C{int}
194        """
195        self.send(AvailablePresence(entity, show, statuses, priority))
196
197    def unavailable(self, entity, statuses=None):
198        """
199        Send unavailable presence.
200
201        @param entity: optional entity to which the presence should be sent.
202        @type entity: {JID}
203        @param statuses: dictionary of natural language descriptions of the
204                         availability status, keyed by the language
205                         descriptor. A status without a language
206                         specified, is keyed with C{None}.
207        @type statuses: C{dict}
208        """
209        self.send(AvailablePresence(entity, statuses))
210
211    def subscribe(self, entity):
212        """
213        Send subscription request
214
215        @param entity: entity to subscribe to.
216        @type entity: {JID}
217        """
218        self.send(Presence(to=entity, type='subscribe'))
219
220    def unsubscribe(self, entity):
221        """
222        Send unsubscription request
223
224        @param entity: entity to unsubscribe from.
225        @type entity: {JID}
226        """
227        self.send(Presence(to=entity, type='unsubscribe'))
228
229    def subscribed(self, entity):
230        """
231        Send subscription confirmation.
232
233        @param entity: entity that subscribed.
234        @type entity: {JID}
235        """
236        self.send(Presence(to=entity, type='subscribed'))
237
238    def unsubscribed(self, entity):
239        """
240        Send unsubscription confirmation.
241
242        @param entity: entity that unsubscribed.
243        @type entity: {JID}
244        """
245        self.send(Presence(to=entity, type='unsubscribed'))
246
247
248class RosterItem(object):
249    """
250    Roster item.
251
252    This represents one contact from an XMPP contact list known as roster.
253
254    @ivar jid: The JID of the contact.
255    @type jid: L{JID}
256    @ivar name: The optional associated nickname for this contact.
257    @type name: C{unicode}
258    @ivar subscriptionTo: Subscription state to contact's presence. If C{True},
259                          the roster owner is subscribed to the presence
260                          information of the contact.
261    @type subscriptionTo: C{bool}
262    @ivar subscriptionFrom: Contact's subscription state. If C{True}, the
263                            contact is subscribed to the presence information
264                            of the roster owner.
265    @type subscriptionTo: C{bool}
266    @ivar ask: Whether subscription is pending.
267    @type ask: C{bool}
268    @ivar groups: Set of groups this contact is categorized in. Groups are
269                  represented by an opaque identifier of type C{unicode}.
270    @type groups: C{set}
271    """
272
273    def __init__(self, jid):
274        self.jid = jid
275        self.name = None
276        self.subscriptionTo = False
277        self.subscriptionFrom = False
278        self.ask = None
279        self.groups = set()
280
281
282class RosterClientProtocol(XMPPHandler):
283    """
284    Client side XMPP roster protocol.
285    """
286
287    def connectionInitialized(self):
288        ROSTER_SET = "/iq[@type='set']/query[@xmlns='%s']" % NS_ROSTER
289        self.xmlstream.addObserver(ROSTER_SET, self._onRosterSet)
290
291    def _parseRosterItem(self, element):
292        jid = JID(element['jid'])
293        item = RosterItem(jid)
294        item.name = element.getAttribute('name')
295        subscription = element.getAttribute('subscription')
296        item.subscriptionTo = subscription in ('to', 'both')
297        item.subscriptionFrom = subscription in ('from', 'both')
298        item.ask = element.getAttribute('ask') == 'subscribe'
299        for subElement in domish.generateElementsQNamed(element.children,
300                                                        'group', NS_ROSTER):
301            item.groups.add(unicode(subElement))
302
303        return item
304
305    def getRoster(self):
306        """
307        Retrieve contact list.
308
309        @return: Roster as a mapping from L{JID} to L{RosterItem}.
310        @rtype: L{twisted.internet.defer.Deferred}
311        """
312
313        def processRoster(result):
314            roster = {}
315            for element in domish.generateElementsQNamed(result.query.children,
316                                                         'item', NS_ROSTER):
317                item = self._parseRosterItem(element)
318                roster[item.jid.userhost()] = item
319
320            return roster
321
322        iq = IQ(self.xmlstream, 'get')
323        iq.addElement((NS_ROSTER, 'query'))
324        d = iq.send()
325        d.addCallback(processRoster)
326        return d
327
328    def _onRosterSet(self, iq):
329        if iq.handled or \
330           iq.hasAttribute('from') and iq['from'] != self.xmlstream:
331            return
332
333        iq.handled = True
334
335        itemElement = iq.query.item
336
337        if unicode(itemElement['subscription']) == 'remove':
338            self.onRosterRemove(JID(itemElement['jid']))
339        else:
340            item = self._parseRosterItem(iq.query.item)
341            self.onRosterSet(item)
342
343    def onRosterSet(self, item):
344        """
345        Called when a roster push for a new or update item was received.
346
347        @param item: The pushed roster item.
348        @type item: L{RosterItem}
349        """
350
351    def onRosterRemove(self, entity):
352        """
353        Called when a roster push for the removal of an item was received.
354
355        @param entity: The entity for which the roster item has been removed.
356        @type entity: L{JID}
357        """
358
359class MessageProtocol(XMPPHandler):
360    """
361    Generic XMPP subprotocol handler for incoming message stanzas.
362    """
363
364    messageTypes = None, 'normal', 'chat', 'headline', 'groupchat'
365
366    def connectionInitialized(self):
367        self.xmlstream.addObserver("/message", self._onMessage)
368
369    def _onMessage(self, message):
370        if message.handled:
371            return
372
373        messageType = message.getAttribute("type")
374
375        if messageType == 'error':
376            return
377
378        if messageType not in self.messageTypes:
379            message["type"] = 'normal'
380
381        self.onMessage(message)
382
383    def onMessage(self, message):
384        """
385        Called when a message stanza was received.
386        """
387
Note: See TracBrowser for help on using the repository browser.