source: wokkel/xmppim.py @ 30:68535ae85c8d

Last change on this file since 30:68535ae85c8d was 28:65fd5f2bd59e, checked in by Ralph Meijer <ralphm@…>, 13 years ago

Add client side support for removing a roster item.

File size: 12.8 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
331    def removeItem(self, entity):
332        """
333        Remove an item from the contact list.
334
335        @param entity: The contact to remove the roster item for.
336        @type entity: L{JID<twisted.words.protocols.jabber.jid.JID>}
337        @rtype: L{twisted.internet.defer.Deferred}
338        """
339        iq = IQ(self.xmlstream, 'set')
340        iq.addElement((NS_ROSTER, 'query'))
341        item = iq.query.addElement('item')
342        item['jid'] = entity.full()
343        item['subscription'] = 'remove'
344        return iq.send()
345
346
347    def _onRosterSet(self, iq):
348        if iq.handled or \
349           iq.hasAttribute('from') and iq['from'] != self.xmlstream:
350            return
351
352        iq.handled = True
353
354        itemElement = iq.query.item
355
356        if unicode(itemElement['subscription']) == 'remove':
357            self.onRosterRemove(JID(itemElement['jid']))
358        else:
359            item = self._parseRosterItem(iq.query.item)
360            self.onRosterSet(item)
361
362    def onRosterSet(self, item):
363        """
364        Called when a roster push for a new or update item was received.
365
366        @param item: The pushed roster item.
367        @type item: L{RosterItem}
368        """
369
370    def onRosterRemove(self, entity):
371        """
372        Called when a roster push for the removal of an item was received.
373
374        @param entity: The entity for which the roster item has been removed.
375        @type entity: L{JID}
376        """
377
378class MessageProtocol(XMPPHandler):
379    """
380    Generic XMPP subprotocol handler for incoming message stanzas.
381    """
382
383    messageTypes = None, 'normal', 'chat', 'headline', 'groupchat'
384
385    def connectionInitialized(self):
386        self.xmlstream.addObserver("/message", self._onMessage)
387
388    def _onMessage(self, message):
389        if message.handled:
390            return
391
392        messageType = message.getAttribute("type")
393
394        if messageType == 'error':
395            return
396
397        if messageType not in self.messageTypes:
398            message["type"] = 'normal'
399
400        self.onMessage(message)
401
402    def onMessage(self, message):
403        """
404        Called when a message stanza was received.
405        """
Note: See TracBrowser for help on using the repository browser.