source: wokkel/xmppim.py @ 96:8e6130587088

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

Remove copyright dates from individual source files, only update LICENSE.

  • Property exe set to *
File size: 21.8 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
10U{RFC 3921<http://www.xmpp.org/rfcs/rfc3921.html>} (XMPP IM).
11
12All of it should eventually move to Twisted.
13"""
14
15from twisted.words.protocols.jabber.jid import JID
16from twisted.words.xish import domish
17
[63]18from wokkel.compat import IQ
[68]19from wokkel.generic import ErrorStanza, Stanza
[2]20from wokkel.subprotocols import XMPPHandler
21
22NS_XML = 'http://www.w3.org/XML/1998/namespace'
23NS_ROSTER = 'jabber:iq:roster'
24
25class Presence(domish.Element):
26    def __init__(self, to=None, type=None):
27        domish.Element.__init__(self, (None, "presence"))
28        if type:
29            self["type"] = type
30
31        if to is not None:
32            self["to"] = to.full()
33
34class AvailablePresence(Presence):
35    def __init__(self, to=None, show=None, statuses=None, priority=0):
36        Presence.__init__(self, to, type=None)
37
38        if show in ['away', 'xa', 'chat', 'dnd']:
39            self.addElement('show', content=show)
40
41        if statuses is not None:
42            for lang, status in statuses.iteritems():
43                s = self.addElement('status', content=status)
44                if lang:
45                    s[(NS_XML, "lang")] = lang
46
47        if priority != 0:
[12]48            self.addElement('priority', content=unicode(int(priority)))
[2]49
50class UnavailablePresence(Presence):
51    def __init__(self, to=None, statuses=None):
52        Presence.__init__(self, to, type='unavailable')
53
54        if statuses is not None:
55            for lang, status in statuses.iteritems():
56                s = self.addElement('status', content=status)
57                if lang:
58                    s[(NS_XML, "lang")] = lang
59
60class PresenceClientProtocol(XMPPHandler):
61
62    def connectionInitialized(self):
63        self.xmlstream.addObserver('/presence', self._onPresence)
64
65    def _getStatuses(self, presence):
66        statuses = {}
67        for element in presence.elements():
68            if element.name == 'status':
69                lang = element.getAttribute((NS_XML, 'lang'))
70                text = unicode(element)
71                statuses[lang] = text
72        return statuses
73
74    def _onPresence(self, presence):
75        type = presence.getAttribute("type", "available")
76        try:
77            handler = getattr(self, '_onPresence%s' % (type.capitalize()))
78        except AttributeError:
79            return
80        else:
81            handler(presence)
82
83    def _onPresenceAvailable(self, presence):
84        entity = JID(presence["from"])
85
86        show = unicode(presence.show or '')
87        if show not in ['away', 'xa', 'chat', 'dnd']:
88            show = None
89
90        statuses = self._getStatuses(presence)
91
92        try:
93            priority = int(unicode(presence.priority or '')) or 0
94        except ValueError:
95            priority = 0
96
97        self.availableReceived(entity, show, statuses, priority)
98
99    def _onPresenceUnavailable(self, presence):
100        entity = JID(presence["from"])
101
102        statuses = self._getStatuses(presence)
103
104        self.unavailableReceived(entity, statuses)
105
106    def _onPresenceSubscribed(self, presence):
107        self.subscribedReceived(JID(presence["from"]))
108
109    def _onPresenceUnsubscribed(self, presence):
110        self.unsubscribedReceived(JID(presence["from"]))
111
112    def _onPresenceSubscribe(self, presence):
113        self.subscribeReceived(JID(presence["from"]))
114
115    def _onPresenceUnsubscribe(self, presence):
116        self.unsubscribeReceived(JID(presence["from"]))
117
[68]118
[2]119    def availableReceived(self, entity, show=None, statuses=None, priority=0):
120        """
121        Available presence was received.
122
123        @param entity: entity from which the presence was received.
124        @type entity: {JID}
125        @param show: detailed presence information. One of C{'away'}, C{'xa'},
126                     C{'chat'}, C{'dnd'} or C{None}.
127        @type show: C{str} or C{NoneType}
128        @param statuses: dictionary of natural language descriptions of the
129                         availability status, keyed by the language
130                         descriptor. A status without a language
131                         specified, is keyed with C{None}.
132        @type statuses: C{dict}
133        @param priority: priority level of the resource.
134        @type priority: C{int}
135        """
136
137    def unavailableReceived(self, entity, statuses=None):
138        """
139        Unavailable presence was received.
140
141        @param entity: entity from which the presence was received.
142        @type entity: {JID}
143        @param statuses: dictionary of natural language descriptions of the
144                         availability status, keyed by the language
145                         descriptor. A status without a language
146                         specified, is keyed with C{None}.
147        @type statuses: C{dict}
148        """
149
150    def subscribedReceived(self, entity):
151        """
152        Subscription approval confirmation was received.
153
154        @param entity: entity from which the confirmation was received.
155        @type entity: {JID}
156        """
157
158    def unsubscribedReceived(self, entity):
159        """
160        Unsubscription confirmation was received.
161
162        @param entity: entity from which the confirmation was received.
163        @type entity: {JID}
164        """
165
166    def subscribeReceived(self, entity):
167        """
168        Subscription request was received.
169
170        @param entity: entity from which the request was received.
171        @type entity: {JID}
172        """
173
174    def unsubscribeReceived(self, entity):
175        """
176        Unsubscription request was received.
177
178        @param entity: entity from which the request was received.
179        @type entity: {JID}
180        """
181
182    def available(self, entity=None, show=None, statuses=None, priority=0):
183        """
184        Send available presence.
185
186        @param entity: optional entity to which the presence should be sent.
187        @type entity: {JID}
188        @param show: optional detailed presence information. One of C{'away'},
189                     C{'xa'}, C{'chat'}, C{'dnd'}.
190        @type show: C{str}
191        @param statuses: dictionary of natural language descriptions of the
192                         availability status, keyed by the language
193                         descriptor. A status without a language
194                         specified, is keyed with C{None}.
195        @type statuses: C{dict}
196        @param priority: priority level of the resource.
197        @type priority: C{int}
198        """
199        self.send(AvailablePresence(entity, show, statuses, priority))
200
[9]201    def unavailable(self, entity=None, statuses=None):
[2]202        """
203        Send unavailable presence.
204
205        @param entity: optional entity to which the presence should be sent.
206        @type entity: {JID}
207        @param statuses: dictionary of natural language descriptions of the
208                         availability status, keyed by the language
209                         descriptor. A status without a language
210                         specified, is keyed with C{None}.
211        @type statuses: C{dict}
212        """
[9]213        self.send(UnavailablePresence(entity, statuses))
[2]214
215    def subscribe(self, entity):
216        """
217        Send subscription request
218
219        @param entity: entity to subscribe to.
220        @type entity: {JID}
221        """
222        self.send(Presence(to=entity, type='subscribe'))
223
224    def unsubscribe(self, entity):
225        """
226        Send unsubscription request
227
228        @param entity: entity to unsubscribe from.
229        @type entity: {JID}
230        """
231        self.send(Presence(to=entity, type='unsubscribe'))
232
233    def subscribed(self, entity):
234        """
235        Send subscription confirmation.
236
237        @param entity: entity that subscribed.
238        @type entity: {JID}
239        """
240        self.send(Presence(to=entity, type='subscribed'))
241
242    def unsubscribed(self, entity):
243        """
244        Send unsubscription confirmation.
245
246        @param entity: entity that unsubscribed.
247        @type entity: {JID}
248        """
249        self.send(Presence(to=entity, type='unsubscribed'))
250
251
[68]252
253class BasePresence(Stanza):
254    """
255    Stanza of kind presence.
256    """
257    stanzaKind = 'presence'
258
259
260
261class AvailabilityPresence(BasePresence):
262    """
263    Presence.
264
265    This represents availability presence (as opposed to
266    L{SubscriptionPresence}).
267
268    @ivar available: The availability being communicated.
269    @type available: C{bool}
270    @ivar show: More specific availability. Can be one of C{'chat'}, C{'away'},
271                C{'xa'}, C{'dnd'} or C{None}.
272    @type show: C{str} or C{NoneType}
273    @ivar statuses: Natural language texts to detail the (un)availability.
274                    These are represented as a mapping from language code
275                    (C{str} or C{None}) to the corresponding text (C{unicode}).
276                    If the key is C{None}, the associated text is in the
277                    default language.
278    @type statuses: C{dict}
279    @ivar priority: Priority level for this resource. Must be between -128 and
280                    127. Defaults to 0.
281    @type priority: C{int}
282    """
283
284    childParsers = {(None, 'show'): '_childParser_show',
285                     (None, 'status'): '_childParser_status',
286                     (None, 'priority'): '_childParser_priority'}
287
288    def __init__(self, recipient=None, sender=None, available=True,
289                       show=None, status=None, statuses=None, priority=0):
290        BasePresence.__init__(self, recipient=recipient, sender=sender)
291        self.available = available
292        self.show = show
293        self.statuses = statuses or {}
294        if status:
295            self.statuses[None] = status
296        self.priority = priority
297
298
299    def _childParser_show(self, element):
300        show = unicode(element)
301        if show in ('chat', 'away', 'xa', 'dnd'):
302            self.show = show
303
304
305    def _childParser_status(self, element):
306        lang = element.getAttribute((NS_XML, 'lang'), None)
307        text = unicode(element)
308        self.statuses[lang] = text
309
310
311    def _childParser_priority(self, element):
312        try:
313            self.priority = int(unicode(element))
314        except ValueError:
315            pass
316
317
318    def parseElement(self, element):
319        BasePresence.parseElement(self, element)
320
321        if self.stanzaType == 'unavailable':
322            self.available = False
323
324
325    def toElement(self):
326        if not self.available:
327            self.stanzaType = 'unavailable'
328
329        presence = BasePresence.toElement(self)
330
331        if self.available:
332            if self.show in ('chat', 'away', 'xa', 'dnd'):
333                presence.addElement('show', content=self.show)
334            if self.priority != 0:
335                presence.addElement('priority', content=unicode(self.priority))
336
337        for lang, text in self.statuses.iteritems():
338            status = presence.addElement('status', content=text)
339            if lang:
340                status[(NS_XML, 'lang')] = lang
341
342        return presence
343
344
345
346class SubscriptionPresence(BasePresence):
347    """
348    Presence subscription request or response.
349
350    This kind of presence is used to represent requests for presence
351    subscription and their replies.
352
353    Based on L{BasePresence} and {Stanza}, it just uses the L{stanzaType}
354    attribute to represent the type of subscription presence. This can be
355    one of C{'subscribe'}, C{'unsubscribe'}, C{'subscribed'} and
356    C{'unsubscribed'}.
357    """
358
359
360
361class ProbePresence(BasePresence):
362    """
363    Presence probe request.
364    """
365
366    stanzaType = 'probe'
367
368
369
370class PresenceProtocol(XMPPHandler):
371    """
372    XMPP Presence protocol.
373
374    @cvar presenceTypeParserMap: Maps presence stanza types to their respective
375        stanza parser classes (derived from L{Stanza}).
376    @type presenceTypeParserMap: C{dict}
377    """
378
379    presenceTypeParserMap = {
380                'error': ErrorStanza,
381                'available': AvailabilityPresence,
382                'unavailable': AvailabilityPresence,
383                'subscribe': SubscriptionPresence,
384                'unsubscribe': SubscriptionPresence,
385                'subscribed': SubscriptionPresence,
386                'unsubscribed': SubscriptionPresence,
387                'probe': ProbePresence,
388        }
389
390    def connectionInitialized(self):
391        self.xmlstream.addObserver("/presence", self._onPresence)
392
393
394    def _onPresence(self, element):
395        stanza = Stanza.fromElement(element)
396
397        presenceType = stanza.stanzaType or 'available'
398
399        try:
400            parser = self.presenceTypeParserMap[presenceType]
401        except KeyError:
402            return
403
404        presence = parser.fromElement(element)
405
406        try:
407            handler = getattr(self, '%sReceived' % presenceType)
408        except AttributeError:
409            return
410        else:
411            handler(presence)
412
413
414    def errorReceived(self, presence):
415        """
416        Error presence was received.
417        """
418        pass
419
420
421    def availableReceived(self, presence):
422        """
423        Available presence was received.
424        """
425        pass
426
427
428    def unavailableReceived(self, presence):
429        """
430        Unavailable presence was received.
431        """
432        pass
433
434
435    def subscribedReceived(self, presence):
436        """
437        Subscription approval confirmation was received.
438        """
439        pass
440
441
442    def unsubscribedReceived(self, presence):
443        """
444        Unsubscription confirmation was received.
445        """
446        pass
447
448
449    def subscribeReceived(self, presence):
450        """
451        Subscription request was received.
452        """
453        pass
454
455
456    def unsubscribeReceived(self, presence):
457        """
458        Unsubscription request was received.
459        """
460        pass
461
462
463    def probeReceived(self, presence):
464        """
465        Probe presence was received.
466        """
467        pass
468
469
470    def available(self, recipient=None, show=None, statuses=None, priority=0,
471                        status=None, sender=None):
472        """
473        Send available presence.
474
475        @param recipient: Optional Recipient to which the presence should be
476            sent.
477        @type recipient: {JID}
478
479        @param show: Optional detailed presence information. One of C{'away'},
480            C{'xa'}, C{'chat'}, C{'dnd'}.
481        @type show: C{str}
482
483        @param statuses: Mapping of natural language descriptions of the
484           availability status, keyed by the language descriptor. A status
485           without a language specified, is keyed with C{None}.
486        @type statuses: C{dict}
487
488        @param priority: priority level of the resource.
489        @type priority: C{int}
490        """
491        presence = AvailabilityPresence(recipient=recipient, sender=sender,
492                                        show=show, statuses=statuses,
493                                        status=status, priority=priority)
494        self.send(presence.toElement())
495
496
497    def unavailable(self, recipient=None, statuses=None, sender=None):
498        """
499        Send unavailable presence.
500
501        @param recipient: Optional entity to which the presence should be sent.
502        @type recipient: {JID}
503
504        @param statuses: dictionary of natural language descriptions of the
505            availability status, keyed by the language descriptor. A status
506            without a language specified, is keyed with C{None}.
507        @type statuses: C{dict}
508        """
509        presence = AvailabilityPresence(recipient=recipient, sender=sender,
510                                        available=False, statuses=statuses)
511        self.send(presence.toElement())
512
513
514    def subscribe(self, recipient, sender=None):
515        """
516        Send subscription request
517
518        @param recipient: Entity to subscribe to.
519        @type recipient: {JID}
520        """
521        presence = SubscriptionPresence(recipient=recipient, sender=sender)
522        presence.stanzaType = 'subscribe'
523        self.send(presence.toElement())
524
525
526    def unsubscribe(self, recipient, sender=None):
527        """
528        Send unsubscription request
529
530        @param recipient: Entity to unsubscribe from.
531        @type recipient: {JID}
532        """
533        presence = SubscriptionPresence(recipient=recipient, sender=sender)
534        presence.stanzaType = 'unsubscribe'
535        self.send(presence.toElement())
536
537
538    def subscribed(self, recipient, sender=None):
539        """
540        Send subscription confirmation.
541
542        @param recipient: Entity that subscribed.
543        @type recipient: {JID}
544        """
545        presence = SubscriptionPresence(recipient=recipient, sender=sender)
546        presence.stanzaType = 'subscribed'
547        self.send(presence.toElement())
548
549
550    def unsubscribed(self, recipient, sender=None):
551        """
552        Send unsubscription confirmation.
553
554        @param recipient: Entity that unsubscribed.
555        @type recipient: {JID}
556        """
557        presence = SubscriptionPresence(recipient=recipient, sender=sender)
558        presence.stanzaType = 'unsubscribed'
559        self.send(presence.toElement())
560
561
562    def probe(self, recipient, sender=None):
563        """
564        Send presence probe.
565
566        @param recipient: Entity to be probed.
567        @type recipient: {JID}
568        """
569        presence = ProbePresence(recipient=recipient, sender=sender)
570        self.send(presence.toElement())
571
572
573
[2]574class RosterItem(object):
575    """
576    Roster item.
577
578    This represents one contact from an XMPP contact list known as roster.
579
580    @ivar jid: The JID of the contact.
581    @type jid: L{JID}
582    @ivar name: The optional associated nickname for this contact.
583    @type name: C{unicode}
584    @ivar subscriptionTo: Subscription state to contact's presence. If C{True},
585                          the roster owner is subscribed to the presence
586                          information of the contact.
587    @type subscriptionTo: C{bool}
588    @ivar subscriptionFrom: Contact's subscription state. If C{True}, the
589                            contact is subscribed to the presence information
590                            of the roster owner.
591    @type subscriptionTo: C{bool}
592    @ivar ask: Whether subscription is pending.
593    @type ask: C{bool}
594    @ivar groups: Set of groups this contact is categorized in. Groups are
595                  represented by an opaque identifier of type C{unicode}.
596    @type groups: C{set}
597    """
598
599    def __init__(self, jid):
600        self.jid = jid
601        self.name = None
602        self.subscriptionTo = False
603        self.subscriptionFrom = False
604        self.ask = None
605        self.groups = set()
606
607
608class RosterClientProtocol(XMPPHandler):
609    """
610    Client side XMPP roster protocol.
611    """
612
613    def connectionInitialized(self):
614        ROSTER_SET = "/iq[@type='set']/query[@xmlns='%s']" % NS_ROSTER
615        self.xmlstream.addObserver(ROSTER_SET, self._onRosterSet)
616
617    def _parseRosterItem(self, element):
618        jid = JID(element['jid'])
619        item = RosterItem(jid)
620        item.name = element.getAttribute('name')
621        subscription = element.getAttribute('subscription')
622        item.subscriptionTo = subscription in ('to', 'both')
623        item.subscriptionFrom = subscription in ('from', 'both')
624        item.ask = element.getAttribute('ask') == 'subscribe'
625        for subElement in domish.generateElementsQNamed(element.children,
626                                                        'group', NS_ROSTER):
627            item.groups.add(unicode(subElement))
628
629        return item
630
631    def getRoster(self):
632        """
633        Retrieve contact list.
634
635        @return: Roster as a mapping from L{JID} to L{RosterItem}.
636        @rtype: L{twisted.internet.defer.Deferred}
637        """
638
639        def processRoster(result):
640            roster = {}
641            for element in domish.generateElementsQNamed(result.query.children,
[9]642                                                         'item', NS_ROSTER):
[2]643                item = self._parseRosterItem(element)
644                roster[item.jid.userhost()] = item
645
646            return roster
647
648        iq = IQ(self.xmlstream, 'get')
649        iq.addElement((NS_ROSTER, 'query'))
650        d = iq.send()
651        d.addCallback(processRoster)
652        return d
653
[28]654
655    def removeItem(self, entity):
656        """
657        Remove an item from the contact list.
658
659        @param entity: The contact to remove the roster item for.
660        @type entity: L{JID<twisted.words.protocols.jabber.jid.JID>}
661        @rtype: L{twisted.internet.defer.Deferred}
662        """
663        iq = IQ(self.xmlstream, 'set')
664        iq.addElement((NS_ROSTER, 'query'))
665        item = iq.query.addElement('item')
666        item['jid'] = entity.full()
667        item['subscription'] = 'remove'
668        return iq.send()
669
670
[2]671    def _onRosterSet(self, iq):
672        if iq.handled or \
673           iq.hasAttribute('from') and iq['from'] != self.xmlstream:
674            return
675
676        iq.handled = True
677
678        itemElement = iq.query.item
679
680        if unicode(itemElement['subscription']) == 'remove':
681            self.onRosterRemove(JID(itemElement['jid']))
682        else:
683            item = self._parseRosterItem(iq.query.item)
684            self.onRosterSet(item)
685
686    def onRosterSet(self, item):
687        """
688        Called when a roster push for a new or update item was received.
689
690        @param item: The pushed roster item.
691        @type item: L{RosterItem}
692        """
693
694    def onRosterRemove(self, entity):
695        """
696        Called when a roster push for the removal of an item was received.
697
698        @param entity: The entity for which the roster item has been removed.
699        @type entity: L{JID}
700        """
701
702class MessageProtocol(XMPPHandler):
703    """
704    Generic XMPP subprotocol handler for incoming message stanzas.
705    """
706
707    messageTypes = None, 'normal', 'chat', 'headline', 'groupchat'
708
709    def connectionInitialized(self):
710        self.xmlstream.addObserver("/message", self._onMessage)
711
712    def _onMessage(self, message):
713        if message.handled:
714            return
715
716        messageType = message.getAttribute("type")
717
718        if messageType == 'error':
719            return
720
721        if messageType not in self.messageTypes:
722            message["type"] = 'normal'
723
724        self.onMessage(message)
725
726    def onMessage(self, message):
727        """
728        Called when a message stanza was received.
729        """
Note: See TracBrowser for help on using the repository browser.