source: wokkel/xmppim.py @ 27:d62d7ea14995

Last change on this file since 27:d62d7ea14995 was 12:d27bd18a2e5c, checked in by Ralph Meijer <ralphm@…>, 14 years ago

Add preliminary support for formats used with publish-subscribe.

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