source: wokkel/xmppim.py

Last change on this file was 203:7f8f69adf13b, checked in by Ralph Meijer <ralphm@…>, 3 years ago

imported patch py3-xmppim.patch

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