source: wokkel/xmppim.py @ 141:3a9bff4e7807

wokkel-muc-client-support-24
Last change on this file since 141:3a9bff4e7807 was 141:3a9bff4e7807, checked in by Ralph Meijer <ralphm@…>, 9 years ago

Use descendants from wokkel.generic.Stanza in messages and presence.

  • Use wokkel.xmppim.AvailabilityPresence as a basis for presence stanzas.
  • Split wokkel.xmppim.PresenceProtocol and Base MUCClient on BasePresenceProtocol
  • Add new wokkel.xmppim.Message as the basis for message stanzas.
  • `receivedGroupChat
  • Change order of parameters for invite, as reason is optional.
  • Use datetimes everywhere where timestamps are needed.
  • Property exe set to *
File size: 23.4 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 __get_status(self):
300        if None in self.statuses:
301            return self.statuses[None]
302        elif self.statuses:
303            for status in self.status.itervalues():
304                return status
305        else:
306            return None
307
308    status = property(__get_status)
309
310
311    def _childParser_show(self, element):
312        show = unicode(element)
313        if show in ('chat', 'away', 'xa', 'dnd'):
314            self.show = show
315
316
317    def _childParser_status(self, element):
318        lang = element.getAttribute((NS_XML, 'lang'), None)
319        text = unicode(element)
320        self.statuses[lang] = text
321
322
323    def _childParser_priority(self, element):
324        try:
325            self.priority = int(unicode(element))
326        except ValueError:
327            pass
328
329
330    def parseElement(self, element):
331        BasePresence.parseElement(self, element)
332
333        if self.stanzaType == 'unavailable':
334            self.available = False
335
336
337    def toElement(self):
338        if not self.available:
339            self.stanzaType = 'unavailable'
340
341        presence = BasePresence.toElement(self)
342
343        if self.available:
344            if self.show in ('chat', 'away', 'xa', 'dnd'):
345                presence.addElement('show', content=self.show)
346            if self.priority != 0:
347                presence.addElement('priority', content=unicode(self.priority))
348
349        for lang, text in self.statuses.iteritems():
350            status = presence.addElement('status', content=text)
351            if lang:
352                status[(NS_XML, 'lang')] = lang
353
354        return presence
355
356
357
358class SubscriptionPresence(BasePresence):
359    """
360    Presence subscription request or response.
361
362    This kind of presence is used to represent requests for presence
363    subscription and their replies.
364
365    Based on L{BasePresence} and {Stanza}, it just uses the L{stanzaType}
366    attribute to represent the type of subscription presence. This can be
367    one of C{'subscribe'}, C{'unsubscribe'}, C{'subscribed'} and
368    C{'unsubscribed'}.
369    """
370
371
372
373class ProbePresence(BasePresence):
374    """
375    Presence probe request.
376    """
377
378    stanzaType = 'probe'
379
380
381
382class BasePresenceProtocol(XMPPHandler):
383    """
384    XMPP Presence base protocol handler.
385
386    This class is the base for protocol handlers that receive presence
387    stanzas. Listening to all incoming presence stanzas, it extracts the
388    stanza's type and looks up a matching stanza parser and calls the
389    associated method. The method's name is the type + C{Received}. E.g.
390    C{availableReceived}. See L{PresenceProtocol} for a complete example.
391
392    @cvar presenceTypeParserMap: Maps presence stanza types to their respective
393        stanza parser classes (derived from L{Stanza}).
394    @type presenceTypeParserMap: C{dict}
395    """
396
397    presenceTypeParserMap = {}
398
399    def connectionInitialized(self):
400        self.xmlstream.addObserver("/presence", self._onPresence)
401
402
403
404    def _onPresence(self, element):
405        """
406        Called when a presence stanza has been received.
407        """
408        stanza = Stanza.fromElement(element)
409
410        presenceType = stanza.stanzaType or 'available'
411
412        try:
413            parser = self.presenceTypeParserMap[presenceType]
414        except KeyError:
415            return
416
417        presence = parser.fromElement(element)
418
419        try:
420            handler = getattr(self, '%sReceived' % presenceType)
421        except AttributeError:
422            return
423        else:
424            handler(presence)
425
426
427
428class PresenceProtocol(BasePresenceProtocol):
429
430    presenceTypeParserMap = {
431                'error': ErrorStanza,
432                'available': AvailabilityPresence,
433                'unavailable': AvailabilityPresence,
434                'subscribe': SubscriptionPresence,
435                'unsubscribe': SubscriptionPresence,
436                'subscribed': SubscriptionPresence,
437                'unsubscribed': SubscriptionPresence,
438                'probe': ProbePresence,
439                }
440
441
442    def errorReceived(self, presence):
443        """
444        Error presence was received.
445        """
446        pass
447
448
449    def availableReceived(self, presence):
450        """
451        Available presence was received.
452        """
453        pass
454
455
456    def unavailableReceived(self, presence):
457        """
458        Unavailable presence was received.
459        """
460        pass
461
462
463    def subscribedReceived(self, presence):
464        """
465        Subscription approval confirmation was received.
466        """
467        pass
468
469
470    def unsubscribedReceived(self, presence):
471        """
472        Unsubscription confirmation was received.
473        """
474        pass
475
476
477    def subscribeReceived(self, presence):
478        """
479        Subscription request was received.
480        """
481        pass
482
483
484    def unsubscribeReceived(self, presence):
485        """
486        Unsubscription request was received.
487        """
488        pass
489
490
491    def probeReceived(self, presence):
492        """
493        Probe presence was received.
494        """
495        pass
496
497
498    def available(self, recipient=None, show=None, statuses=None, priority=0,
499                        status=None, sender=None):
500        """
501        Send available presence.
502
503        @param recipient: Optional Recipient to which the presence should be
504            sent.
505        @type recipient: {JID}
506
507        @param show: Optional detailed presence information. One of C{'away'},
508            C{'xa'}, C{'chat'}, C{'dnd'}.
509        @type show: C{str}
510
511        @param statuses: Mapping of natural language descriptions of the
512           availability status, keyed by the language descriptor. A status
513           without a language specified, is keyed with C{None}.
514        @type statuses: C{dict}
515
516        @param priority: priority level of the resource.
517        @type priority: C{int}
518        """
519        presence = AvailabilityPresence(recipient=recipient, sender=sender,
520                                        show=show, statuses=statuses,
521                                        status=status, priority=priority)
522        self.send(presence.toElement())
523
524
525    def unavailable(self, recipient=None, statuses=None, sender=None):
526        """
527        Send unavailable presence.
528
529        @param recipient: Optional entity to which the presence should be sent.
530        @type recipient: {JID}
531
532        @param statuses: dictionary of natural language descriptions of the
533            availability status, keyed by the language descriptor. A status
534            without a language specified, is keyed with C{None}.
535        @type statuses: C{dict}
536        """
537        presence = AvailabilityPresence(recipient=recipient, sender=sender,
538                                        available=False, statuses=statuses)
539        self.send(presence.toElement())
540
541
542    def subscribe(self, recipient, sender=None):
543        """
544        Send subscription request
545
546        @param recipient: Entity to subscribe to.
547        @type recipient: {JID}
548        """
549        presence = SubscriptionPresence(recipient=recipient, sender=sender)
550        presence.stanzaType = 'subscribe'
551        self.send(presence.toElement())
552
553
554    def unsubscribe(self, recipient, sender=None):
555        """
556        Send unsubscription request
557
558        @param recipient: Entity to unsubscribe from.
559        @type recipient: {JID}
560        """
561        presence = SubscriptionPresence(recipient=recipient, sender=sender)
562        presence.stanzaType = 'unsubscribe'
563        self.send(presence.toElement())
564
565
566    def subscribed(self, recipient, sender=None):
567        """
568        Send subscription confirmation.
569
570        @param recipient: Entity that subscribed.
571        @type recipient: {JID}
572        """
573        presence = SubscriptionPresence(recipient=recipient, sender=sender)
574        presence.stanzaType = 'subscribed'
575        self.send(presence.toElement())
576
577
578    def unsubscribed(self, recipient, sender=None):
579        """
580        Send unsubscription confirmation.
581
582        @param recipient: Entity that unsubscribed.
583        @type recipient: {JID}
584        """
585        presence = SubscriptionPresence(recipient=recipient, sender=sender)
586        presence.stanzaType = 'unsubscribed'
587        self.send(presence.toElement())
588
589
590    def probe(self, recipient, sender=None):
591        """
592        Send presence probe.
593
594        @param recipient: Entity to be probed.
595        @type recipient: {JID}
596        """
597        presence = ProbePresence(recipient=recipient, sender=sender)
598        self.send(presence.toElement())
599
600
601
602class RosterItem(object):
603    """
604    Roster item.
605
606    This represents one contact from an XMPP contact list known as roster.
607
608    @ivar jid: The JID of the contact.
609    @type jid: L{JID}
610    @ivar name: The optional associated nickname for this contact.
611    @type name: C{unicode}
612    @ivar subscriptionTo: Subscription state to contact's presence. If C{True},
613                          the roster owner is subscribed to the presence
614                          information of the contact.
615    @type subscriptionTo: C{bool}
616    @ivar subscriptionFrom: Contact's subscription state. If C{True}, the
617                            contact is subscribed to the presence information
618                            of the roster owner.
619    @type subscriptionTo: C{bool}
620    @ivar ask: Whether subscription is pending.
621    @type ask: C{bool}
622    @ivar groups: Set of groups this contact is categorized in. Groups are
623                  represented by an opaque identifier of type C{unicode}.
624    @type groups: C{set}
625    """
626
627    def __init__(self, jid):
628        self.jid = jid
629        self.name = None
630        self.subscriptionTo = False
631        self.subscriptionFrom = False
632        self.ask = None
633        self.groups = set()
634
635
636class RosterClientProtocol(XMPPHandler):
637    """
638    Client side XMPP roster protocol.
639    """
640
641    def connectionInitialized(self):
642        ROSTER_SET = "/iq[@type='set']/query[@xmlns='%s']" % NS_ROSTER
643        self.xmlstream.addObserver(ROSTER_SET, self._onRosterSet)
644
645    def _parseRosterItem(self, element):
646        jid = JID(element['jid'])
647        item = RosterItem(jid)
648        item.name = element.getAttribute('name')
649        subscription = element.getAttribute('subscription')
650        item.subscriptionTo = subscription in ('to', 'both')
651        item.subscriptionFrom = subscription in ('from', 'both')
652        item.ask = element.getAttribute('ask') == 'subscribe'
653        for subElement in domish.generateElementsQNamed(element.children,
654                                                        'group', NS_ROSTER):
655            item.groups.add(unicode(subElement))
656
657        return item
658
659    def getRoster(self):
660        """
661        Retrieve contact list.
662
663        @return: Roster as a mapping from L{JID} to L{RosterItem}.
664        @rtype: L{twisted.internet.defer.Deferred}
665        """
666
667        def processRoster(result):
668            roster = {}
669            for element in domish.generateElementsQNamed(result.query.children,
670                                                         'item', NS_ROSTER):
671                item = self._parseRosterItem(element)
672                roster[item.jid.userhost()] = item
673
674            return roster
675
676        iq = IQ(self.xmlstream, 'get')
677        iq.addElement((NS_ROSTER, 'query'))
678        d = iq.send()
679        d.addCallback(processRoster)
680        return d
681
682
683    def removeItem(self, entity):
684        """
685        Remove an item from the contact list.
686
687        @param entity: The contact to remove the roster item for.
688        @type entity: L{JID<twisted.words.protocols.jabber.jid.JID>}
689        @rtype: L{twisted.internet.defer.Deferred}
690        """
691        iq = IQ(self.xmlstream, 'set')
692        iq.addElement((NS_ROSTER, 'query'))
693        item = iq.query.addElement('item')
694        item['jid'] = entity.full()
695        item['subscription'] = 'remove'
696        return iq.send()
697
698
699    def _onRosterSet(self, iq):
700        if iq.handled or \
701           iq.hasAttribute('from') and iq['from'] != self.xmlstream:
702            return
703
704        iq.handled = True
705
706        itemElement = iq.query.item
707
708        if unicode(itemElement['subscription']) == 'remove':
709            self.onRosterRemove(JID(itemElement['jid']))
710        else:
711            item = self._parseRosterItem(iq.query.item)
712            self.onRosterSet(item)
713
714    def onRosterSet(self, item):
715        """
716        Called when a roster push for a new or update item was received.
717
718        @param item: The pushed roster item.
719        @type item: L{RosterItem}
720        """
721
722    def onRosterRemove(self, entity):
723        """
724        Called when a roster push for the removal of an item was received.
725
726        @param entity: The entity for which the roster item has been removed.
727        @type entity: L{JID}
728        """
729
730
731
732class Message(Stanza):
733    """
734    A message stanza.
735    """
736
737    stanzaKind = 'message'
738
739    childParsers = {
740            (None, 'body'): '_childParser_body',
741            (None, 'subject'): '_childParser_subject',
742            }
743
744    def __init__(self, recipient=None, sender=None, body=None, subject=None):
745        Stanza.__init__(self, recipient, sender)
746        self.body = body
747        self.subject = subject
748
749
750    def _childParser_body(self, element):
751        self.body = unicode(element)
752
753
754    def _childParser_subject(self, element):
755        self.subject = unicode(element)
756
757
758    def toElement(self):
759        element = Stanza.toElement(self)
760
761        if self.body:
762            element.addElement('body', content=self.body)
763        if self.subject:
764            element.addElement('subject', content=self.subject)
765
766        return element
767
768
769
770class MessageProtocol(XMPPHandler):
771    """
772    Generic XMPP subprotocol handler for incoming message stanzas.
773    """
774
775    messageTypes = None, 'normal', 'chat', 'headline', 'groupchat'
776
777    def connectionInitialized(self):
778        self.xmlstream.addObserver("/message", self._onMessage)
779
780    def _onMessage(self, message):
781        if message.handled:
782            return
783
784        messageType = message.getAttribute("type")
785
786        if messageType == 'error':
787            return
788
789        if messageType not in self.messageTypes:
790            message["type"] = 'normal'
791
792        self.onMessage(message)
793
794    def onMessage(self, message):
795        """
796        Called when a message stanza was received.
797        """
Note: See TracBrowser for help on using the repository browser.