source: wokkel/xmppim.py @ 174:c0f51d95bf0f

Last change on this file since 174:c0f51d95bf0f was 174:c0f51d95bf0f, checked in by Ralph Meijer <ralphm@…>, 8 years ago

Add support for adding and updating roster items.

Roster items can be added or updated by sending a roster set request using the
new wokkel.xmppim.RosterClientProtocol.setItem. It takes a RosterItem as
the only argument. Note that changes in presence subscriptions for the contact
need to be done using presence as provided by
wokkel.xmppim.PresenceProtocol.

Author: ralphm.
Fixes: #56.

  • Property exe set to *
File size: 32.6 KB
Line 
1# -*- test-case-name: wokkel.test.test_xmppim -*-
2#
3# Copyright (c) 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 6121<http://www.xmpp.org/rfcs/rfc6121.html>} (XMPP IM).
11"""
12
13import warnings
14
15from twisted.internet import defer
16from twisted.words.protocols.jabber import error
17from twisted.words.protocols.jabber.jid import JID
18from twisted.words.xish import domish
19
20from wokkel.generic import ErrorStanza, Stanza, Request
21from wokkel.subprotocols import IQHandlerMixin
22from wokkel.subprotocols import XMPPHandler
23
24NS_XML = 'http://www.w3.org/XML/1998/namespace'
25NS_ROSTER = 'jabber:iq:roster'
26
27XPATH_ROSTER_SET = "/iq[@type='set']/query[@xmlns='%s']" % NS_ROSTER
28
29
30
31class Presence(domish.Element):
32    def __init__(self, to=None, type=None):
33        domish.Element.__init__(self, (None, "presence"))
34        if type:
35            self["type"] = type
36
37        if to is not None:
38            self["to"] = to.full()
39
40class AvailablePresence(Presence):
41    def __init__(self, to=None, show=None, statuses=None, priority=0):
42        Presence.__init__(self, to, type=None)
43
44        if show in ['away', 'xa', 'chat', 'dnd']:
45            self.addElement('show', content=show)
46
47        if statuses is not None:
48            for lang, status in statuses.iteritems():
49                s = self.addElement('status', content=status)
50                if lang:
51                    s[(NS_XML, "lang")] = lang
52
53        if priority != 0:
54            self.addElement('priority', content=unicode(int(priority)))
55
56class UnavailablePresence(Presence):
57    def __init__(self, to=None, statuses=None):
58        Presence.__init__(self, to, type='unavailable')
59
60        if statuses is not None:
61            for lang, status in statuses.iteritems():
62                s = self.addElement('status', content=status)
63                if lang:
64                    s[(NS_XML, "lang")] = lang
65
66class PresenceClientProtocol(XMPPHandler):
67
68    def connectionInitialized(self):
69        self.xmlstream.addObserver('/presence', self._onPresence)
70
71    def _getStatuses(self, presence):
72        statuses = {}
73        for element in presence.elements():
74            if element.name == 'status':
75                lang = element.getAttribute((NS_XML, 'lang'))
76                text = unicode(element)
77                statuses[lang] = text
78        return statuses
79
80    def _onPresence(self, presence):
81        type = presence.getAttribute("type", "available")
82        try:
83            handler = getattr(self, '_onPresence%s' % (type.capitalize()))
84        except AttributeError:
85            return
86        else:
87            handler(presence)
88
89    def _onPresenceAvailable(self, presence):
90        entity = JID(presence["from"])
91
92        show = unicode(presence.show or '')
93        if show not in ['away', 'xa', 'chat', 'dnd']:
94            show = None
95
96        statuses = self._getStatuses(presence)
97
98        try:
99            priority = int(unicode(presence.priority or '')) or 0
100        except ValueError:
101            priority = 0
102
103        self.availableReceived(entity, show, statuses, priority)
104
105    def _onPresenceUnavailable(self, presence):
106        entity = JID(presence["from"])
107
108        statuses = self._getStatuses(presence)
109
110        self.unavailableReceived(entity, statuses)
111
112    def _onPresenceSubscribed(self, presence):
113        self.subscribedReceived(JID(presence["from"]))
114
115    def _onPresenceUnsubscribed(self, presence):
116        self.unsubscribedReceived(JID(presence["from"]))
117
118    def _onPresenceSubscribe(self, presence):
119        self.subscribeReceived(JID(presence["from"]))
120
121    def _onPresenceUnsubscribe(self, presence):
122        self.unsubscribeReceived(JID(presence["from"]))
123
124
125    def availableReceived(self, entity, show=None, statuses=None, priority=0):
126        """
127        Available presence was received.
128
129        @param entity: entity from which the presence was received.
130        @type entity: {JID}
131        @param show: detailed presence information. One of C{'away'}, C{'xa'},
132                     C{'chat'}, C{'dnd'} or C{None}.
133        @type show: C{str} or C{NoneType}
134        @param statuses: dictionary of natural language descriptions of the
135                         availability status, keyed by the language
136                         descriptor. A status without a language
137                         specified, is keyed with C{None}.
138        @type statuses: C{dict}
139        @param priority: priority level of the resource.
140        @type priority: C{int}
141        """
142
143    def unavailableReceived(self, entity, statuses=None):
144        """
145        Unavailable presence was received.
146
147        @param entity: entity from which the presence was received.
148        @type entity: {JID}
149        @param statuses: dictionary of natural language descriptions of the
150                         availability status, keyed by the language
151                         descriptor. A status without a language
152                         specified, is keyed with C{None}.
153        @type statuses: C{dict}
154        """
155
156    def subscribedReceived(self, entity):
157        """
158        Subscription approval confirmation was received.
159
160        @param entity: entity from which the confirmation was received.
161        @type entity: {JID}
162        """
163
164    def unsubscribedReceived(self, entity):
165        """
166        Unsubscription confirmation was received.
167
168        @param entity: entity from which the confirmation was received.
169        @type entity: {JID}
170        """
171
172    def subscribeReceived(self, entity):
173        """
174        Subscription request was received.
175
176        @param entity: entity from which the request was received.
177        @type entity: {JID}
178        """
179
180    def unsubscribeReceived(self, entity):
181        """
182        Unsubscription request was received.
183
184        @param entity: entity from which the request was received.
185        @type entity: {JID}
186        """
187
188    def available(self, entity=None, show=None, statuses=None, priority=0):
189        """
190        Send available presence.
191
192        @param entity: optional entity to which the presence should be sent.
193        @type entity: {JID}
194        @param show: optional detailed presence information. One of C{'away'},
195                     C{'xa'}, C{'chat'}, C{'dnd'}.
196        @type show: C{str}
197        @param statuses: dictionary of natural language descriptions of the
198                         availability status, keyed by the language
199                         descriptor. A status without a language
200                         specified, is keyed with C{None}.
201        @type statuses: C{dict}
202        @param priority: priority level of the resource.
203        @type priority: C{int}
204        """
205        self.send(AvailablePresence(entity, show, statuses, priority))
206
207    def unavailable(self, entity=None, statuses=None):
208        """
209        Send unavailable presence.
210
211        @param entity: optional entity to which the presence should be sent.
212        @type entity: {JID}
213        @param statuses: dictionary of natural language descriptions of the
214                         availability status, keyed by the language
215                         descriptor. A status without a language
216                         specified, is keyed with C{None}.
217        @type statuses: C{dict}
218        """
219        self.send(UnavailablePresence(entity, statuses))
220
221    def subscribe(self, entity):
222        """
223        Send subscription request
224
225        @param entity: entity to subscribe to.
226        @type entity: {JID}
227        """
228        self.send(Presence(to=entity, type='subscribe'))
229
230    def unsubscribe(self, entity):
231        """
232        Send unsubscription request
233
234        @param entity: entity to unsubscribe from.
235        @type entity: {JID}
236        """
237        self.send(Presence(to=entity, type='unsubscribe'))
238
239    def subscribed(self, entity):
240        """
241        Send subscription confirmation.
242
243        @param entity: entity that subscribed.
244        @type entity: {JID}
245        """
246        self.send(Presence(to=entity, type='subscribed'))
247
248    def unsubscribed(self, entity):
249        """
250        Send unsubscription confirmation.
251
252        @param entity: entity that unsubscribed.
253        @type entity: {JID}
254        """
255        self.send(Presence(to=entity, type='unsubscribed'))
256
257
258
259class BasePresence(Stanza):
260    """
261    Stanza of kind presence.
262    """
263    stanzaKind = 'presence'
264
265
266
267class AvailabilityPresence(BasePresence):
268    """
269    Presence.
270
271    This represents availability presence (as opposed to
272    L{SubscriptionPresence}).
273
274    @ivar available: The availability being communicated.
275    @type available: C{bool}
276    @ivar show: More specific availability. Can be one of C{'chat'}, C{'away'},
277                C{'xa'}, C{'dnd'} or C{None}.
278    @type show: C{str} or C{NoneType}
279    @ivar statuses: Natural language texts to detail the (un)availability.
280                    These are represented as a mapping from language code
281                    (C{str} or C{None}) to the corresponding text (C{unicode}).
282                    If the key is C{None}, the associated text is in the
283                    default language.
284    @type statuses: C{dict}
285    @ivar priority: Priority level for this resource. Must be between -128 and
286                    127. Defaults to 0.
287    @type priority: C{int}
288    """
289
290    childParsers = {(None, 'show'): '_childParser_show',
291                     (None, 'status'): '_childParser_status',
292                     (None, 'priority'): '_childParser_priority'}
293
294    def __init__(self, recipient=None, sender=None, available=True,
295                       show=None, status=None, statuses=None, priority=0):
296        BasePresence.__init__(self, recipient=recipient, sender=sender)
297        self.available = available
298        self.show = show
299        self.statuses = statuses or {}
300        if status:
301            self.statuses[None] = status
302        self.priority = priority
303
304
305    def __get_status(self):
306        if None in self.statuses:
307            return self.statuses[None]
308        elif self.statuses:
309            for status in self.status.itervalues():
310                return status
311        else:
312            return None
313
314    status = property(__get_status)
315
316
317    def _childParser_show(self, element):
318        show = unicode(element)
319        if show in ('chat', 'away', 'xa', 'dnd'):
320            self.show = show
321
322
323    def _childParser_status(self, element):
324        lang = element.getAttribute((NS_XML, 'lang'), None)
325        text = unicode(element)
326        self.statuses[lang] = text
327
328
329    def _childParser_priority(self, element):
330        try:
331            self.priority = int(unicode(element))
332        except ValueError:
333            pass
334
335
336    def parseElement(self, element):
337        BasePresence.parseElement(self, element)
338
339        if self.stanzaType == 'unavailable':
340            self.available = False
341
342
343    def toElement(self):
344        if not self.available:
345            self.stanzaType = 'unavailable'
346
347        presence = BasePresence.toElement(self)
348
349        if self.available:
350            if self.show in ('chat', 'away', 'xa', 'dnd'):
351                presence.addElement('show', content=self.show)
352            if self.priority != 0:
353                presence.addElement('priority', content=unicode(self.priority))
354
355        for lang, text in self.statuses.iteritems():
356            status = presence.addElement('status', content=text)
357            if lang:
358                status[(NS_XML, 'lang')] = lang
359
360        return presence
361
362
363
364class SubscriptionPresence(BasePresence):
365    """
366    Presence subscription request or response.
367
368    This kind of presence is used to represent requests for presence
369    subscription and their replies.
370
371    Based on L{BasePresence} and {Stanza}, it just uses the C{stanzaType}
372    attribute to represent the type of subscription presence. This can be
373    one of C{'subscribe'}, C{'unsubscribe'}, C{'subscribed'} and
374    C{'unsubscribed'}.
375    """
376
377
378
379class ProbePresence(BasePresence):
380    """
381    Presence probe request.
382    """
383
384    stanzaType = 'probe'
385
386
387
388class BasePresenceProtocol(XMPPHandler):
389    """
390    XMPP Presence base protocol handler.
391
392    This class is the base for protocol handlers that receive presence
393    stanzas. Listening to all incoming presence stanzas, it extracts the
394    stanza's type and looks up a matching stanza parser and calls the
395    associated method. The method's name is the type + C{Received}. E.g.
396    C{availableReceived}. See L{PresenceProtocol} for a complete example.
397
398    @cvar presenceTypeParserMap: Maps presence stanza types to their respective
399        stanza parser classes (derived from L{Stanza}).
400    @type presenceTypeParserMap: C{dict}
401    """
402
403    presenceTypeParserMap = {}
404
405    def connectionInitialized(self):
406        self.xmlstream.addObserver("/presence", self._onPresence)
407
408
409
410    def _onPresence(self, element):
411        """
412        Called when a presence stanza has been received.
413        """
414        stanza = Stanza.fromElement(element)
415
416        presenceType = stanza.stanzaType or 'available'
417
418        try:
419            parser = self.presenceTypeParserMap[presenceType]
420        except KeyError:
421            return
422
423        presence = parser.fromElement(element)
424
425        try:
426            handler = getattr(self, '%sReceived' % presenceType)
427        except AttributeError:
428            return
429        else:
430            handler(presence)
431
432
433
434class PresenceProtocol(BasePresenceProtocol):
435
436    presenceTypeParserMap = {
437                'error': ErrorStanza,
438                'available': AvailabilityPresence,
439                'unavailable': AvailabilityPresence,
440                'subscribe': SubscriptionPresence,
441                'unsubscribe': SubscriptionPresence,
442                'subscribed': SubscriptionPresence,
443                'unsubscribed': SubscriptionPresence,
444                'probe': ProbePresence,
445                }
446
447
448    def errorReceived(self, presence):
449        """
450        Error presence was received.
451        """
452        pass
453
454
455    def availableReceived(self, presence):
456        """
457        Available presence was received.
458        """
459        pass
460
461
462    def unavailableReceived(self, presence):
463        """
464        Unavailable presence was received.
465        """
466        pass
467
468
469    def subscribedReceived(self, presence):
470        """
471        Subscription approval confirmation was received.
472        """
473        pass
474
475
476    def unsubscribedReceived(self, presence):
477        """
478        Unsubscription confirmation was received.
479        """
480        pass
481
482
483    def subscribeReceived(self, presence):
484        """
485        Subscription request was received.
486        """
487        pass
488
489
490    def unsubscribeReceived(self, presence):
491        """
492        Unsubscription request was received.
493        """
494        pass
495
496
497    def probeReceived(self, presence):
498        """
499        Probe presence was received.
500        """
501        pass
502
503
504    def available(self, recipient=None, show=None, statuses=None, priority=0,
505                        status=None, sender=None):
506        """
507        Send available presence.
508
509        @param recipient: Optional Recipient to which the presence should be
510            sent.
511        @type recipient: {JID}
512
513        @param show: Optional detailed presence information. One of C{'away'},
514            C{'xa'}, C{'chat'}, C{'dnd'}.
515        @type show: C{str}
516
517        @param statuses: Mapping of natural language descriptions of the
518           availability status, keyed by the language descriptor. A status
519           without a language specified, is keyed with C{None}.
520        @type statuses: C{dict}
521
522        @param priority: priority level of the resource.
523        @type priority: C{int}
524        """
525        presence = AvailabilityPresence(recipient=recipient, sender=sender,
526                                        show=show, statuses=statuses,
527                                        status=status, priority=priority)
528        self.send(presence.toElement())
529
530
531    def unavailable(self, recipient=None, statuses=None, sender=None):
532        """
533        Send unavailable presence.
534
535        @param recipient: Optional entity to which the presence should be sent.
536        @type recipient: {JID}
537
538        @param statuses: dictionary of natural language descriptions of the
539            availability status, keyed by the language descriptor. A status
540            without a language specified, is keyed with C{None}.
541        @type statuses: C{dict}
542        """
543        presence = AvailabilityPresence(recipient=recipient, sender=sender,
544                                        available=False, statuses=statuses)
545        self.send(presence.toElement())
546
547
548    def subscribe(self, recipient, sender=None):
549        """
550        Send subscription request
551
552        @param recipient: Entity to subscribe to.
553        @type recipient: {JID}
554        """
555        presence = SubscriptionPresence(recipient=recipient, sender=sender)
556        presence.stanzaType = 'subscribe'
557        self.send(presence.toElement())
558
559
560    def unsubscribe(self, recipient, sender=None):
561        """
562        Send unsubscription request
563
564        @param recipient: Entity to unsubscribe from.
565        @type recipient: {JID}
566        """
567        presence = SubscriptionPresence(recipient=recipient, sender=sender)
568        presence.stanzaType = 'unsubscribe'
569        self.send(presence.toElement())
570
571
572    def subscribed(self, recipient, sender=None):
573        """
574        Send subscription confirmation.
575
576        @param recipient: Entity that subscribed.
577        @type recipient: {JID}
578        """
579        presence = SubscriptionPresence(recipient=recipient, sender=sender)
580        presence.stanzaType = 'subscribed'
581        self.send(presence.toElement())
582
583
584    def unsubscribed(self, recipient, sender=None):
585        """
586        Send unsubscription confirmation.
587
588        @param recipient: Entity that unsubscribed.
589        @type recipient: {JID}
590        """
591        presence = SubscriptionPresence(recipient=recipient, sender=sender)
592        presence.stanzaType = 'unsubscribed'
593        self.send(presence.toElement())
594
595
596    def probe(self, recipient, sender=None):
597        """
598        Send presence probe.
599
600        @param recipient: Entity to be probed.
601        @type recipient: {JID}
602        """
603        presence = ProbePresence(recipient=recipient, sender=sender)
604        self.send(presence.toElement())
605
606
607
608class RosterItem(object):
609    """
610    Roster item.
611
612    This represents one contact from an XMPP contact list known as roster.
613
614    @ivar entity: The JID of the contact.
615    @type entity: L{JID}
616    @ivar name: The associated nickname for this contact.
617    @type name: C{unicode}
618    @ivar subscriptionTo: Subscription state to contact's presence. If C{True},
619                          the roster owner is subscribed to the presence
620                          information of the contact.
621    @type subscriptionTo: C{bool}
622    @ivar subscriptionFrom: Contact's subscription state. If C{True}, the
623                            contact is subscribed to the presence information
624                            of the roster owner.
625    @type subscriptionFrom: C{bool}
626    @ivar pendingOut: Whether the subscription request to this contact is
627        pending.
628    @type pendingOut: C{bool}
629    @ivar groups: Set of groups this contact is categorized in. Groups are
630                  represented by an opaque identifier of type C{unicode}.
631    @type groups: C{set}
632    @ivar approved: Signals pre-approved subscription.
633    @type approved: C{bool}
634    @ivar remove: Signals roster item removal.
635    @type remove: C{bool}
636    """
637
638    __subscriptionStates = {(False, False): None,
639                            (True, False): 'to',
640                            (False, True): 'from',
641                            (True, True): 'both'}
642
643    def __init__(self, entity, subscriptionTo=False, subscriptionFrom=False,
644                       name=u'', groups=None):
645        self.entity = entity
646        self.subscriptionTo = subscriptionTo
647        self.subscriptionFrom = subscriptionFrom
648        self.name = name
649        self.groups = groups or set()
650
651        self.pendingOut = False
652        self.approved = False
653        self.remove = False
654
655
656    def __getJID(self):
657        warnings.warn(
658            "wokkel.xmppim.RosterItem.jid is deprecated. "
659            "Use RosterItem.entity instead.",
660            DeprecationWarning)
661        return self.entity
662
663
664    def __setJID(self, value):
665        warnings.warn(
666            "wokkel.xmppim.RosterItem.jid is deprecated. "
667            "Use RosterItem.entity instead.",
668            DeprecationWarning)
669        self.entity = value
670
671
672    jid = property(__getJID, __setJID, doc="""
673            JID of the contact. Deprecated in favour of C{entity}.""")
674
675
676    def __getAsk(self):
677        warnings.warn(
678            "wokkel.xmppim.RosterItem.ask is deprecated. "
679            "Use RosterItem.pendingOut instead.",
680            DeprecationWarning)
681        return self.pendingOut
682
683
684    def __setAsk(self, value):
685        warnings.warn(
686            "wokkel.xmppim.RosterItem.ask is deprecated. "
687            "Use RosterItem.pendingOut instead.",
688            DeprecationWarning)
689        self.pendingOut = value
690
691
692    ask = property(__getAsk, __setAsk, doc="""
693            Pending out subscription. Deprecated in favour of C{pendingOut}.""")
694
695
696    def toElement(self, rosterSet=False):
697        """
698        Render to a DOM representation.
699
700        If C{rosterSet} is set, some attributes, that may not be sent
701        as a roster set, will not be rendered.
702
703        @type rosterSet: C{boolean}.
704        """
705        element = domish.Element((NS_ROSTER, 'item'))
706        element['jid'] = self.entity.full()
707
708        if self.remove:
709            subscription = 'remove'
710        else:
711            if self.name:
712                element['name'] = self.name
713
714            if self.groups:
715                for group in self.groups:
716                    element.addElement('group', content=group)
717
718            if rosterSet:
719                subscription = None
720            else:
721                subscription = self.__subscriptionStates[self.subscriptionTo,
722                                                         self.subscriptionFrom]
723
724                if self.pendingOut:
725                    element['ask'] = u'subscribe'
726
727                if self.approved:
728                    element['approved'] = u'true'
729
730        if subscription:
731            element['subscription'] = subscription
732
733        return element
734
735
736    @classmethod
737    def fromElement(Class, element):
738        entity = JID(element['jid'])
739        item = Class(entity)
740        subscription = element.getAttribute('subscription')
741        if subscription == 'remove':
742            item.remove = True
743        else:
744            item.name = element.getAttribute('name', u'')
745            item.subscriptionTo = subscription in ('to', 'both')
746            item.subscriptionFrom = subscription in ('from', 'both')
747            item.pendingOut = element.getAttribute('ask') == 'subscribe'
748            item.approved = element.getAttribute('approved') in ('true', '1')
749            for subElement in domish.generateElementsQNamed(element.children,
750                                                            'group', NS_ROSTER):
751                item.groups.add(unicode(subElement))
752        return item
753
754
755
756class RosterRequest(Request):
757    """
758    Roster request.
759
760    @ivar item: Roster item to be set or pushed.
761    @type item: L{RosterItem}.
762
763    @ivar version: Roster version identifier for roster pushes and
764        retrieving the roster as a delta from a known cached version. This
765        should only be set if the recipient is known to support roster
766        versioning.
767    @type version: C{unicode}
768
769    @ivar rosterSet: If set, this is a roster set request. This flag is used
770        to make sure some attributes of the roster item are not rendered by
771        L{toElement}.
772    @type roster: C{boolean}
773    """
774    item = None
775    version = None
776    rosterSet = False
777
778    def parseRequest(self, element):
779        self.version = element.getAttribute('ver')
780
781        for child in element.elements(NS_ROSTER, 'item'):
782            self.item = RosterItem.fromElement(child)
783            break
784
785
786    def toElement(self):
787        element = Request.toElement(self)
788        query = element.addElement((NS_ROSTER, 'query'))
789        if self.version is not None:
790            query['ver'] = self.version
791        if self.item:
792            query.addChild(self.item.toElement(rosterSet=self.rosterSet))
793        return element
794
795
796
797class RosterPushIgnored(Exception):
798    """
799    Raised when this entity doesn't want to accept/trust a roster push.
800
801    To avert presence leaks, a handler can raise L{RosterPushIgnored} when
802    not accepting a roster push (directly or via Deferred). This will
803    result in a C{'service-unavailable'} error being sent in return.
804    """
805
806
807
808class Roster(dict):
809    """
810    In-memory roster container.
811
812    This provides a roster as a mapping from L{JID} to L{RosterItem}. If
813    roster versioning is used, the C{version} attribute holds the version
814    identifier for this version of the roster.
815
816    @ivar version: Roster version identifier.
817    @type version: C{unicode}.
818    """
819
820    version = None
821
822
823
824class RosterClientProtocol(XMPPHandler, IQHandlerMixin):
825    """
826    Client side XMPP roster protocol.
827
828    The roster can be retrieved using L{getRoster}. Subsequent changes to the
829    roster will be pushed, resulting in calls to L{setReceived} or
830    L{removeReceived}. These methods should be overridden to handle the
831    roster pushes.
832
833    RFC 6121 specifically allows entities other than a user's server to
834    hold a roster for that user. However, how a client should deal with
835    that is currently not yet specfied.
836
837    By default roster pushes from other source. I.e. when C{request.sender}
838    is set but the sender's bare JID is different from the user's bare JID.
839    Set L{allowAnySender} to allow roster pushes from any sender. To
840    avert presence leaks, L{RosterPushIgnored} should then be raised for
841    pushes from untrusted senders.
842
843    If roster versioning is supported by the server, the roster and
844    subsequent pushes are annotated with a version identifier. This can be
845    used to cache the roster on the client side. Upon reconnect, the client
846    can request the roster with the version identifier of the cached version.
847    The server may then choose to only send roster pushes for the changes
848    since that version, instead of a complete roster.
849
850    @cvar allowAnySender: Flag to allow roster pushes from any sender.
851        C{False} by default.
852    @type allowAnySender: C{boolean}
853    """
854
855    allowAnySender = False
856    iqHandlers = {XPATH_ROSTER_SET: "_onRosterSet"}
857
858
859    def connectionInitialized(self):
860        self.xmlstream.addObserver(XPATH_ROSTER_SET, self.handleRequest)
861
862
863    def getRoster(self, version=None):
864        """
865        Retrieve contact list.
866
867        The returned deferred fires with the result of the roster request as
868        L{Roster}, a mapping from contact JID to L{RosterItem}.
869
870        If roster versioning is supported, the recipient responds with either
871        a the complete roster or with an empty result. In case of complete
872        roster, the L{Roster} is annotated with a C{version} attribute that
873        holds the version identifier for this version of the roster. This
874        identifier should be used for caching.
875
876        If the recipient responds with an empty result, the returned deferred
877        fires with C{None}. This indicates that any roster modifications
878        since C{version} will be sent as roster pushes.
879
880        Note that the empty result (C{None}) is different from an empty
881        roster (L{Roster} with no items).
882
883        @param version: Optional version identifier of the last cashed
884            version of the roster. This shall only be set if the recipient is
885            known to support roster versioning. If there is no (valid) cached
886            version of the roster, but roster versioning is desired,
887            C{version} should be set to the empty string (C{u''}).
888        @type version: C{unicode}
889
890        @return: Roster as a mapping from L{JID} to L{RosterItem}.
891        @rtype: L{twisted.internet.defer.Deferred}
892        """
893
894        def processRoster(result):
895            if result.query is not None:
896                roster = Roster()
897                roster.version = result.query.getAttribute('ver')
898                for element in result.query.elements(NS_ROSTER, 'item'):
899                    item = RosterItem.fromElement(element)
900                    roster[item.entity] = item
901                return roster
902            else:
903                return None
904
905        request = RosterRequest(stanzaType='get')
906        request.version = version
907        d = self.request(request)
908        d.addCallback(processRoster)
909        return d
910
911
912    def setItem(self, item):
913        """
914        Add or modify a roster item.
915
916        Note that RFC 6121 doesn't allow all properties of a roster item to
917        be sent when setting a roster item. Only the C{name} and C{groups}
918        attributes from C{item} are sent to the server. Presence subscription
919        management must be done through L{PresenceProtocol}.
920
921        @param item: The roster item to be set.
922        @type item: L{RosterItem}.
923
924        @rtype: L{twisted.internet.defer.Deferred}
925        """
926        request = RosterRequest(stanzaType='set')
927        request.rosterSet = True
928        request.item = item
929        return self.request(request)
930
931
932    def removeItem(self, entity):
933        """
934        Remove an item from the contact list.
935
936        @param entity: The contact to remove the roster item for.
937        @type entity: L{JID<twisted.words.protocols.jabber.jid.JID>}
938
939        @rtype: L{twisted.internet.defer.Deferred}
940        """
941        item = RosterItem(entity)
942        item.remove = True
943        return self.setItem(item)
944
945
946    def _onRosterSet(self, iq):
947        def trapIgnored(failure):
948            failure.trap(RosterPushIgnored)
949            raise error.StanzaError('service-unavailable')
950
951        request = RosterRequest.fromElement(iq)
952
953        if (not self.allowAnySender and
954                request.sender and
955                request.sender.userhostJID() !=
956                self.parent.jid.userhostJID()):
957            d = defer.fail(RosterPushIgnored())
958        elif request.item.remove:
959            d = defer.maybeDeferred(self.removeReceived, request)
960        else:
961            d = defer.maybeDeferred(self.setReceived, request)
962        d.addErrback(trapIgnored)
963        return d
964
965
966    def setReceived(self, request):
967        """
968        Called when a roster push for a new or update item was received.
969
970        @param request: The push request.
971        @type request: L{RosterRequest}
972        """
973        if hasattr(self, 'onRosterSet'):
974            warnings.warn(
975                "wokkel.xmppim.RosterClientProtocol.onRosterSet "
976                "is deprecated. "
977                "Use RosterClientProtocol.setReceived instead.",
978                DeprecationWarning)
979            return defer.maybeDeferred(self.onRosterSet, request.item)
980
981
982    def removeReceived(self, request):
983        """
984        Called when a roster push for the removal of an item was received.
985
986        @param request: The push request.
987        @type request: L{RosterRequest}
988        """
989        if hasattr(self, 'onRosterRemove'):
990            warnings.warn(
991                "wokkel.xmppim.RosterClientProtocol.onRosterRemove "
992                "is deprecated. "
993                "Use RosterClientProtocol.removeReceived instead.",
994                DeprecationWarning)
995            return defer.maybeDeferred(self.onRosterRemove,
996                                       request.item.entity)
997
998
999
1000class Message(Stanza):
1001    """
1002    A message stanza.
1003    """
1004
1005    stanzaKind = 'message'
1006
1007    childParsers = {
1008            (None, 'body'): '_childParser_body',
1009            (None, 'subject'): '_childParser_subject',
1010            }
1011
1012    def __init__(self, recipient=None, sender=None, body=None, subject=None):
1013        Stanza.__init__(self, recipient, sender)
1014        self.body = body
1015        self.subject = subject
1016
1017
1018    def _childParser_body(self, element):
1019        self.body = unicode(element)
1020
1021
1022    def _childParser_subject(self, element):
1023        self.subject = unicode(element)
1024
1025
1026    def toElement(self):
1027        element = Stanza.toElement(self)
1028
1029        if self.body:
1030            element.addElement('body', content=self.body)
1031        if self.subject:
1032            element.addElement('subject', content=self.subject)
1033
1034        return element
1035
1036
1037
1038class MessageProtocol(XMPPHandler):
1039    """
1040    Generic XMPP subprotocol handler for incoming message stanzas.
1041    """
1042
1043    messageTypes = None, 'normal', 'chat', 'headline', 'groupchat'
1044
1045    def connectionInitialized(self):
1046        self.xmlstream.addObserver("/message", self._onMessage)
1047
1048    def _onMessage(self, message):
1049        if message.handled:
1050            return
1051
1052        messageType = message.getAttribute("type")
1053
1054        if messageType == 'error':
1055            return
1056
1057        if messageType not in self.messageTypes:
1058            message["type"] = 'normal'
1059
1060        self.onMessage(message)
1061
1062    def onMessage(self, message):
1063        """
1064        Called when a message stanza was received.
1065        """
Note: See TracBrowser for help on using the repository browser.