basexmpp.py 29.3 KB
Newer Older
Lance Stout's avatar
Lance Stout committed
1
# -*- coding: utf-8 -*-
Nathan Fritz's avatar
Nathan Fritz committed
2
"""
louiz’'s avatar
louiz’ committed
3
    slixmpp.basexmpp
Lance Stout's avatar
Lance Stout committed
4
    ~~~~~~~~~~~~~~~~~~
Nathan Fritz's avatar
Nathan Fritz committed
5

Lance Stout's avatar
Lance Stout committed
6 7 8
    This module provides the common XMPP functionality
    for both clients and components.

louiz’'s avatar
louiz’ committed
9
    Part of Slixmpp: The Slick XMPP Library
Lance Stout's avatar
Lance Stout committed
10 11 12

    :copyright: (c) 2011 Nathanael C. Fritz
    :license: MIT, see LICENSE for more details
Nathan Fritz's avatar
Nathan Fritz committed
13
"""
14 15

import logging
16
import threading
Nathan Fritz's avatar
Nathan Fritz committed
17

louiz’'s avatar
louiz’ committed
18 19 20
from slixmpp import plugins, roster, stanza
from slixmpp.api import APIRegistry
from slixmpp.exceptions import IqError, IqTimeout
Nathan Fritz's avatar
Nathan Fritz committed
21

louiz’'s avatar
louiz’ committed
22 23 24 25
from slixmpp.stanza import Message, Presence, Iq, StreamError
from slixmpp.stanza.roster import Roster
from slixmpp.stanza.nick import Nick
from slixmpp.stanza.htmlim import HTMLIM
Nathan Fritz's avatar
Nathan Fritz committed
26

louiz’'s avatar
louiz’ committed
27 28 29 30 31
from slixmpp.xmlstream import XMLStream, JID
from slixmpp.xmlstream import ET, register_stanza_plugin
from slixmpp.xmlstream.matcher import MatchXPath
from slixmpp.xmlstream.handler import Callback
from slixmpp.xmlstream.stanzabase import XML_NS
32

louiz’'s avatar
louiz’ committed
33
from slixmpp.plugins import PluginManager, load_plugin
Lance Stout's avatar
Lance Stout committed
34

35

36 37
log = logging.getLogger(__name__)

38 39 40 41 42 43 44
class BaseXMPP(XMLStream):

    """
    The BaseXMPP class adapts the generic XMLStream class for use
    with XMPP. It also provides a plugin mechanism to easily extend
    and add support for new XMPP features.

Lance Stout's avatar
Lance Stout committed
45 46
    :param default_ns: Ensure that the correct default XML namespace
                       is used during initialization.
47 48
    """

49
    def __init__(self, jid='', default_ns='jabber:client'):
50 51 52
        XMLStream.__init__(self)

        self.default_ns = default_ns
53
        self.stream_ns = 'http://etherx.jabber.org/streams'
Lance Stout's avatar
Lance Stout committed
54
        self.namespace_map[self.stream_ns] = 'stream'
55

Lance Stout's avatar
Lance Stout committed
56 57 58
        #: An identifier for the stream as given by the server.
        self.stream_id = None

59
        #: The JabberID (JID) requested for this connection.
60
        self.requested_jid = JID(jid, cache_lock=True)
61 62 63 64

        #: The JabberID (JID) used by this connection,
        #: as set after session binding. This may even be a
        #: different bare JID than what was requested.
65
        self.boundjid = JID(jid, cache_lock=True)
66

Lance Stout's avatar
Lance Stout committed
67
        self._expected_server_name = self.boundjid.host
68 69 70 71 72
        self._redirect_attempts = 0

        #: The maximum number of consecutive see-other-host
        #: redirections that will be followed before quitting.
        self.max_redirects = 5
73

74 75
        self.session_bind_event = threading.Event()

Lance Stout's avatar
Lance Stout committed
76
        #: A dictionary mapping plugin names to plugins.
Lance Stout's avatar
Lance Stout committed
77
        self.plugin = PluginManager(self)
Lance Stout's avatar
Lance Stout committed
78 79 80 81

        #: Configuration options for whitelisted plugins.
        #: If a plugin is registered without any configuration,
        #: and there is an entry here, it will be used.
82
        self.plugin_config = {}
Lance Stout's avatar
Lance Stout committed
83 84 85

        #: A list of plugins that will be loaded if
        #: :meth:`register_plugins` is called.
86
        self.plugin_whitelist = []
87

Lance Stout's avatar
Lance Stout committed
88 89 90
        #: The main roster object. This roster supports multiple
        #: owner JIDs, as in the case for components. For clients
        #: which only have a single JID, see :attr:`client_roster`.
Lance Stout's avatar
Lance Stout committed
91
        self.roster = roster.Roster(self)
92
        self.roster.add(self.boundjid)
Lance Stout's avatar
Lance Stout committed
93 94 95 96 97

        #: The single roster for the bound JID. This is the
        #: equivalent of::
        #:
        #:     self.roster[self.boundjid.bare]
98
        self.client_roster = self.roster[self.boundjid]
99

Lance Stout's avatar
Lance Stout committed
100 101 102
        #: The distinction between clients and components can be
        #: important, primarily for choosing how to handle the
        #: ``'to'`` and ``'from'`` JIDs of stanzas.
103
        self.is_component = False
Lance Stout's avatar
Lance Stout committed
104

105 106 107 108 109 110 111 112 113 114 115
        #: Messages may optionally be tagged with ID values. Setting
        #: :attr:`use_message_ids` to `True` will assign all outgoing
        #: messages an ID. Some plugin features require enabling
        #: this option.
        self.use_message_ids = False

        #: Presence updates may optionally be tagged with ID values.
        #: Setting :attr:`use_message_ids` to `True` will assign all
        #: outgoing messages an ID.
        self.use_presence_ids = False

116 117 118
        #: The API registry is a way to process callbacks based on
        #: JID+node combinations. Each callback in the registry is
        #: marked with:
Lance Stout's avatar
Lance Stout committed
119
        #:
120 121 122 123 124 125 126 127 128 129 130 131
        #:   - An API name, e.g. xep_0030
        #:   - The name of an action, e.g. get_info
        #:   - The JID that will be affected
        #:   - The node that will be affected
        #:
        #: API handlers with no JID or node will act as global handlers,
        #: while those with a JID and no node will service all nodes
        #: for a JID, and handlers with both a JID and node will be
        #: used only for that specific combination. The handler that
        #: provides the most specificity will be used.
        self.api = APIRegistry(self)

Lance Stout's avatar
Lance Stout committed
132 133 134
        #: Flag indicating that the initial presence broadcast has
        #: been sent. Until this happens, some servers may not
        #: behave as expected when sending stanzas.
135 136
        self.sentpresence = False

louiz’'s avatar
louiz’ committed
137
        #: A reference to :mod:`slixmpp.stanza` to make accessing
Lance Stout's avatar
Lance Stout committed
138
        #: stanza classes easier.
139
        self.stanza = stanza
Lance Stout's avatar
Lance Stout committed
140

141 142 143 144 145
        self.register_handler(
            Callback('IM',
                     MatchXPath('{%s}message/{%s}body' % (self.default_ns,
                                                          self.default_ns)),
                     self._handle_message))
mathieui's avatar
mathieui committed
146 147 148 149 150 151 152

        self.register_handler(
            Callback('IMError',
                     MatchXPath('{%s}message/{%s}error' % (self.default_ns,
                                                           self.default_ns)),
                     self._handle_message_error))

153 154 155 156
        self.register_handler(
            Callback('Presence',
                     MatchXPath("{%s}presence" % self.default_ns),
                     self._handle_presence))
157

158 159 160 161
        self.register_handler(
            Callback('Stream Error',
                     MatchXPath("{%s}error" % self.stream_ns),
                     self._handle_stream_error))
162

163 164
        self.add_event_handler('session_start',
                               self._handle_session_start)
165 166
        self.add_event_handler('disconnected',
                               self._handle_disconnected)
Lance Stout's avatar
Lance Stout committed
167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188
        self.add_event_handler('presence_available',
                               self._handle_available)
        self.add_event_handler('presence_dnd',
                               self._handle_available)
        self.add_event_handler('presence_xa',
                               self._handle_available)
        self.add_event_handler('presence_chat',
                               self._handle_available)
        self.add_event_handler('presence_away',
                               self._handle_available)
        self.add_event_handler('presence_unavailable',
                               self._handle_unavailable)
        self.add_event_handler('presence_subscribe',
                               self._handle_subscribe)
        self.add_event_handler('presence_subscribed',
                               self._handle_subscribed)
        self.add_event_handler('presence_unsubscribe',
                               self._handle_unsubscribe)
        self.add_event_handler('presence_unsubscribed',
                               self._handle_unsubscribed)
        self.add_event_handler('roster_subscription_request',
                               self._handle_new_subscription)
189 190

        # Set up the XML stream with XMPP's root stanzas.
Lance Stout's avatar
Lance Stout committed
191 192 193
        self.register_stanza(Message)
        self.register_stanza(Iq)
        self.register_stanza(Presence)
194
        self.register_stanza(StreamError)
195 196

        # Initialize a few default stanza plugins.
Lance Stout's avatar
Lance Stout committed
197 198
        register_stanza_plugin(Iq, Roster)
        register_stanza_plugin(Message, Nick)
199

200
    def start_stream_handler(self, xml):
Lance Stout's avatar
Lance Stout committed
201
        """Save the stream ID once the streams have been established.
202

Lance Stout's avatar
Lance Stout committed
203
        :param xml: The incoming stream's root element.
204
        """
205
        self.stream_id = xml.get('id', '')
206 207
        self.stream_version = xml.get('version', '')
        self.peer_default_lang = xml.get('{%s}lang' % XML_NS, None)
208

209 210 211 212
        if not self.is_component and not self.stream_version:
            log.warning('Legacy XMPP 0.9 protocol detected.')
            self.event('legacy_protocol')

213
    def process(self, *, forever=True, timeout=None):
214
        self.init_plugins()
215
        XMLStream.process(self, forever=forever, timeout=timeout)
216

217
    def init_plugins(self):
218
        for name in self.plugin:
Lance Stout's avatar
Lance Stout committed
219 220 221 222
            if not hasattr(self.plugin[name], 'post_inited'):
                if hasattr(self.plugin[name], 'post_init'):
                    self.plugin[name].post_init()
                self.plugin[name].post_inited = True
223 224

    def register_plugin(self, plugin, pconfig={}, module=None):
Lance Stout's avatar
Lance Stout committed
225
        """Register and configure  a plugin for use in this stream.
226

Lance Stout's avatar
Lance Stout committed
227
        :param plugin: The name of the plugin class. Plugin names must
228
                       be unique.
Lance Stout's avatar
Lance Stout committed
229 230 231
        :param pconfig: A dictionary of configuration data for the plugin.
                        Defaults to an empty dictionary.
        :param module: Optional refence to the module containing the plugin
232 233
                       class if using custom plugins.
        """
Lance Stout's avatar
Lance Stout committed
234 235 236 237 238 239

        # Use the global plugin config cache, if applicable
        if not pconfig:
            pconfig = self.plugin_config.get(plugin, {})

        if not self.plugin.registered(plugin):
240
            load_plugin(plugin, module)
Lance Stout's avatar
Lance Stout committed
241
        self.plugin.enable(plugin, pconfig)
242 243

    def register_plugins(self):
Lance Stout's avatar
Lance Stout committed
244
        """Register and initialize all built-in plugins.
245 246

        Optionally, the list of plugins loaded may be limited to those
Lance Stout's avatar
Lance Stout committed
247
        contained in :attr:`plugin_whitelist`.
248

Lance Stout's avatar
Lance Stout committed
249
        Plugin configurations stored in :attr:`plugin_config` will be used.
250 251 252 253 254 255 256 257
        """
        if self.plugin_whitelist:
            plugin_list = self.plugin_whitelist
        else:
            plugin_list = plugins.__all__

        for plugin in plugin_list:
            if plugin in plugins.__all__:
Lance Stout's avatar
Lance Stout committed
258
                self.register_plugin(plugin)
259 260 261 262
            else:
                raise NameError("Plugin %s not in plugins.__all__." % plugin)

    def __getitem__(self, key):
Lance Stout's avatar
Lance Stout committed
263
        """Return a plugin given its name, if it has been registered."""
264 265 266
        if key in self.plugin:
            return self.plugin[key]
        else:
Lance Stout's avatar
Lance Stout committed
267
            log.warning("Plugin '%s' is not loaded.", key)
268 269 270
            return False

    def get(self, key, default):
Lance Stout's avatar
Lance Stout committed
271
        """Return a plugin given its name, if it has been registered."""
272 273 274 275
        return self.plugin.get(key, default)

    def Message(self, *args, **kwargs):
        """Create a Message stanza associated with this stream."""
276 277 278
        msg = Message(self, *args, **kwargs)
        msg['lang'] = self.default_lang
        return msg
279 280 281 282 283 284 285

    def Iq(self, *args, **kwargs):
        """Create an Iq stanza associated with this stream."""
        return Iq(self, *args, **kwargs)

    def Presence(self, *args, **kwargs):
        """Create a Presence stanza associated with this stream."""
286 287 288
        pres = Presence(self, *args, **kwargs)
        pres['lang'] = self.default_lang
        return pres
289

Lance Stout's avatar
Lance Stout committed
290
    def make_iq(self, id=0, ifrom=None, ito=None, itype=None, iquery=None):
Lance Stout's avatar
Lance Stout committed
291 292 293 294
        """Create a new Iq stanza with a given Id and from JID.

        :param id: An ideally unique ID value for this stanza thread.
                   Defaults to 0.
louiz’'s avatar
louiz’ committed
295
        :param ifrom: The from :class:`~slixmpp.xmlstream.jid.JID`
Lance Stout's avatar
Lance Stout committed
296
                      to use for this stanza.
louiz’'s avatar
louiz’ committed
297
        :param ito: The destination :class:`~slixmpp.xmlstream.jid.JID`
Lance Stout's avatar
Lance Stout committed
298
                    for this stanza.
louiz’'s avatar
louiz’ committed
299
        :param itype: The :class:`~slixmpp.stanza.iq.Iq`'s type,
Lance Stout's avatar
Lance Stout committed
300 301 302
                      one of: ``'get'``, ``'set'``, ``'result'``,
                      or ``'error'``.
        :param iquery: Optional namespace for adding a query element.
303
        """
304 305 306 307 308
        iq = self.Iq()
        iq['id'] = str(id)
        iq['to'] = ito
        iq['from'] = ifrom
        iq['type'] = itype
Lance Stout's avatar
Lance Stout committed
309
        iq['query'] = iquery
310
        return iq
311

312
    def make_iq_get(self, queryxmlns=None, ito=None, ifrom=None, iq=None):
louiz’'s avatar
louiz’ committed
313
        """Create an :class:`~slixmpp.stanza.iq.Iq` stanza of type ``'get'``.
314 315 316

        Optionally, a query element may be added.

Lance Stout's avatar
Lance Stout committed
317
        :param queryxmlns: The namespace of the query to use.
louiz’'s avatar
louiz’ committed
318
        :param ito: The destination :class:`~slixmpp.xmlstream.jid.JID`
Lance Stout's avatar
Lance Stout committed
319
                    for this stanza.
louiz’'s avatar
louiz’ committed
320
        :param ifrom: The ``'from'`` :class:`~slixmpp.xmlstream.jid.JID`
Lance Stout's avatar
Lance Stout committed
321 322 323
                      to use for this stanza.
        :param iq: Optionally use an existing stanza instead
                   of generating a new one.
324
        """
325 326 327 328 329 330 331 332 333
        if not iq:
            iq = self.Iq()
        iq['type'] = 'get'
        iq['query'] = queryxmlns
        if ito:
            iq['to'] = ito
        if ifrom:
            iq['from'] = ifrom
        return iq
334

335
    def make_iq_result(self, id=None, ito=None, ifrom=None, iq=None):
336
        """
louiz’'s avatar
louiz’ committed
337
        Create an :class:`~slixmpp.stanza.iq.Iq` stanza of type
Lance Stout's avatar
Lance Stout committed
338 339 340
        ``'result'`` with the given ID value.

        :param id: An ideally unique ID value. May use :meth:`new_id()`.
louiz’'s avatar
louiz’ committed
341
        :param ito: The destination :class:`~slixmpp.xmlstream.jid.JID`
Lance Stout's avatar
Lance Stout committed
342
                    for this stanza.
louiz’'s avatar
louiz’ committed
343
        :param ifrom: The ``'from'`` :class:`~slixmpp.xmlstream.jid.JID`
Lance Stout's avatar
Lance Stout committed
344 345 346
                      to use for this stanza.
        :param iq: Optionally use an existing stanza instead
                   of generating a new one.
347
        """
348 349 350 351 352 353 354 355 356 357 358
        if not iq:
            iq = self.Iq()
            if id is None:
                id = self.new_id()
            iq['id'] = id
        iq['type'] = 'result'
        if ito:
            iq['to'] = ito
        if ifrom:
            iq['from'] = ifrom
        return iq
359

360
    def make_iq_set(self, sub=None, ito=None, ifrom=None, iq=None):
361
        """
louiz’'s avatar
louiz’ committed
362
        Create an :class:`~slixmpp.stanza.iq.Iq` stanza of type ``'set'``.
363 364 365 366

        Optionally, a substanza may be given to use as the
        stanza's payload.

Lance Stout's avatar
Lance Stout committed
367
        :param sub: Either an
louiz’'s avatar
louiz’ committed
368
                    :class:`~slixmpp.xmlstream.stanzabase.ElementBase`
Lance Stout's avatar
Lance Stout committed
369
                    stanza object or an
Lance Stout's avatar
Lance Stout committed
370
                    :class:`~xml.etree.ElementTree.Element` XML object
louiz’'s avatar
louiz’ committed
371 372
                    to use as the :class:`~slixmpp.stanza.iq.Iq`'s payload.
        :param ito: The destination :class:`~slixmpp.xmlstream.jid.JID`
Lance Stout's avatar
Lance Stout committed
373
                    for this stanza.
louiz’'s avatar
louiz’ committed
374
        :param ifrom: The ``'from'`` :class:`~slixmpp.xmlstream.jid.JID`
Lance Stout's avatar
Lance Stout committed
375 376 377
                      to use for this stanza.
        :param iq: Optionally use an existing stanza instead
                   of generating a new one.
378
        """
379 380 381
        if not iq:
            iq = self.Iq()
        iq['type'] = 'set'
382 383
        if sub != None:
            iq.append(sub)
384 385 386 387
        if ito:
            iq['to'] = ito
        if ifrom:
            iq['from'] = ifrom
388 389 390
        return iq

    def make_iq_error(self, id, type='cancel',
391 392
                      condition='feature-not-implemented',
                      text=None, ito=None, ifrom=None, iq=None):
393
        """
louiz’'s avatar
louiz’ committed
394
        Create an :class:`~slixmpp.stanza.iq.Iq` stanza of type ``'error'``.
Lance Stout's avatar
Lance Stout committed
395 396

        :param id: An ideally unique ID value. May use :meth:`new_id()`.
Lance Stout's avatar
Lance Stout committed
397
        :param type: The type of the error, such as ``'cancel'`` or
Lance Stout's avatar
Lance Stout committed
398
                     ``'modify'``. Defaults to ``'cancel'``.
Lance Stout's avatar
Lance Stout committed
399
        :param condition: The error condition. Defaults to
Lance Stout's avatar
Lance Stout committed
400 401
                          ``'feature-not-implemented'``.
        :param text: A message describing the cause of the error.
louiz’'s avatar
louiz’ committed
402
        :param ito: The destination :class:`~slixmpp.xmlstream.jid.JID`
Lance Stout's avatar
Lance Stout committed
403
                    for this stanza.
louiz’'s avatar
louiz’ committed
404
        :param ifrom: The ``'from'`` :class:`~slixmpp.xmlstream.jid.JID`
Lance Stout's avatar
Lance Stout committed
405 406 407
                      to use for this stanza.
        :param iq: Optionally use an existing stanza instead
                   of generating a new one.
408
        """
409 410 411 412 413 414 415 416 417 418
        if not iq:
            iq = self.Iq()
        iq['id'] = id
        iq['error']['type'] = type
        iq['error']['condition'] = condition
        iq['error']['text'] = text
        if ito:
            iq['to'] = ito
        if ifrom:
            iq['from'] = ifrom
419 420
        return iq

421
    def make_iq_query(self, iq=None, xmlns='', ito=None, ifrom=None):
422
        """
louiz’'s avatar
louiz’ committed
423
        Create or modify an :class:`~slixmpp.stanza.iq.Iq` stanza
Lance Stout's avatar
Lance Stout committed
424 425 426 427 428
        to use the given query namespace.

        :param iq: Optionally use an existing stanza instead
                   of generating a new one.
        :param xmlns: The query's namespace.
louiz’'s avatar
louiz’ committed
429
        :param ito: The destination :class:`~slixmpp.xmlstream.jid.JID`
Lance Stout's avatar
Lance Stout committed
430
                    for this stanza.
louiz’'s avatar
louiz’ committed
431
        :param ifrom: The ``'from'`` :class:`~slixmpp.xmlstream.jid.JID`
Lance Stout's avatar
Lance Stout committed
432
                      to use for this stanza.
433 434 435 436
        """
        if not iq:
            iq = self.Iq()
        iq['query'] = xmlns
437 438 439 440
        if ito:
            iq['to'] = ito
        if ifrom:
            iq['from'] = ifrom
441 442 443
        return iq

    def make_query_roster(self, iq=None):
Lance Stout's avatar
Lance Stout committed
444
        """Create a roster query element.
445

Lance Stout's avatar
Lance Stout committed
446 447
        :param iq: Optionally use an existing stanza instead
                   of generating a new one.
448 449 450 451 452 453 454 455
        """
        if iq:
            iq['query'] = 'jabber:iq:roster'
        return ET.Element("{jabber:iq:roster}query")

    def make_message(self, mto, mbody=None, msubject=None, mtype=None,
                     mhtml=None, mfrom=None, mnick=None):
        """
Lance Stout's avatar
Lance Stout committed
456
        Create and initialize a new
louiz’'s avatar
louiz’ committed
457
        :class:`~slixmpp.stanza.message.Message` stanza.
Lance Stout's avatar
Lance Stout committed
458 459 460 461 462 463 464 465 466 467 468

        :param mto: The recipient of the message.
        :param mbody: The main contents of the message.
        :param msubject: Optional subject for the message.
        :param mtype: The message's type, such as ``'chat'`` or
                      ``'groupchat'``.
        :param mhtml: Optional HTML body content in the form of a string.
        :param mfrom: The sender of the message. if sending from a client,
                      be aware that some servers require that the full JID
                      of the sender be used.
        :param mnick: Optional nickname of the sender.
469 470 471 472 473 474 475 476 477 478 479
        """
        message = self.Message(sto=mto, stype=mtype, sfrom=mfrom)
        message['body'] = mbody
        message['subject'] = msubject
        if mnick is not None:
            message['nick'] = mnick
        if mhtml is not None:
            message['html']['body'] = mhtml
        return message

    def make_presence(self, pshow=None, pstatus=None, ppriority=None,
Lance Stout's avatar
Lance Stout committed
480
                      pto=None, ptype=None, pfrom=None, pnick=None):
481
        """
Lance Stout's avatar
Lance Stout committed
482
        Create and initialize a new
louiz’'s avatar
louiz’ committed
483
        :class:`~slixmpp.stanza.presence.Presence` stanza.
Lance Stout's avatar
Lance Stout committed
484 485 486 487 488 489 490 491

        :param pshow: The presence's show value.
        :param pstatus: The presence's status message.
        :param ppriority: This connection's priority.
        :param pto: The recipient of a directed presence.
        :param ptype: The type of presence, such as ``'subscribe'``.
        :param pfrom: The sender of the presence.
        :param pnick: Optional nickname of the presence's sender.
492 493 494 495
        """
        presence = self.Presence(stype=ptype, sfrom=pfrom, sto=pto)
        if pshow is not None:
            presence['type'] = pshow
Lance Stout's avatar
Lance Stout committed
496
        if pfrom is None and self.is_component:
louiz’'s avatar
louiz’ committed
497
            presence['from'] = self.boundjid.full
498 499
        presence['priority'] = ppriority
        presence['status'] = pstatus
Lance Stout's avatar
Lance Stout committed
500
        presence['nick'] = pnick
501 502 503 504 505
        return presence

    def send_message(self, mto, mbody, msubject=None, mtype=None,
                     mhtml=None, mfrom=None, mnick=None):
        """
Lance Stout's avatar
Lance Stout committed
506
        Create, initialize, and send a new
louiz’'s avatar
louiz’ committed
507
        :class:`~slixmpp.stanza.message.Message` stanza.
Lance Stout's avatar
Lance Stout committed
508 509 510 511 512 513 514 515 516 517 518

        :param mto: The recipient of the message.
        :param mbody: The main contents of the message.
        :param msubject: Optional subject for the message.
        :param mtype: The message's type, such as ``'chat'`` or
                      ``'groupchat'``.
        :param mhtml: Optional HTML body content in the form of a string.
        :param mfrom: The sender of the message. if sending from a client,
                      be aware that some servers require that the full JID
                      of the sender be used.
        :param mnick: Optional nickname of the sender.
519
        """
Lance Stout's avatar
Lance Stout committed
520 521
        self.make_message(mto, mbody, msubject, mtype,
                          mhtml, mfrom, mnick).send()
522 523

    def send_presence(self, pshow=None, pstatus=None, ppriority=None,
Lance Stout's avatar
Lance Stout committed
524
                      pto=None, pfrom=None, ptype=None, pnick=None):
525
        """
Lance Stout's avatar
Lance Stout committed
526
        Create, initialize, and send a new
louiz’'s avatar
louiz’ committed
527
        :class:`~slixmpp.stanza.presence.Presence` stanza.
Lance Stout's avatar
Lance Stout committed
528 529 530 531 532 533 534 535

        :param pshow: The presence's show value.
        :param pstatus: The presence's status message.
        :param ppriority: This connection's priority.
        :param pto: The recipient of a directed presence.
        :param ptype: The type of presence, such as ``'subscribe'``.
        :param pfrom: The sender of the presence.
        :param pnick: Optional nickname of the presence's sender.
Lance Stout's avatar
Lance Stout committed
536
        """
Lance Stout's avatar
Lance Stout committed
537
        self.make_presence(pshow, pstatus, ppriority, pto,
538
                           ptype, pfrom, pnick).send()
539 540 541 542

    def send_presence_subscription(self, pto, pfrom=None,
                                   ptype='subscribe', pnick=None):
        """
Lance Stout's avatar
Lance Stout committed
543
        Create, initialize, and send a new
louiz’'s avatar
louiz’ committed
544
        :class:`~slixmpp.stanza.presence.Presence` stanza of
Lance Stout's avatar
Lance Stout committed
545
        type ``'subscribe'``.
546

Lance Stout's avatar
Lance Stout committed
547 548 549 550
        :param pto: The recipient of a directed presence.
        :param pfrom: The sender of the presence.
        :param ptype: The type of presence, such as ``'subscribe'``.
        :param pnick: Optional nickname of the presence's sender.
551
        """
552 553 554 555
        self.make_presence(ptype=ptype,
                           pfrom=pfrom,
                           pto=JID(pto).bare,
                           pnick=pnick).send()
556

557 558
    @property
    def jid(self):
Lance Stout's avatar
Lance Stout committed
559
        """Attribute accessor for bare jid"""
560
        log.warning("jid property deprecated. Use boundjid.bare")
561 562 563 564
        return self.boundjid.bare

    @jid.setter
    def jid(self, value):
565
        log.warning("jid property deprecated. Use boundjid.bare")
566 567 568 569
        self.boundjid.bare = value

    @property
    def fulljid(self):
Lance Stout's avatar
Lance Stout committed
570
        """Attribute accessor for full jid"""
571
        log.warning("fulljid property deprecated. Use boundjid.full")
572 573 574 575
        return self.boundjid.full

    @fulljid.setter
    def fulljid(self, value):
576
        log.warning("fulljid property deprecated. Use boundjid.full")
577
        self.boundjid.full = value
Lance Stout's avatar
Lance Stout committed
578

579 580
    @property
    def resource(self):
Lance Stout's avatar
Lance Stout committed
581
        """Attribute accessor for jid resource"""
582
        log.warning("resource property deprecated. Use boundjid.resource")
583 584 585 586
        return self.boundjid.resource

    @resource.setter
    def resource(self, value):
587
        log.warning("fulljid property deprecated. Use boundjid.resource")
588
        self.boundjid.resource = value
Lance Stout's avatar
Lance Stout committed
589

590 591
    @property
    def username(self):
Lance Stout's avatar
Lance Stout committed
592
        """Attribute accessor for jid usernode"""
593
        log.warning("username property deprecated. Use boundjid.user")
594 595 596 597
        return self.boundjid.user

    @username.setter
    def username(self, value):
598
        log.warning("username property deprecated. Use boundjid.user")
599 600 601 602
        self.boundjid.user = value

    @property
    def server(self):
Lance Stout's avatar
Lance Stout committed
603
        """Attribute accessor for jid host"""
604
        log.warning("server property deprecated. Use boundjid.host")
605 606 607 608
        return self.boundjid.server

    @server.setter
    def server(self, value):
609
        log.warning("server property deprecated. Use boundjid.host")
610 611
        self.boundjid.server = value

612 613
    @property
    def auto_authorize(self):
Lance Stout's avatar
Lance Stout committed
614
        """Auto accept or deny subscription requests.
615

Lance Stout's avatar
Lance Stout committed
616 617 618
        If ``True``, auto accept subscription requests.
        If ``False``, auto deny subscription requests.
        If ``None``, don't automatically respond.
619 620 621 622 623 624 625 626 627
        """
        return self.roster.auto_authorize

    @auto_authorize.setter
    def auto_authorize(self, value):
        self.roster.auto_authorize = value

    @property
    def auto_subscribe(self):
Lance Stout's avatar
Lance Stout committed
628
        """Auto send requests for mutual subscriptions.
629

Lance Stout's avatar
Lance Stout committed
630
        If ``True``, auto send mutual subscription requests.
631 632 633 634 635 636 637
        """
        return self.roster.auto_subscribe

    @auto_subscribe.setter
    def auto_subscribe(self, value):
        self.roster.auto_subscribe = value

638 639
    def set_jid(self, jid):
        """Rip a JID apart and claim it as our own."""
Lance Stout's avatar
Lance Stout committed
640
        log.debug("setting jid to %s", jid)
641
        self.boundjid = JID(jid, cache_lock=True)
642 643 644 645 646 647 648 649 650 651

    def getjidresource(self, fulljid):
        if '/' in fulljid:
            return fulljid.split('/', 1)[-1]
        else:
            return ''

    def getjidbare(self, fulljid):
        return fulljid.split('/', 1)[0]

652 653 654 655
    def _handle_session_start(self, event):
        """Reset redirection attempt count."""
        self._redirect_attempts = 0

656 657
    def _handle_disconnected(self, event):
        """When disconnected, reset the roster"""
658
        self.roster.reset()
659
        self.session_bind_event.clear()
660

661 662 663
    def _handle_stream_error(self, error):
        self.event('stream_error', error)

664 665
        if error['condition'] == 'see-other-host':
            other_host = error['see_other_host']
666 667 668 669 670 671 672 673 674
            if not other_host:
                log.warning("No other host specified.")
                return

            if self._redirect_attempts > self.max_redirects:
                log.error("Exceeded maximum number of redirection attempts.")
                return

            self._redirect_attempts += 1
675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693

            host = other_host
            port = 5222

            if '[' in other_host and ']' in other_host:
                host = other_host.split(']')[0][1:]
            elif ':' in other_host:
                host = other_host.split(':')[0]

            port_sec = other_host.split(']')[-1]
            if ':' in port_sec:
                port = int(port_sec.split(':')[1])

            self.address = (host, port)
            self.default_domain = host
            self.dns_records = None
            self.reconnect_delay = None
            self.reconnect()

694 695
    def _handle_message(self, msg):
        """Process incoming message stanzas."""
696 697
        if not self.is_component and not msg['to'].bare:
            msg['to'] = self.boundjid
698 699
        self.event('message', msg)

mathieui's avatar
mathieui committed
700 701 702 703 704 705
    def _handle_message_error(self, msg):
        """Process incoming message error stanzas."""
        if not self.is_component and not msg['to'].bare:
            msg['to'] = self.boundjid
        self.event('message_error', msg)

706 707
    def _handle_available(self, pres):
        self.roster[pres['to']][pres['from']].handle_available(pres)
708

709 710
    def _handle_unavailable(self, pres):
        self.roster[pres['to']][pres['from']].handle_unavailable(pres)
711

712
    def _handle_new_subscription(self, pres):
Lance Stout's avatar
Lance Stout committed
713
        """Attempt to automatically handle subscription requests.
Lance Stout's avatar
Lance Stout committed
714 715

        Subscriptions will be approved if the request is from
Lance Stout's avatar
Lance Stout committed
716 717 718
        a whitelisted JID, of :attr:`auto_authorize` is True. They
        will be rejected if :attr:`auto_authorize` is False. Setting
        :attr:`auto_authorize` to ``None`` will disable automatic
Lance Stout's avatar
Lance Stout committed
719 720 721
        subscription handling (except for whitelisted JIDs).

        If a subscription is accepted, a request for a mutual
Lance Stout's avatar
Lance Stout committed
722
        subscription will be sent if :attr:`auto_subscribe` is ``True``.
Lance Stout's avatar
Lance Stout committed
723
        """
724 725
        roster = self.roster[pres['to']]
        item = self.roster[pres['to']][pres['from']]
726 727
        if item['whitelisted']:
            item.authorize()
728 729
            if roster.auto_subscribe:
                item.subscribe()
730 731 732 733 734 735 736
        elif roster.auto_authorize:
            item.authorize()
            if roster.auto_subscribe:
                item.subscribe()
        elif roster.auto_authorize == False:
            item.unauthorize()

737 738 739 740 741 742 743 744 745 746 747 748 749 750
    def _handle_removed_subscription(self, pres):
        self.roster[pres['to']][pres['from']].handle_unauthorize(pres)

    def _handle_subscribe(self, pres):
        self.roster[pres['to']][pres['from']].handle_subscribe(pres)

    def _handle_subscribed(self, pres):
        self.roster[pres['to']][pres['from']].handle_subscribed(pres)

    def _handle_unsubscribe(self, pres):
        self.roster[pres['to']][pres['from']].handle_unsubscribe(pres)

    def _handle_unsubscribed(self, pres):
        self.roster[pres['to']][pres['from']].handle_unsubscribed(pres)
Lance Stout's avatar
Lance Stout committed
751

752
    def _handle_presence(self, presence):
Lance Stout's avatar
Lance Stout committed
753
        """Process incoming presence stanzas.
754 755 756

        Update the roster with presence information.
        """
757 758 759
        if not self.is_component and not presence['to'].bare:
            presence['to'] = self.boundjid

760 761
        self.event('presence', presence)
        self.event('presence_%s' % presence['type'], presence)
762 763 764 765 766 767 768 769 770 771

        # Check for changes in subscription state.
        if presence['type'] in ('subscribe', 'subscribed',
                                'unsubscribe', 'unsubscribed'):
            self.event('changed_subscription', presence)
            return
        elif not presence['type'] in ('available', 'unavailable') and \
             not presence['type'] in presence.showtypes:
            return

772
    def exception(self, exception):
Lance Stout's avatar
Lance Stout committed
773
        """Process any uncaught exceptions, notably
louiz’'s avatar
louiz’ committed
774 775
        :class:`~slixmpp.exceptions.IqError` and
        :class:`~slixmpp.exceptions.IqTimeout` exceptions.
776

Lance Stout's avatar
Lance Stout committed
777
        :param exception: An unhandled :class:`Exception` object.
778 779 780
        """
        if isinstance(exception, IqError):
            iq = exception.iq
Lance Stout's avatar
Lance Stout committed
781 782
            log.error('%s: %s', iq['error']['condition'],
                                iq['error']['text'])
783 784 785
            log.warning('You should catch IqError exceptions')
        elif isinstance(exception, IqTimeout):
            iq = exception.iq
Lance Stout's avatar
Lance Stout committed
786
            log.error('Request timed out: %s', iq)
787
            log.warning('You should catch IqTimeout exceptions')
788 789 790 791 792
        elif isinstance(exception, SyntaxError):
            # Hide stream parsing errors that occur when the
            # stream is disconnected (they've been handled, we
            # don't need to make a mess in the logs).
            pass
793 794
        else:
            log.exception(exception)