Source code for xmpp.models.core

# <xmpp - stateless and concurrency-agnostic XMPP library for python>
#
# Copyright (C) <2016-2017> Gabriel Falcao <gabriel@nacaolivre.org>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.

import re

from xmpp.core import ET
from xmpp.models.node import Node


class JID(object):
    regex = re.compile(r'^(?P<full>(?P<bare>(?P<nick>[^@]+)?@?(?P<domain>[^/]+)?)(/(?P<resource>\S+)?)?)$')

    def __init__(self, data):
        self.data = data
        self.parts = {}

        if isinstance(data, basestring):
            found = self.regex.search(data)
            if found:
                self.parts = found.groupdict()

        elif isinstance(data, JID):
            self.parts = data.parts

        elif isinstance(data, dict):
            self.parts = data

        else:
            raise TypeError('invalid jid: {0}'.format(repr(data)))

    @property
    def full(self):
        return self.parts.get('full', self.text)

    @property
    def bare(self):
        return self.parts.get('bare', self.text)

    @property
    def nick(self):
        return self.parts.get('nick', self.text)

    @property
    def domain(self):
        return self.parts.get('domain', self.text)

    @property
    def resource(self):
        return self.parts.get('resource', self.text)

    @property
    def muc(self):
        return {
            'nick': self.resource,
            'room': self.nick,
            'server': self.domain,
        }

    @property
    def text(self):
        return '{nick}@{domain}/{resource}'.format(**self.parts)

    def __str__(self):
        return self.full

    def __repr__(self):
        return 'JID({0})'.format(self.parts)


class Stream(Node):
    __tag__ = 'stream:stream'
    __etag__ = '{http://etherx.jabber.org/streams}stream'

    __namespaces__ = [
        ('', 'jabber:client'),
        ('stream', 'http://etherx.jabber.org/streams'),
    ]

    def initialize(self):
        self._features = {}

    def to_xml(self):
        xml = super(Stream, self).to_xml()

        END = '</stream:stream>'
        if END in xml:
            xml = xml.replace(END, '')
        else:
            xml = re.sub(r'\s*[/][>]\s*$', '>', xml)

        return xml

    @staticmethod
    def create_client(to, tls=False):
        node = ClientStream.create(to=to, version='1.0')
        if tls:
            node.append(StartTLS.create())

        return node

    def get_features(self):
        children = self.get_children()
        if not children:
            return {}

        features_node = children[0]
        data = {}
        for feature in features_node.get_children():
            name = feature.attr['xmlns']
            value = filter(bool, [x.value for x in feature.get_children()])
            data[name] = value

        return data

    @property
    def features(self):
        if not self._features:
            self._features.update(self.get_features())

        return self._features

    def supports_tls(self):
        return 'urn:ietf:params:xml:ns:xmpp-tls' in self.features

    def accepts_registration(self):
        return 'http://jabber.org/features/iq-register' in self.features

    def sasl_support(self):
        return self.features.get('urn:ietf:params:xml:ns:xmpp-sasl', [])


[docs]class ClientStream(Stream): """``<stream:stream xmlns='jabber:client' version="1.0" xmlns:stream='http://etherx.jabber.org/streams' />``""" __tag__ = 'stream:stream' __etag__ = None __namespaces__ = [ ('', 'jabber:client'), ('stream', 'http://etherx.jabber.org/streams'), ]
[docs]class StreamFeatures(Node): """``<stream:features></stream:features>``""" __tag__ = 'stream:features' __etag__ = '{http://etherx.jabber.org/streams}features' __children_of__ = Stream __namespaces__ = []
class Feature(Node): def get_name(self): return self.tag @property def name(self): return self.get_name()
[docs]class SASLMechanismSet(Feature): """``<mechanisms xmlns="urn:ietf:params:xml:ns:xmpp-sasl"></mechanisms>``""" __tag__ = 'mechanisms' __etag__ = '{urn:ietf:params:xml:ns:xmpp-sasl}mechanisms' __children_of__ = StreamFeatures __namespaces__ = [ ('', 'urn:ietf:params:xml:ns:xmpp-sasl'), ] def supported_mechanisms(self): return [c.value.strip() for c in self.get_children()]
[docs]class SASLMechanism(Node): """``<mechanism></mechanism>``""" __tag__ = 'mechanism' __single__ = True __etag__ = '{urn:ietf:params:xml:ns:xmpp-sasl}mechanism' __children_of__ = SASLMechanismSet __namespaces__ = []
[docs]class IQRegister(Feature): """``<register xmlns="http://jabber.org/features/iq-register" />``""" __tag__ = 'register' __single__ = True __etag__ = '{http://jabber.org/features/iq-register}register' __children_of__ = StreamFeatures __namespaces__ = [ ('', "http://jabber.org/features/iq-register"), ]
[docs]class StartTLS(Node): """``<starttls xmlns="urn:ietf:params:xml:ns:xmpp-tls" />``""" __tag__ = 'starttls' __single__ = True __etag__ = '{urn:ietf:params:xml:ns:xmpp-tls}starttls' __children_of__ = StreamFeatures __namespaces__ = [ ('', 'urn:ietf:params:xml:ns:xmpp-tls'), ]
[docs]class ProceedTLS(Node): """``<proceed xmlns="urn:ietf:params:xml:ns:xmpp-tls" />``""" __tag__ = 'proceed' __etag__ = '{urn:ietf:params:xml:ns:xmpp-tls}proceed' __single__ = True __namespaces__ = [ ('', 'urn:ietf:params:xml:ns:xmpp-tls'), ]
[docs]class Message(Node): """``<message type="chat"></message>``""" __etag__ = 'message' __children_of__ = Stream @property def states(self): return [c.tag for c in self.get_children()] def set_composing(self): self.append(ChatStateComposing.create()) def is_composing(self): return 'composing' in self.states def set_active(self): self.append(ChatStateActive.create()) def is_active(self): return 'active' in self.states def set_paused(self): self.append(ChatStatePaused.create()) def is_paused(self): return 'paused' in self.states def get_body(self): data = [] for item in self.query('body'): data.append(item.value) return u'\n'.join(map(unicode, data)) def add_text(self, text): body = self.get('body') if not body: body = MessageBody.create() self.append(body) body.add_text(text) @classmethod def create(cls, text=None, **kw): if 'type' not in kw: kw['type'] = 'chat' node = super(Message, cls).create(**kw) if text: node.add_text(text) return node
[docs]class Presence(Node): """``<presence></presence>``""" __etag__ = 'presence' __children_of__ = Stream @property def delay(self): delay = self.get('delay') if delay: return delay.attr.get('stamp') @property def show(self): node = self.get('show') if node: return node.value return '' @property def priority(self): node = self.get('priority') if node: return node.value return '' @property def status(self): node = self.get('status') if node: return node.value return ''
[docs]class IQ(Node): """``<iq></iq>``""" __etag__ = 'iq' __children_of__ = Stream
class ResourceBind(Node): __tag__ = 'bind' __etag__ = '{urn:ietf:params:xml:ns:xmpp-bind}bind' __namespaces__ = [ ('', 'urn:ietf:params:xml:ns:xmpp-bind'), ] __children_of__ = StreamFeatures @staticmethod def with_resource(resource): node = ResourceBind.create() if resource: echild = ET.Element('resource', {}) echild.text = resource.strip() node._element.append(echild) return node class BoundJid(Node): __tag__ = 'jid' __etag__ = '{urn:ietf:params:xml:ns:xmpp-bind}jid' __namespaces__ = [] __children_of__ = ResourceBind class BindRequired(Node): __tag__ = 'required' __etag__ = '{urn:ietf:params:xml:ns:xmpp-bind}required' __single__ = True __namespaces__ = [] __children_of__ = ResourceBind class BindOptional(Node): __tag__ = 'optional' __etag__ = '{urn:ietf:params:xml:ns:xmpp-bind}optional' __single__ = True __namespaces__ = [] __children_of__ = ResourceBind class Session(Node): __tag__ = 'session' __etag__ = '{urn:ietf:params:xml:ns:xmpp-session}session' __namespaces__ = [ ('', 'urn:ietf:params:xml:ns:xmpp-session'), ] __children_of__ = StreamFeatures class SessionRequired(Node): __tag__ = 'required' __etag__ = '{urn:ietf:params:xml:ns:xmpp-session}required' __single__ = True __namespaces__ = [] __children_of__ = Session class SessionOptional(Node): __tag__ = 'optional' __etag__ = '{urn:ietf:params:xml:ns:xmpp-session}optional' __single__ = True __namespaces__ = [] __children_of__ = Session class RosterVersioning(Node): __tag__ = 'ver' __etag__ = '{urn:xmpp:features:rosterver}ver' __single__ = True __namespaces__ = [ ('', 'urn:xmpp:features:rosterver'), ] __children_of__ = StreamFeatures class SASLAuth(Node): __tag__ = 'auth' __etag__ = '{urn:ietf:params:xml:ns:xmpp-sasl}auth' __namespaces__ = [ ('', 'urn:ietf:params:xml:ns:xmpp-sasl'), ] @staticmethod def prepare(mechanism, message): node = SASLAuth.create(mechanism=mechanism) node.value = bytes(message) return node class SASLFailure(Node): __tag__ = 'failure' __etag__ = '{urn:ietf:params:xml:ns:xmpp-sasl}failure' __namespaces__ = [ ('', 'urn:ietf:params:xml:ns:xmpp-sasl'), ] class SASLText(Node): __tag__ = 'text' __etag__ = '{urn:ietf:params:xml:ns:xmpp-sasl}text' __namespaces__ = [ ] class SASLMalformedRequest(Node): __tag__ = 'malformed-request' __etag__ = '{urn:ietf:params:xml:ns:xmpp-sasl}malformed-request' __namespaces__ = [ ] class SASLChallenge(Node): __tag__ = 'challenge' __etag__ = '{urn:ietf:params:xml:ns:xmpp-sasl}challenge' __namespaces__ = [ ('', 'urn:ietf:params:xml:ns:xmpp-sasl'), ] def get_data(self): return self.value.decode('base64') @property def decoded(self): return self.get_data() class SASLAborted(Node): __tag__ = 'aborted' __etag__ = '{urn:ietf:params:xml:ns:xmpp-sasl}aborted' __namespaces__ = [ ('', 'urn:ietf:params:xml:ns:xmpp-sasl'), ] class SASLIncorrectEncoding(Node): __tag__ = 'incorrect-encoding' __etag__ = '{urn:ietf:params:xml:ns:xmpp-sasl}incorrect-encoding' __namespaces__ = [ ('', 'urn:ietf:params:xml:ns:xmpp-sasl'), ] class SASLInvalidAuthzid(Node): __tag__ = 'invalid-authzid' __etag__ = '{urn:ietf:params:xml:ns:xmpp-sasl}invalid-authzid' __namespaces__ = [ ('', 'urn:ietf:params:xml:ns:xmpp-sasl'), ] class SASLInvalidMechanism(Node): __tag__ = 'invalid-mechanism' __etag__ = '{urn:ietf:params:xml:ns:xmpp-sasl}invalid-mechanism' __namespaces__ = [ ('', 'urn:ietf:params:xml:ns:xmpp-sasl'), ] class SASLMechanismTooWeak(Node): __tag__ = 'mechanism-too-weak' __etag__ = '{urn:ietf:params:xml:ns:xmpp-sasl}mechanism-too-weak' __namespaces__ = [ ('', 'urn:ietf:params:xml:ns:xmpp-sasl'), ] class SASLNotAuthorized(Node): __tag__ = 'not-authorized' __etag__ = '{urn:ietf:params:xml:ns:xmpp-sasl}not-authorized' __namespaces__ = [ ('', 'urn:ietf:params:xml:ns:xmpp-sasl'), ] class SASLTemporaryAuthFailure(Node): __tag__ = 'temporary-auth-failure' __etag__ = '{urn:ietf:params:xml:ns:xmpp-sasl}temporary-auth-failure' __namespaces__ = [ ('', 'urn:ietf:params:xml:ns:xmpp-sasl'), ] class SASLSuccess(Node): __tag__ = 'success' __etag__ = '{urn:ietf:params:xml:ns:xmpp-sasl}success' __namespaces__ = [ ('', 'urn:ietf:params:xml:ns:xmpp-sasl'), ('stream', 'http://etherx.jabber.org/streams'), ] def get_data(self): return self.value.decode('base64') @property def decoded(self): return self.get_data() class SASLResponse(Node): __tag__ = 'response' __etag__ = '{urn:ietf:params:xml:ns:xmpp-sasl}response' __namespaces__ = [ ('', 'urn:ietf:params:xml:ns:xmpp-sasl'), ] @staticmethod def prepare(mechanism, message): node = SASLResponse.create(mechanism=mechanism) node.value = message return node class EntityCapability(Node): __tag__ = 'c' __etag__ = '{http://jabber.org/protocol/caps}c' __single__ = True __namespaces__ = [ ('', 'http://jabber.org/protocol/caps'), ] class VCardUpdate(Node): __tag__ = 'x' __etag__ = '{vcard-temp:x:update}x' __namespaces__ = [ ('', 'vcard-temp:x:update') ] class ChatStateComposing(Node): __tag__ = 'composing' __etag__ = '{http://jabber.org/protocol/chatstates}composing' __single__ = True __namespaces__ = [ ('', 'http://jabber.org/protocol/chatstates') ] class ChatStateActive(Node): __tag__ = 'active' __etag__ = '{http://jabber.org/protocol/chatstates}active' __single__ = True __namespaces__ = [ ('', 'http://jabber.org/protocol/chatstates') ] class ChatStatePaused(Node): __tag__ = 'paused' __etag__ = '{http://jabber.org/protocol/chatstates}paused' __single__ = True __namespaces__ = [ ('', 'http://jabber.org/protocol/chatstates') ] class MessageBody(Node): __etag__ = 'body' __children_of__ = Message class PresenceDelay(Node): __tag__ = 'delay' __etag__ = '{urn:xmpp:delay}delay' __single__ = True __namespaces__ = [ ('', 'urn:xmpp:delay') ] class PresencePriority(Node): __etag__ = 'priority' __children_of__ = Presence class MessageDelay(Node): __tag__ = 'delay' __etag__ = '{jabber:x:delay}delay' __single__ = True __namespaces__ = [ ('', 'jabber:x:delay') ] __children_of__ = Message class VCardPhoto(Node): __tag__ = 'photo' __etag__ = '{vcard-temp:x:update}photo' __single__ = True __namespaces__ = [ ('', 'vcard-temp:x:update') ] class Error(Node): __etag__ = 'error' class Text(Node): __tag__ = 'text' __etag__ = '{urn:ietf:params:xml:ns:xmpp-stanzas}text' __namespaces__ = [ ('', 'urn:ietf:params:xml:ns:xmpp-stanzas') ] class StreamError(Error): __tag__ = 'stream:error' __etag__ = 'error' __children_of__ = Stream # stream errors class BadFormat(Error): __tag__ = 'bad-format' __etag__ = '{urn:ietf:params:xml:ns:xmpp-streams}bad-format' __namespaces__ = [ ('', 'urn:ietf:params:xml:ns:xmpp-streams') ] __children_of__ = StreamError class BadNamespace(Error): __tag__ = 'bad-namespace' __etag__ = '{urn:ietf:params:xml:ns:xmpp-streams}bad-namespace' __namespaces__ = [ ('', 'urn:ietf:params:xml:ns:xmpp-streams') ] __children_of__ = StreamError class InvalidNamespace(Error): __tag__ = 'invalid-namespace' __etag__ = '{urn:ietf:params:xml:ns:xmpp-streams}invalid-namespace' __namespaces__ = [ ('', 'urn:ietf:params:xml:ns:xmpp-streams') ] __children_of__ = StreamError class Conflict(Error): __tag__ = 'conflict' __etag__ = '{urn:ietf:params:xml:ns:xmpp-streams}conflict' __namespaces__ = [ ('', 'urn:ietf:params:xml:ns:xmpp-streams') ] __children_of__ = StreamError class ConnectionTimeout(Error): __tag__ = 'connection-timeout' __etag__ = '{urn:ietf:params:xml:ns:xmpp-streams}connection-timeout' __namespaces__ = [ ('', 'urn:ietf:params:xml:ns:xmpp-streams') ] __children_of__ = StreamError class HostGone(Error): __tag__ = 'host-gone' __etag__ = '{urn:ietf:params:xml:ns:xmpp-streams}host-gone' __namespaces__ = [ ('', 'urn:ietf:params:xml:ns:xmpp-streams') ] __children_of__ = StreamError class NotAuthorized(Error): __tag__ = 'not-authorized' __etag__ = '{urn:ietf:params:xml:ns:xmpp-streams}not-authorized' __namespaces__ = [ ('', 'urn:ietf:params:xml:ns:xmpp-streams') ] __children_of__ = StreamError class UnsupportedStanzaType(Error): __tag__ = 'unsupported-stanza-type' __etag__ = '{urn:ietf:params:xml:ns:xmpp-streams}unsupported-stanza-type' __namespaces__ = [ ('', 'urn:ietf:params:xml:ns:xmpp-streams') ] __children_of__ = StreamError # stanza errors class ServiceUnavailable(Error): __tag__ = 'service-unavailable' __etag__ = '{urn:ietf:params:xml:ns:xmpp-stanzas}service-unavailable' __namespaces__ = [ ('', 'urn:ietf:params:xml:ns:xmpp-stanzas') ] class BadRequest(Error): __tag__ = 'bad-request' __etag__ = '{urn:ietf:params:xml:ns:xmpp-stanzas}bad-request' __namespaces__ = [ ('', 'urn:ietf:params:xml:ns:xmpp-stanzas') ] class VCard(Node): __tag__ = 'vCard' __etag__ = '{vcard-temp}vCard' __namespaces__ = [ ('', 'vcard-temp') ] __children_of__ = IQ class PresenceStatus(Node): __etag__ = 'status' __children_of__ = Presence class PresenceShow(Node): __etag__ = 'show' __children_of__ = Presence class RosterQuery(Node): __tag__ = 'query' __etag__ = '{jabber:iq:roster}query' __namespaces__ = [ ('', 'jabber:iq:roster') ] __children_of__ = IQ class RosterItem(Node): __etag__ = 'item' __children_of__ = RosterQuery class RosterGroup(Node): __tag__ = 'group' __etag__ = 'query' __namespaces__ = [ ('', 'jabber:iq:roster') ] __children_of__ = RosterItem
[docs]class MissingJID(Exception): """raised when trying to send a stanza but it is missing either the "to" or "from" fields """