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
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 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
18from wokkel.compat import IQ
19from wokkel.generic import ErrorStanza, Stanza
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:
48            self.addElement('priority', content=unicode(int(priority)))
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
118
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
201    def unavailable(self, entity=None, statuses=None):
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        """
213        self.send(UnavailablePresence(entity, statuses))
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
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
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,
642                                                         'item', NS_ROSTER):
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
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
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.