source: wokkel/xmppim.py @ 173:6b0eb01b5744

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

Add support for roster versioning.

  • Property exe set to *
File size: 31.4 KB
RevLine 
[9]1# -*- test-case-name: wokkel.test.test_xmppim -*-
2#
[96]3# Copyright (c) Ralph Meijer.
[2]4# See LICENSE for details.
5
6"""
7XMPP IM protocol support.
8
9This module provides generic implementations for the protocols defined in
[172]10U{RFC 6121<http://www.xmpp.org/rfcs/rfc6121.html>} (XMPP IM).
[2]11"""
12
[172]13import warnings
14
15from twisted.internet import defer
16from twisted.words.protocols.jabber import error
[2]17from twisted.words.protocols.jabber.jid import JID
18from twisted.words.xish import domish
19
[172]20from wokkel.generic import ErrorStanza, Stanza, Request
21from wokkel.subprotocols import IQHandlerMixin
[2]22from wokkel.subprotocols import XMPPHandler
23
24NS_XML = 'http://www.w3.org/XML/1998/namespace'
25NS_ROSTER = 'jabber:iq:roster'
26
[172]27XPATH_ROSTER_SET = "/iq[@type='set']/query[@xmlns='%s']" % NS_ROSTER
28
29
30
[2]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:
[12]54            self.addElement('priority', content=unicode(int(priority)))
[2]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
[68]124
[2]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
[9]207    def unavailable(self, entity=None, statuses=None):
[2]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        """
[9]219        self.send(UnavailablePresence(entity, statuses))
[2]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
[68]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
[141]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
[68]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
[166]371    Based on L{BasePresence} and {Stanza}, it just uses the C{stanzaType}
[68]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
[141]388class BasePresenceProtocol(XMPPHandler):
[68]389    """
[141]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.
[68]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
[141]403    presenceTypeParserMap = {}
[68]404
405    def connectionInitialized(self):
406        self.xmlstream.addObserver("/presence", self._onPresence)
407
408
[141]409
[68]410    def _onPresence(self, element):
[141]411        """
412        Called when a presence stanza has been received.
413        """
[68]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
[141]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
[68]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
[2]608class RosterItem(object):
609    """
610    Roster item.
611
612    This represents one contact from an XMPP contact list known as roster.
613
[172]614    @ivar entity: The JID of the contact.
615    @type entity: L{JID}
616    @ivar name: The associated nickname for this contact.
[2]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.
[172]625    @type subscriptionFrom: C{bool}
626    @ivar pendingOut: Whether the subscription request to this contact is
627        pending.
628    @type pendingOut: C{bool}
[2]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}
[172]632    @ivar approved: Signals pre-approved subscription.
633    @type approved: C{bool}
634    @ivar remove: Signals roster item removal.
635    @type remove: C{bool}
[2]636    """
637
[172]638    __subscriptionStates = {(False, False): None,
639                            (True, False): 'to',
640                            (False, True): 'from',
641                            (True, True): 'both'}
[2]642
[172]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()
[2]650
[172]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):
697        element = domish.Element((NS_ROSTER, 'item'))
698        element['jid'] = self.entity.full()
699
700        if self.remove:
701            subscription = 'remove'
702        else:
703            subscription = self.__subscriptionStates[self.subscriptionTo,
704                                                     self.subscriptionFrom]
705
706            if self.pendingOut:
707                element['ask'] = u'subscribe'
708
709            if self.name:
710                element['name'] = self.name
711
712            if self.approved:
713                element['approved'] = u'true'
714
715            if self.groups:
716                for group in self.groups:
717                    element.addElement('group', content=group)
718
719        if subscription:
720            element['subscription'] = subscription
721
722        return element
723
724
725    @classmethod
726    def fromElement(Class, element):
727        entity = JID(element['jid'])
728        item = Class(entity)
729        subscription = element.getAttribute('subscription')
730        if subscription == 'remove':
731            item.remove = True
732        else:
733            item.name = element.getAttribute('name', u'')
734            item.subscriptionTo = subscription in ('to', 'both')
735            item.subscriptionFrom = subscription in ('from', 'both')
736            item.pendingOut = element.getAttribute('ask') == 'subscribe'
737            item.approved = element.getAttribute('approved') in ('true', '1')
738            for subElement in domish.generateElementsQNamed(element.children,
739                                                            'group', NS_ROSTER):
740                item.groups.add(unicode(subElement))
741        return item
742
743
744
745class RosterRequest(Request):
746    """
747    Roster request.
748
749    @ivar item: Roster item to be set or pushed.
750    @type item: L{RosterItem}.
[173]751
752    @ivar version: Roster version identifier for roster pushes and
753        retrieving the roster as a delta from a known cached version. This
754        should only be set if the recipient is known to support roster
755        versioning.
756    @type version: C{unicode}
[172]757    """
758    item = None
[173]759    version = None
[172]760
761    def parseRequest(self, element):
[173]762        self.version = element.getAttribute('ver')
763
[172]764        for child in element.elements(NS_ROSTER, 'item'):
765            self.item = RosterItem.fromElement(child)
766            break
767
768
769    def toElement(self):
770        element = Request.toElement(self)
771        query = element.addElement((NS_ROSTER, 'query'))
[173]772        if self.version is not None:
773            query['ver'] = self.version
[172]774        if self.item:
775            query.addChild(self.item.toElement())
776        return element
777
778
779
780class RosterPushIgnored(Exception):
781    """
782    Raised when this entity doesn't want to accept/trust a roster push.
783
784    To avert presence leaks, a handler can raise L{RosterPushIgnored} when
785    not accepting a roster push (directly or via Deferred). This will
786    result in a C{'service-unavailable'} error being sent in return.
787    """
788
789
790
[173]791class Roster(dict):
792    """
793    In-memory roster container.
794
795    This provides a roster as a mapping from L{JID} to L{RosterItem}. If
796    roster versioning is used, the C{version} attribute holds the version
797    identifier for this version of the roster.
798
799    @ivar version: Roster version identifier.
800    @type version: C{unicode}.
801    """
802
803    version = None
804
805
806
[172]807class RosterClientProtocol(XMPPHandler, IQHandlerMixin):
[2]808    """
809    Client side XMPP roster protocol.
[172]810
811    The roster can be retrieved using L{getRoster}. Subsequent changes to the
812    roster will be pushed, resulting in calls to L{setReceived} or
813    L{removeReceived}. These methods should be overridden to handle the
814    roster pushes.
815
816    RFC 6121 specifically allows entities other than a user's server to
817    hold a roster for that user. However, how a client should deal with
818    that is currently not yet specfied.
819
820    By default roster pushes from other source. I.e. when C{request.sender}
821    is set but the sender's bare JID is different from the user's bare JID.
822    Set L{allowAnySender} to allow roster pushes from any sender. To
823    avert presence leaks, L{RosterPushIgnored} should then be raised for
824    pushes from untrusted senders.
825
[173]826    If roster versioning is supported by the server, the roster and
827    subsequent pushes are annotated with a version identifier. This can be
828    used to cache the roster on the client side. Upon reconnect, the client
829    can request the roster with the version identifier of the cached version.
830    The server may then choose to only send roster pushes for the changes
831    since that version, instead of a complete roster.
832
[172]833    @cvar allowAnySender: Flag to allow roster pushes from any sender.
834        C{False} by default.
835    @type allowAnySender: C{boolean}
[2]836    """
837
[172]838    allowAnySender = False
839    iqHandlers = {XPATH_ROSTER_SET: "_onRosterSet"}
840
841
[2]842    def connectionInitialized(self):
[172]843        self.xmlstream.addObserver(XPATH_ROSTER_SET, self.handleRequest)
[2]844
845
[173]846    def getRoster(self, version=None):
[2]847        """
848        Retrieve contact list.
849
[173]850        The returned deferred fires with the result of the roster request as
851        L{Roster}, a mapping from contact JID to L{RosterItem}.
852
853        If roster versioning is supported, the recipient responds with either
854        a the complete roster or with an empty result. In case of complete
855        roster, the L{Roster} is annotated with a C{version} attribute that
856        holds the version identifier for this version of the roster. This
857        identifier should be used for caching.
858
859        If the recipient responds with an empty result, the returned deferred
860        fires with C{None}. This indicates that any roster modifications
861        since C{version} will be sent as roster pushes.
862
863        Note that the empty result (C{None}) is different from an empty
864        roster (L{Roster} with no items).
865
866        @param version: Optional version identifier of the last cashed
867            version of the roster. This shall only be set if the recipient is
868            known to support roster versioning. If there is no (valid) cached
869            version of the roster, but roster versioning is desired,
870            C{version} should be set to the empty string (C{u''}).
871        @type version: C{unicode}
872
[2]873        @return: Roster as a mapping from L{JID} to L{RosterItem}.
874        @rtype: L{twisted.internet.defer.Deferred}
875        """
876
877        def processRoster(result):
[173]878            if result.query is not None:
879                roster = Roster()
880                roster.version = result.query.getAttribute('ver')
881                for element in result.query.elements(NS_ROSTER, 'item'):
882                    item = RosterItem.fromElement(element)
883                    roster[item.entity] = item
884                return roster
885            else:
886                return None
[2]887
[172]888        request = RosterRequest(stanzaType='get')
[173]889        request.version = version
[172]890        d = self.request(request)
[2]891        d.addCallback(processRoster)
892        return d
893
[28]894
895    def removeItem(self, entity):
896        """
897        Remove an item from the contact list.
898
899        @param entity: The contact to remove the roster item for.
900        @type entity: L{JID<twisted.words.protocols.jabber.jid.JID>}
901        @rtype: L{twisted.internet.defer.Deferred}
902        """
[172]903        request = RosterRequest(stanzaType='set')
904        request.item = RosterItem(entity)
905        request.item.remove = True
906        return self.request(request)
[28]907
908
[2]909    def _onRosterSet(self, iq):
[172]910        def trapIgnored(failure):
911            failure.trap(RosterPushIgnored)
912            raise error.StanzaError('service-unavailable')
[2]913
[172]914        request = RosterRequest.fromElement(iq)
[2]915
[172]916        if (not self.allowAnySender and
917                request.sender and
918                request.sender.userhostJID() !=
919                self.parent.jid.userhostJID()):
920            d = defer.fail(RosterPushIgnored())
921        elif request.item.remove:
922            d = defer.maybeDeferred(self.removeReceived, request)
923        else:
924            d = defer.maybeDeferred(self.setReceived, request)
925        d.addErrback(trapIgnored)
926        return d
[2]927
928
[172]929    def setReceived(self, request):
[2]930        """
931        Called when a roster push for a new or update item was received.
932
[172]933        @param request: The push request.
934        @type request: L{RosterRequest}
[2]935        """
[172]936        if hasattr(self, 'onRosterSet'):
937            warnings.warn(
938                "wokkel.xmppim.RosterClientProtocol.onRosterSet "
939                "is deprecated. "
940                "Use RosterClientProtocol.setReceived instead.",
941                DeprecationWarning)
942            return defer.maybeDeferred(self.onRosterSet, request.item)
[2]943
[172]944
945    def removeReceived(self, request):
[2]946        """
947        Called when a roster push for the removal of an item was received.
948
[172]949        @param request: The push request.
950        @type request: L{RosterRequest}
[2]951        """
[172]952        if hasattr(self, 'onRosterRemove'):
953            warnings.warn(
954                "wokkel.xmppim.RosterClientProtocol.onRosterRemove "
955                "is deprecated. "
956                "Use RosterClientProtocol.removeReceived instead.",
957                DeprecationWarning)
958            return defer.maybeDeferred(self.onRosterRemove,
959                                       request.item.entity)
[2]960
[141]961
962
963class Message(Stanza):
964    """
965    A message stanza.
966    """
967
968    stanzaKind = 'message'
969
970    childParsers = {
971            (None, 'body'): '_childParser_body',
972            (None, 'subject'): '_childParser_subject',
973            }
974
975    def __init__(self, recipient=None, sender=None, body=None, subject=None):
976        Stanza.__init__(self, recipient, sender)
977        self.body = body
978        self.subject = subject
979
980
981    def _childParser_body(self, element):
982        self.body = unicode(element)
983
984
985    def _childParser_subject(self, element):
986        self.subject = unicode(element)
987
988
989    def toElement(self):
990        element = Stanza.toElement(self)
991
992        if self.body:
993            element.addElement('body', content=self.body)
994        if self.subject:
995            element.addElement('subject', content=self.subject)
996
997        return element
998
999
1000
[2]1001class MessageProtocol(XMPPHandler):
1002    """
1003    Generic XMPP subprotocol handler for incoming message stanzas.
1004    """
1005
1006    messageTypes = None, 'normal', 'chat', 'headline', 'groupchat'
1007
1008    def connectionInitialized(self):
1009        self.xmlstream.addObserver("/message", self._onMessage)
1010
1011    def _onMessage(self, message):
1012        if message.handled:
1013            return
1014
1015        messageType = message.getAttribute("type")
1016
1017        if messageType == 'error':
1018            return
1019
1020        if messageType not in self.messageTypes:
1021            message["type"] = 'normal'
1022
1023        self.onMessage(message)
1024
1025    def onMessage(self, message):
1026        """
1027        Called when a message stanza was received.
1028        """
Note: See TracBrowser for help on using the repository browser.