source: wokkel/xmppim.py @ 179:4d3066bc4034

0.7.1
Last change on this file since 179:4d3066bc4034 was 179:4d3066bc4034, checked in by Ralph Meijer <ralphm@…>, 8 years ago

Release Wokkel 0.7.1.

  • Property exe set to *
File size: 32.9 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 was deprecated in Wokkel 0.7.1; "
659            "please 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 was deprecated in Wokkel 0.7.1; "
667            "please use RosterItem.entity instead.",
668            DeprecationWarning)
669        self.entity = value
670
671
672    jid = property(__getJID, __setJID,
673                   doc="JID of the contact. "
674                       "Deprecated in Wokkel 0.7.1; "
675                       "please use C{entity} instead.")
676
677
678    def __getAsk(self):
679        warnings.warn(
680            "wokkel.xmppim.RosterItem.ask was deprecated in Wokkel 0.7.1; "
681            "please use RosterItem.pendingOut instead.",
682            DeprecationWarning)
683        return self.pendingOut
684
685
686    def __setAsk(self, value):
687        warnings.warn(
688            "wokkel.xmppim.RosterItem.ask was deprecated in Wokkel 0.7.1; "
689            "please use RosterItem.pendingOut instead.",
690            DeprecationWarning)
691        self.pendingOut = value
692
693
694    ask = property(__getAsk, __setAsk,
695                   doc="Pending out subscription. "
696                       "Deprecated in Wokkel 0.7.1; "
697                       "please use C{pendingOut} instead.")
698
699
700    def toElement(self, rosterSet=False):
701        """
702        Render to a DOM representation.
703
704        If C{rosterSet} is set, some attributes, that may not be sent
705        as a roster set, will not be rendered.
706
707        @type rosterSet: C{boolean}.
708        """
709        element = domish.Element((NS_ROSTER, 'item'))
710        element['jid'] = self.entity.full()
711
712        if self.remove:
713            subscription = 'remove'
714        else:
715            if self.name:
716                element['name'] = self.name
717
718            if self.groups:
719                for group in self.groups:
720                    element.addElement('group', content=group)
721
722            if rosterSet:
723                subscription = None
724            else:
725                subscription = self.__subscriptionStates[self.subscriptionTo,
726                                                         self.subscriptionFrom]
727
728                if self.pendingOut:
729                    element['ask'] = u'subscribe'
730
731                if self.approved:
732                    element['approved'] = u'true'
733
734        if subscription:
735            element['subscription'] = subscription
736
737        return element
738
739
740    @classmethod
741    def fromElement(Class, element):
742        entity = JID(element['jid'])
743        item = Class(entity)
744        subscription = element.getAttribute('subscription')
745        if subscription == 'remove':
746            item.remove = True
747        else:
748            item.name = element.getAttribute('name', u'')
749            item.subscriptionTo = subscription in ('to', 'both')
750            item.subscriptionFrom = subscription in ('from', 'both')
751            item.pendingOut = element.getAttribute('ask') == 'subscribe'
752            item.approved = element.getAttribute('approved') in ('true', '1')
753            for subElement in domish.generateElementsQNamed(element.children,
754                                                            'group', NS_ROSTER):
755                item.groups.add(unicode(subElement))
756        return item
757
758
759
760class RosterRequest(Request):
761    """
762    Roster request.
763
764    @ivar item: Roster item to be set or pushed.
765    @type item: L{RosterItem}.
766
767    @ivar version: Roster version identifier for roster pushes and
768        retrieving the roster as a delta from a known cached version. This
769        should only be set if the recipient is known to support roster
770        versioning.
771    @type version: C{unicode}
772
773    @ivar rosterSet: If set, this is a roster set request. This flag is used
774        to make sure some attributes of the roster item are not rendered by
775        L{toElement}.
776    @type roster: C{boolean}
777    """
778    item = None
779    version = None
780    rosterSet = False
781
782    def parseRequest(self, element):
783        self.version = element.getAttribute('ver')
784
785        for child in element.elements(NS_ROSTER, 'item'):
786            self.item = RosterItem.fromElement(child)
787            break
788
789
790    def toElement(self):
791        element = Request.toElement(self)
792        query = element.addElement((NS_ROSTER, 'query'))
793        if self.version is not None:
794            query['ver'] = self.version
795        if self.item:
796            query.addChild(self.item.toElement(rosterSet=self.rosterSet))
797        return element
798
799
800
801class RosterPushIgnored(Exception):
802    """
803    Raised when this entity doesn't want to accept/trust a roster push.
804
805    To avert presence leaks, a handler can raise L{RosterPushIgnored} when
806    not accepting a roster push (directly or via Deferred). This will
807    result in a C{'service-unavailable'} error being sent in return.
808    """
809
810
811
812class Roster(dict):
813    """
814    In-memory roster container.
815
816    This provides a roster as a mapping from L{JID} to L{RosterItem}. If
817    roster versioning is used, the C{version} attribute holds the version
818    identifier for this version of the roster.
819
820    @ivar version: Roster version identifier.
821    @type version: C{unicode}.
822    """
823
824    version = None
825
826
827
828class RosterClientProtocol(XMPPHandler, IQHandlerMixin):
829    """
830    Client side XMPP roster protocol.
831
832    The roster can be retrieved using L{getRoster}. Subsequent changes to the
833    roster will be pushed, resulting in calls to L{setReceived} or
834    L{removeReceived}. These methods should be overridden to handle the
835    roster pushes.
836
837    RFC 6121 specifically allows entities other than a user's server to
838    hold a roster for that user. However, how a client should deal with
839    that is currently not yet specfied.
840
841    By default roster pushes from other source. I.e. when C{request.sender}
842    is set but the sender's bare JID is different from the user's bare JID.
843    Set L{allowAnySender} to allow roster pushes from any sender. To
844    avert presence leaks, L{RosterPushIgnored} should then be raised for
845    pushes from untrusted senders.
846
847    If roster versioning is supported by the server, the roster and
848    subsequent pushes are annotated with a version identifier. This can be
849    used to cache the roster on the client side. Upon reconnect, the client
850    can request the roster with the version identifier of the cached version.
851    The server may then choose to only send roster pushes for the changes
852    since that version, instead of a complete roster.
853
854    @cvar allowAnySender: Flag to allow roster pushes from any sender.
855        C{False} by default.
856    @type allowAnySender: C{boolean}
857    """
858
859    allowAnySender = False
860    iqHandlers = {XPATH_ROSTER_SET: "_onRosterSet"}
861
862
863    def connectionInitialized(self):
864        self.xmlstream.addObserver(XPATH_ROSTER_SET, self.handleRequest)
865
866
867    def getRoster(self, version=None):
868        """
869        Retrieve contact list.
870
871        The returned deferred fires with the result of the roster request as
872        L{Roster}, a mapping from contact JID to L{RosterItem}.
873
874        If roster versioning is supported, the recipient responds with either
875        a the complete roster or with an empty result. In case of complete
876        roster, the L{Roster} is annotated with a C{version} attribute that
877        holds the version identifier for this version of the roster. This
878        identifier should be used for caching.
879
880        If the recipient responds with an empty result, the returned deferred
881        fires with C{None}. This indicates that any roster modifications
882        since C{version} will be sent as roster pushes.
883
884        Note that the empty result (C{None}) is different from an empty
885        roster (L{Roster} with no items).
886
887        @param version: Optional version identifier of the last cashed
888            version of the roster. This shall only be set if the recipient is
889            known to support roster versioning. If there is no (valid) cached
890            version of the roster, but roster versioning is desired,
891            C{version} should be set to the empty string (C{u''}).
892        @type version: C{unicode}
893
894        @return: Roster as a mapping from L{JID} to L{RosterItem}.
895        @rtype: L{twisted.internet.defer.Deferred}
896        """
897
898        def processRoster(result):
899            if result.query is not None:
900                roster = Roster()
901                roster.version = result.query.getAttribute('ver')
902                for element in result.query.elements(NS_ROSTER, 'item'):
903                    item = RosterItem.fromElement(element)
904                    roster[item.entity] = item
905                return roster
906            else:
907                return None
908
909        request = RosterRequest(stanzaType='get')
910        request.version = version
911        d = self.request(request)
912        d.addCallback(processRoster)
913        return d
914
915
916    def setItem(self, item):
917        """
918        Add or modify a roster item.
919
920        Note that RFC 6121 doesn't allow all properties of a roster item to
921        be sent when setting a roster item. Only the C{name} and C{groups}
922        attributes from C{item} are sent to the server. Presence subscription
923        management must be done through L{PresenceProtocol}.
924
925        @param item: The roster item to be set.
926        @type item: L{RosterItem}.
927
928        @rtype: L{twisted.internet.defer.Deferred}
929        """
930        request = RosterRequest(stanzaType='set')
931        request.rosterSet = True
932        request.item = item
933        return self.request(request)
934
935
936    def removeItem(self, entity):
937        """
938        Remove an item from the contact list.
939
940        @param entity: The contact to remove the roster item for.
941        @type entity: L{JID<twisted.words.protocols.jabber.jid.JID>}
942
943        @rtype: L{twisted.internet.defer.Deferred}
944        """
945        item = RosterItem(entity)
946        item.remove = True
947        return self.setItem(item)
948
949
950    def _onRosterSet(self, iq):
951        def trapIgnored(failure):
952            failure.trap(RosterPushIgnored)
953            raise error.StanzaError('service-unavailable')
954
955        request = RosterRequest.fromElement(iq)
956
957        if (not self.allowAnySender and
958                request.sender and
959                request.sender.userhostJID() !=
960                self.parent.jid.userhostJID()):
961            d = defer.fail(RosterPushIgnored())
962        elif request.item.remove:
963            d = defer.maybeDeferred(self.removeReceived, request)
964        else:
965            d = defer.maybeDeferred(self.setReceived, request)
966        d.addErrback(trapIgnored)
967        return d
968
969
970    def setReceived(self, request):
971        """
972        Called when a roster push for a new or update item was received.
973
974        @param request: The push request.
975        @type request: L{RosterRequest}
976        """
977        if hasattr(self, 'onRosterSet'):
978            warnings.warn(
979                "wokkel.xmppim.RosterClientProtocol.onRosterSet "
980                "was deprecated in Wokkel 0.7.1; "
981                "please use RosterClientProtocol.setReceived instead.",
982                DeprecationWarning)
983            return defer.maybeDeferred(self.onRosterSet, request.item)
984
985
986    def removeReceived(self, request):
987        """
988        Called when a roster push for the removal of an item was received.
989
990        @param request: The push request.
991        @type request: L{RosterRequest}
992        """
993        if hasattr(self, 'onRosterRemove'):
994            warnings.warn(
995                "wokkel.xmppim.RosterClientProtocol.onRosterRemove "
996                "was deprecated in Wokkel 0.7.1; "
997                "please use RosterClientProtocol.removeReceived instead.",
998                DeprecationWarning)
999            return defer.maybeDeferred(self.onRosterRemove,
1000                                       request.item.entity)
1001
1002
1003
1004class Message(Stanza):
1005    """
1006    A message stanza.
1007    """
1008
1009    stanzaKind = 'message'
1010
1011    childParsers = {
1012            (None, 'body'): '_childParser_body',
1013            (None, 'subject'): '_childParser_subject',
1014            }
1015
1016    def __init__(self, recipient=None, sender=None, body=None, subject=None):
1017        Stanza.__init__(self, recipient, sender)
1018        self.body = body
1019        self.subject = subject
1020
1021
1022    def _childParser_body(self, element):
1023        self.body = unicode(element)
1024
1025
1026    def _childParser_subject(self, element):
1027        self.subject = unicode(element)
1028
1029
1030    def toElement(self):
1031        element = Stanza.toElement(self)
1032
1033        if self.body:
1034            element.addElement('body', content=self.body)
1035        if self.subject:
1036            element.addElement('subject', content=self.subject)
1037
1038        return element
1039
1040
1041
1042class MessageProtocol(XMPPHandler):
1043    """
1044    Generic XMPP subprotocol handler for incoming message stanzas.
1045    """
1046
1047    messageTypes = None, 'normal', 'chat', 'headline', 'groupchat'
1048
1049    def connectionInitialized(self):
1050        self.xmlstream.addObserver("/message", self._onMessage)
1051
1052    def _onMessage(self, message):
1053        if message.handled:
1054            return
1055
1056        messageType = message.getAttribute("type")
1057
1058        if messageType == 'error':
1059            return
1060
1061        if messageType not in self.messageTypes:
1062            message["type"] = 'normal'
1063
1064        self.onMessage(message)
1065
1066    def onMessage(self, message):
1067        """
1068        Called when a message stanza was received.
1069        """
Note: See TracBrowser for help on using the repository browser.