basexmpp.py 31 KB
Newer Older
1 2 3 4 5 6 7
# slixmpp.basexmpp
# ~~~~~~~~~~~~~~~~~~
# This module provides the common XMPP functionality
# for both clients and components.
# Part of Slixmpp: The Slick XMPP Library
# :copyright: (c) 2011 Nathanael C. Fritz
# :license: MIT, see LICENSE for more details
8
from __future__ import annotations
9

Dan Sully's avatar
Dan Sully committed
10
import asyncio
11
import logging
Nathan Fritz's avatar
Nathan Fritz committed
12

13
from typing import (
14 15 16
    Dict,
    Optional,
    Union,
17 18 19
    TYPE_CHECKING,
)

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

24 25 26 27 28 29
from slixmpp.stanza import (
    Message,
    Presence,
    Iq,
    StreamError,
)
louiz’'s avatar
louiz’ committed
30
from slixmpp.stanza.roster import Roster
Nathan Fritz's avatar
Nathan Fritz committed
31

louiz’'s avatar
louiz’ committed
32 33 34 35
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
36 37 38 39
from slixmpp.xmlstream.stanzabase import (
    ElementBase,
    XML_NS,
)
40

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

43

44 45
log = logging.getLogger(__name__)

46

47 48 49 50
from slixmpp.types import (
    PresenceTypes,
    MessageTypes,
    IqTypes,
mathieui's avatar
mathieui committed
51 52
    JidStr,
    OptJidStr,
53 54
)

55
if TYPE_CHECKING:
56 57
    # Circular imports
    from slixmpp.pluginsdict import PluginsDict
58 59


60 61 62 63 64 65 66
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
67 68
    :param default_ns: Ensure that the correct default XML namespace
                       is used during initialization.
69 70
    """

71 72 73 74
    # This is technically not correct, but much more useful to typecheck
    # than the internal use of the PluginManager API
    plugin: PluginsDict

75 76
    def __init__(self, jid='', default_ns='jabber:client', **kwargs):
        XMLStream.__init__(self, **kwargs)
77 78

        self.default_ns = default_ns
79
        self.stream_ns = 'http://etherx.jabber.org/streams'
Lance Stout's avatar
Lance Stout committed
80
        self.namespace_map[self.stream_ns] = 'stream'
81

Lance Stout's avatar
Lance Stout committed
82 83 84
        #: An identifier for the stream as given by the server.
        self.stream_id = None

85
        #: The JabberID (JID) requested for this connection.
86
        self.requested_jid = JID(jid)
87 88 89 90

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

Lance Stout's avatar
Lance Stout committed
93
        self._expected_server_name = self.boundjid.host
94 95 96 97 98
        self._redirect_attempts = 0

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

Dan Sully's avatar
Dan Sully committed
100
        self.session_bind_event = asyncio.Event()
101

Lance Stout's avatar
Lance Stout committed
102
        #: A dictionary mapping plugin names to plugins.
Lance Stout's avatar
Lance Stout committed
103
        self.plugin = PluginManager(self)
Lance Stout's avatar
Lance Stout committed
104 105 106 107

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

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

Lance Stout's avatar
Lance Stout committed
114 115 116
        #: 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
117
        self.roster = roster.Roster(self)
118
        self.roster.add(self.boundjid)
Lance Stout's avatar
Lance Stout committed
119 120 121 122 123

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

Lance Stout's avatar
Lance Stout committed
126 127 128
        #: The distinction between clients and components can be
        #: important, primarily for choosing how to handle the
        #: ``'to'`` and ``'from'`` JIDs of stanzas.
129
        self.is_component = False
Lance Stout's avatar
Lance Stout committed
130

131 132 133 134
        #: 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.
135
        self.use_message_ids = True
136 137 138 139

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

142 143 144
        #: XEP-0359 <origin-id/> tag that gets added to <message/> stanzas.
        self.use_origin_id = True

145 146 147
        #: 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
148
        #:
149 150 151 152 153 154 155 156 157 158 159 160
        #:   - 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
161 162 163
        #: Flag indicating that the initial presence broadcast has
        #: been sent. Until this happens, some servers may not
        #: behave as expected when sending stanzas.
164 165
        self.sentpresence = False

louiz’'s avatar
louiz’ committed
166
        #: A reference to :mod:`slixmpp.stanza` to make accessing
Lance Stout's avatar
Lance Stout committed
167
        #: stanza classes easier.
168
        self.stanza = stanza
Lance Stout's avatar
Lance Stout committed
169

170 171 172 173 174
        self.register_handler(
            Callback('IM',
                     MatchXPath('{%s}message/{%s}body' % (self.default_ns,
                                                          self.default_ns)),
                     self._handle_message))
mathieui's avatar
mathieui committed
175 176 177 178 179 180 181

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

182 183 184 185
        self.register_handler(
            Callback('Presence',
                     MatchXPath("{%s}presence" % self.default_ns),
                     self._handle_presence))
186

187 188 189 190
        self.register_handler(
            Callback('Stream Error',
                     MatchXPath("{%s}error" % self.stream_ns),
                     self._handle_stream_error))
191

192 193
        self.add_event_handler('session_start',
                               self._handle_session_start)
194 195
        self.add_event_handler('disconnected',
                               self._handle_disconnected)
Lance Stout's avatar
Lance Stout committed
196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217
        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)
218 219

        # Set up the XML stream with XMPP's root stanzas.
Lance Stout's avatar
Lance Stout committed
220 221 222
        self.register_stanza(Message)
        self.register_stanza(Iq)
        self.register_stanza(Presence)
223
        self.register_stanza(StreamError)
224 225

        # Initialize a few default stanza plugins.
Lance Stout's avatar
Lance Stout committed
226
        register_stanza_plugin(Iq, Roster)
227

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

Lance Stout's avatar
Lance Stout committed
231
        :param xml: The incoming stream's root element.
232
        """
233
        self.stream_id = xml.get('id', '')
234 235
        self.stream_version = xml.get('version', '')
        self.peer_default_lang = xml.get('{%s}lang' % XML_NS, None)
236

237 238 239 240
        if not self.is_component and not self.stream_version:
            log.warning('Legacy XMPP 0.9 protocol detected.')
            self.event('legacy_protocol')

241
    def process(self, *, forever=True, timeout=None):
242
        self.init_plugins()
243
        XMLStream.process(self, forever=forever, timeout=timeout)
244

245
    def init_plugins(self):
246
        for name in self.plugin:
Lance Stout's avatar
Lance Stout committed
247 248 249 250
            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
251

252
    def register_plugin(self, plugin: str, pconfig: Optional[Dict] = None, module=None):
Lance Stout's avatar
Lance Stout committed
253
        """Register and configure  a plugin for use in this stream.
254

Lance Stout's avatar
Lance Stout committed
255
        :param plugin: The name of the plugin class. Plugin names must
256
                       be unique.
Lance Stout's avatar
Lance Stout committed
257 258 259
        :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
260 261
                       class if using custom plugins.
        """
Lance Stout's avatar
Lance Stout committed
262 263 264 265 266 267

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

        if not self.plugin.registered(plugin):
268
            load_plugin(plugin, module)
Lance Stout's avatar
Lance Stout committed
269
        self.plugin.enable(plugin, pconfig)
270 271

    def register_plugins(self):
Lance Stout's avatar
Lance Stout committed
272
        """Register and initialize all built-in plugins.
273 274

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

Lance Stout's avatar
Lance Stout committed
277
        Plugin configurations stored in :attr:`plugin_config` will be used.
278 279 280 281 282 283 284 285
        """
        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
286
                self.register_plugin(plugin)
287 288 289 290
            else:
                raise NameError("Plugin %s not in plugins.__all__." % plugin)

    def __getitem__(self, key):
Lance Stout's avatar
Lance Stout committed
291
        """Return a plugin given its name, if it has been registered."""
292 293 294
        if key in self.plugin:
            return self.plugin[key]
        else:
Lance Stout's avatar
Lance Stout committed
295
            log.warning("Plugin '%s' is not loaded.", key)
296 297 298
            return False

    def get(self, key, default):
Lance Stout's avatar
Lance Stout committed
299
        """Return a plugin given its name, if it has been registered."""
300 301
        return self.plugin.get(key, default)

mathieui's avatar
mathieui committed
302
    def Message(self, *args, **kwargs) -> stanza.Message:
303
        """Create a Message stanza associated with this stream."""
304 305 306
        msg = Message(self, *args, **kwargs)
        msg['lang'] = self.default_lang
        return msg
307

mathieui's avatar
mathieui committed
308
    def Iq(self, *args, **kwargs) -> stanza.Iq:
309 310 311
        """Create an Iq stanza associated with this stream."""
        return Iq(self, *args, **kwargs)

mathieui's avatar
mathieui committed
312
    def Presence(self, *args, **kwargs) -> stanza.Presence:
313
        """Create a Presence stanza associated with this stream."""
314 315 316
        pres = Presence(self, *args, **kwargs)
        pres['lang'] = self.default_lang
        return pres
317

mathieui's avatar
mathieui committed
318 319
    def make_iq(self, id: str = "0", ifrom: OptJidStr = None,
                ito: OptJidStr = None, itype: Optional[IqTypes] = None,
mathieui's avatar
mathieui committed
320
                iquery: Optional[str] = None) -> stanza.Iq:
321
        """Create a new :class:`~.Iq` stanza with a given Id and from JID.
Lance Stout's avatar
Lance Stout committed
322 323 324

        :param id: An ideally unique ID value for this stanza thread.
                   Defaults to 0.
325
        :param ifrom: The from :class:`~.JID`
Lance Stout's avatar
Lance Stout committed
326
                      to use for this stanza.
327
        :param ito: The destination :class:`~.JID`
Lance Stout's avatar
Lance Stout committed
328
                    for this stanza.
329
        :param itype: The :class:`~.Iq`'s type,
Lance Stout's avatar
Lance Stout committed
330 331 332
                      one of: ``'get'``, ``'set'``, ``'result'``,
                      or ``'error'``.
        :param iquery: Optional namespace for adding a query element.
333
        """
334 335 336 337 338
        iq = self.Iq()
        iq['id'] = str(id)
        iq['to'] = ito
        iq['from'] = ifrom
        iq['type'] = itype
Lance Stout's avatar
Lance Stout committed
339
        iq['query'] = iquery
340
        return iq
341

342
    def make_iq_get(self, queryxmlns: Optional[str] =None,
mathieui's avatar
mathieui committed
343
                    ito: OptJidStr = None, ifrom: OptJidStr = None,
mathieui's avatar
mathieui committed
344
                    iq: Optional[stanza.Iq] = None) -> stanza.Iq:
345
        """Create an :class:`~.Iq` stanza of type ``'get'``.
346 347 348

        Optionally, a query element may be added.

Lance Stout's avatar
Lance Stout committed
349
        :param queryxmlns: The namespace of the query to use.
350
        :param ito: The destination :class:`~.JID`
Lance Stout's avatar
Lance Stout committed
351
                    for this stanza.
352
        :param ifrom: The ``'from'`` :class:`~.JID`
Lance Stout's avatar
Lance Stout committed
353 354 355
                      to use for this stanza.
        :param iq: Optionally use an existing stanza instead
                   of generating a new one.
356
        """
357 358 359 360 361 362 363 364 365
        if not iq:
            iq = self.Iq()
        iq['type'] = 'get'
        iq['query'] = queryxmlns
        if ito:
            iq['to'] = ito
        if ifrom:
            iq['from'] = ifrom
        return iq
366

367
    def make_iq_result(self, id: Optional[str] = None,
mathieui's avatar
mathieui committed
368
                       ito: OptJidStr = None, ifrom: OptJidStr = None,
mathieui's avatar
mathieui committed
369
                       iq: Optional[stanza.Iq] = None) -> stanza.Iq:
370
        """
371
        Create an :class:`~.Iq` stanza of type
Lance Stout's avatar
Lance Stout committed
372 373 374
        ``'result'`` with the given ID value.

        :param id: An ideally unique ID value. May use :meth:`new_id()`.
375
        :param ito: The destination :class:`~.JID`
Lance Stout's avatar
Lance Stout committed
376
                    for this stanza.
377
        :param ifrom: The ``'from'`` :class:`~.JID`
Lance Stout's avatar
Lance Stout committed
378 379 380
                      to use for this stanza.
        :param iq: Optionally use an existing stanza instead
                   of generating a new one.
381
        """
382 383 384 385 386 387 388 389 390 391 392
        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
393

394
    def make_iq_set(self, sub: Optional[Union[ElementBase, ET.Element]] = None,
mathieui's avatar
mathieui committed
395
                    ito: OptJidStr = None, ifrom: OptJidStr = None,
mathieui's avatar
mathieui committed
396
                    iq: Optional[stanza.Iq] = None) -> stanza.Iq:
397
        """
398
        Create an :class:`~.Iq` stanza of type ``'set'``.
399 400 401 402

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

Lance Stout's avatar
Lance Stout committed
403
        :param sub: Either an
404
                    :class:`~.ElementBase`
Lance Stout's avatar
Lance Stout committed
405
                    stanza object or an
Lance Stout's avatar
Lance Stout committed
406
                    :class:`~xml.etree.ElementTree.Element` XML object
407 408
                    to use as the :class:`~.Iq`'s payload.
        :param ito: The destination :class:`~.JID`
Lance Stout's avatar
Lance Stout committed
409
                    for this stanza.
410
        :param ifrom: The ``'from'`` :class:`~.JID`
Lance Stout's avatar
Lance Stout committed
411 412 413
                      to use for this stanza.
        :param iq: Optionally use an existing stanza instead
                   of generating a new one.
414
        """
415 416 417
        if not iq:
            iq = self.Iq()
        iq['type'] = 'set'
418 419
        if sub != None:
            iq.append(sub)
420 421 422 423
        if ito:
            iq['to'] = ito
        if ifrom:
            iq['from'] = ifrom
424 425 426
        return iq

    def make_iq_error(self, id, type='cancel',
427 428
                      condition='feature-not-implemented',
                      text=None, ito=None, ifrom=None, iq=None):
429
        """
430
        Create an :class:`~.Iq` stanza of type ``'error'``.
Lance Stout's avatar
Lance Stout committed
431 432

        :param id: An ideally unique ID value. May use :meth:`new_id()`.
Lance Stout's avatar
Lance Stout committed
433
        :param type: The type of the error, such as ``'cancel'`` or
Lance Stout's avatar
Lance Stout committed
434
                     ``'modify'``. Defaults to ``'cancel'``.
Lance Stout's avatar
Lance Stout committed
435
        :param condition: The error condition. Defaults to
Lance Stout's avatar
Lance Stout committed
436 437
                          ``'feature-not-implemented'``.
        :param text: A message describing the cause of the error.
438
        :param ito: The destination :class:`~.JID`
Lance Stout's avatar
Lance Stout committed
439
                    for this stanza.
440
        :param ifrom: The ``'from'`` :class:`~jid.JID`
Lance Stout's avatar
Lance Stout committed
441 442 443
                      to use for this stanza.
        :param iq: Optionally use an existing stanza instead
                   of generating a new one.
444
        """
445 446 447 448 449 450 451 452 453 454
        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
455 456
        return iq

mathieui's avatar
mathieui committed
457
    def make_iq_query(self, iq: Optional[stanza.Iq] = None, xmlns: str = '',
mathieui's avatar
mathieui committed
458
                      ito: OptJidStr = None,
mathieui's avatar
mathieui committed
459
                      ifrom: OptJidStr = None) -> stanza.Iq:
460
        """
461
        Create or modify an :class:`~.Iq` stanza
Lance Stout's avatar
Lance Stout committed
462 463 464 465 466
        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.
467
        :param ito: The destination :class:`~.JID`
Lance Stout's avatar
Lance Stout committed
468
                    for this stanza.
469
        :param ifrom: The ``'from'`` :class:`~.JID`
Lance Stout's avatar
Lance Stout committed
470
                      to use for this stanza.
471 472 473 474
        """
        if not iq:
            iq = self.Iq()
        iq['query'] = xmlns
475 476 477 478
        if ito:
            iq['to'] = ito
        if ifrom:
            iq['from'] = ifrom
479 480
        return iq

mathieui's avatar
mathieui committed
481
    def make_query_roster(self, iq: Optional[stanza.Iq] = None) -> ET.Element:
Lance Stout's avatar
Lance Stout committed
482
        """Create a roster query element.
483

Lance Stout's avatar
Lance Stout committed
484 485
        :param iq: Optionally use an existing stanza instead
                   of generating a new one.
486 487 488 489 490
        """
        if iq:
            iq['query'] = 'jabber:iq:roster'
        return ET.Element("{jabber:iq:roster}query")

mathieui's avatar
mathieui committed
491
    def make_message(self, mto: JidStr, mbody: Optional[str] = None,
492 493
                     msubject: Optional[str] = None,
                     mtype: Optional[MessageTypes] = None,
mathieui's avatar
mathieui committed
494
                     mhtml: Optional[str] = None, mfrom: OptJidStr = None,
mathieui's avatar
mathieui committed
495
                     mnick: Optional[str] = None) -> stanza.Message:
496
        """
Lance Stout's avatar
Lance Stout committed
497
        Create and initialize a new
498
        :class:`~.Message` stanza.
Lance Stout's avatar
Lance Stout committed
499 500 501 502 503 504 505 506 507 508 509

        :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.
510 511 512 513 514 515 516 517 518 519
        """
        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

mathieui's avatar
mathieui committed
520
    def make_presence(self, pshow: Optional[str] = None,
521 522
                      pstatus: Optional[str] = None,
                      ppriority: Optional[int] = None,
mathieui's avatar
mathieui committed
523
                      pto: OptJidStr = None,
524
                      ptype: Optional[PresenceTypes] = None,
mathieui's avatar
mathieui committed
525
                      pfrom: OptJidStr = None,
mathieui's avatar
mathieui committed
526
                      pnick: Optional[str] = None) -> stanza.Presence:
527
        """
Lance Stout's avatar
Lance Stout committed
528
        Create and initialize a new
529
        :class:`~.Presence` stanza.
Lance Stout's avatar
Lance Stout committed
530 531 532 533 534 535 536 537

        :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.
538 539 540 541
        """
        presence = self.Presence(stype=ptype, sfrom=pfrom, sto=pto)
        if pshow is not None:
            presence['type'] = pshow
Lance Stout's avatar
Lance Stout committed
542
        if pfrom is None and self.is_component:
louiz’'s avatar
louiz’ committed
543
            presence['from'] = self.boundjid.full
544 545
        presence['priority'] = ppriority
        presence['status'] = pstatus
Lance Stout's avatar
Lance Stout committed
546
        presence['nick'] = pnick
547 548
        return presence

549 550 551
    def send_message(self, mto: JID, mbody: Optional[str] = None,
                     msubject: Optional[str] = None,
                     mtype: Optional[MessageTypes] = None,
mathieui's avatar
mathieui committed
552
                     mhtml: Optional[str] = None, mfrom: OptJidStr = None,
553
                     mnick: Optional[str] = None):
554
        """
Lance Stout's avatar
Lance Stout committed
555
        Create, initialize, and send a new
556
        :class:`~.Message` stanza.
Lance Stout's avatar
Lance Stout committed
557 558 559 560 561 562 563 564 565 566 567

        :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.
568
        """
Lance Stout's avatar
Lance Stout committed
569 570
        self.make_message(mto, mbody, msubject, mtype,
                          mhtml, mfrom, mnick).send()
571

mathieui's avatar
mathieui committed
572
    def send_presence(self, pshow: Optional[str] = None,
573 574
                      pstatus: Optional[str] = None,
                      ppriority: Optional[int] = None,
mathieui's avatar
mathieui committed
575
                      pto: OptJidStr = None,
576
                      ptype: Optional[PresenceTypes] = None,
mathieui's avatar
mathieui committed
577
                      pfrom: OptJidStr = None,
578
                      pnick: Optional[str] = None):
579
        """
Lance Stout's avatar
Lance Stout committed
580
        Create, initialize, and send a new
581
        :class:`~.Presence` stanza.
Lance Stout's avatar
Lance Stout committed
582 583 584 585 586 587 588 589

        :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
590
        """
Lance Stout's avatar
Lance Stout committed
591
        self.make_presence(pshow, pstatus, ppriority, pto,
592
                           ptype, pfrom, pnick).send()
593

mathieui's avatar
mathieui committed
594 595 596
    def send_presence_subscription(self, pto: JidStr, pfrom: OptJidStr = None,
                                   ptype: PresenceTypes='subscribe', pnick:
                                   Optional[str] = None):
597
        """
Lance Stout's avatar
Lance Stout committed
598
        Create, initialize, and send a new
599
        :class:`~.Presence` stanza of
Lance Stout's avatar
Lance Stout committed
600
        type ``'subscribe'``.
601

Lance Stout's avatar
Lance Stout committed
602 603 604 605
        :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.
606
        """
607 608 609 610
        self.make_presence(ptype=ptype,
                           pfrom=pfrom,
                           pto=JID(pto).bare,
                           pnick=pnick).send()
611

612
    @property
mathieui's avatar
mathieui committed
613
    def jid(self) -> str:
Lance Stout's avatar
Lance Stout committed
614
        """Attribute accessor for bare jid"""
615
        log.warning("jid property deprecated. Use boundjid.bare")
616 617 618
        return self.boundjid.bare

    @jid.setter
mathieui's avatar
mathieui committed
619
    def jid(self, value: str):
620
        log.warning("jid property deprecated. Use boundjid.bare")
621 622 623
        self.boundjid.bare = value

    @property
mathieui's avatar
mathieui committed
624
    def fulljid(self) -> str:
Lance Stout's avatar
Lance Stout committed
625
        """Attribute accessor for full jid"""
626
        log.warning("fulljid property deprecated. Use boundjid.full")
627 628 629
        return self.boundjid.full

    @fulljid.setter
mathieui's avatar
mathieui committed
630
    def fulljid(self, value: str):
631
        log.warning("fulljid property deprecated. Use boundjid.full")
632
        self.boundjid.full = value
Lance Stout's avatar
Lance Stout committed
633

634
    @property
mathieui's avatar
mathieui committed
635
    def resource(self) -> str:
Lance Stout's avatar
Lance Stout committed
636
        """Attribute accessor for jid resource"""
637
        log.warning("resource property deprecated. Use boundjid.resource")
638 639 640
        return self.boundjid.resource

    @resource.setter
mathieui's avatar
mathieui committed
641
    def resource(self, value: str):
642
        log.warning("fulljid property deprecated. Use boundjid.resource")
643
        self.boundjid.resource = value
Lance Stout's avatar
Lance Stout committed
644

645
    @property
mathieui's avatar
mathieui committed
646
    def username(self) -> str:
Lance Stout's avatar
Lance Stout committed
647
        """Attribute accessor for jid usernode"""
648
        log.warning("username property deprecated. Use boundjid.user")
649 650 651
        return self.boundjid.user

    @username.setter
mathieui's avatar
mathieui committed
652
    def username(self, value: str):
653
        log.warning("username property deprecated. Use boundjid.user")
654 655 656
        self.boundjid.user = value

    @property
mathieui's avatar
mathieui committed
657
    def server(self) -> str:
Lance Stout's avatar
Lance Stout committed
658
        """Attribute accessor for jid host"""
659
        log.warning("server property deprecated. Use boundjid.host")
660 661 662
        return self.boundjid.server

    @server.setter
mathieui's avatar
mathieui committed
663
    def server(self, value: str):
664
        log.warning("server property deprecated. Use boundjid.host")
665 666
        self.boundjid.server = value

667
    @property
mathieui's avatar
mathieui committed
668
    def auto_authorize(self) -> Optional[bool]:
Lance Stout's avatar
Lance Stout committed
669
        """Auto accept or deny subscription requests.
670

Lance Stout's avatar
Lance Stout committed
671 672 673
        If ``True``, auto accept subscription requests.
        If ``False``, auto deny subscription requests.
        If ``None``, don't automatically respond.
674 675 676 677
        """
        return self.roster.auto_authorize

    @auto_authorize.setter
mathieui's avatar
mathieui committed
678
    def auto_authorize(self, value: Optional[bool]):
679 680 681
        self.roster.auto_authorize = value

    @property
mathieui's avatar
mathieui committed
682
    def auto_subscribe(self) -> bool:
Lance Stout's avatar
Lance Stout committed
683
        """Auto send requests for mutual subscriptions.
684

Lance Stout's avatar
Lance Stout committed
685
        If ``True``, auto send mutual subscription requests.
686 687 688 689
        """
        return self.roster.auto_subscribe

    @auto_subscribe.setter
mathieui's avatar
mathieui committed
690
    def auto_subscribe(self, value: bool):
691 692
        self.roster.auto_subscribe = value

mathieui's avatar
mathieui committed
693
    def set_jid(self, jid: JidStr):
694
        """Rip a JID apart and claim it as our own."""
Lance Stout's avatar
Lance Stout committed
695
        log.debug("setting jid to %s", jid)
696
        self.boundjid = JID(jid)
697

mathieui's avatar
mathieui committed
698
    def getjidresource(self, fulljid: str):
699 700 701 702 703
        if '/' in fulljid:
            return fulljid.split('/', 1)[-1]
        else:
            return ''

mathieui's avatar
mathieui committed
704
    def getjidbare(self, fulljid: str):
705 706
        return fulljid.split('/', 1)[0]

707 708 709 710
    def _handle_session_start(self, event):
        """Reset redirection attempt count."""
        self._redirect_attempts = 0

711 712
    def _handle_disconnected(self, event):
        """When disconnected, reset the roster"""
713
        self.roster.reset()
714
        self.session_bind_event.clear()
715

716 717 718
    def _handle_stream_error(self, error):
        self.event('stream_error', error)

719 720
        if error['condition'] == 'see-other-host':
            other_host = error['see_other_host']
721 722 723 724 725 726 727 728 729
            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
730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747

            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()

748 749
    def _handle_message(self, msg):
        """Process incoming message stanzas."""
750 751
        if not self.is_component and not msg['to'].bare:
            msg['to'] = self.boundjid
752 753
        self.event('message', msg)

mathieui's avatar
mathieui committed
754 755 756 757 758 759
    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)

760 761
    def _handle_available(self, pres):
        self.roster[pres['to']][pres['from']].handle_available(pres)
762

763 764
    def _handle_unavailable(self, pres):
        self.roster[pres['to']][pres['from']].handle_unavailable(pres)
765

766
    def _handle_new_subscription(self, pres):
Lance Stout's avatar
Lance Stout committed
767
        """Attempt to automatically handle subscription requests.
Lance Stout's avatar
Lance Stout committed
768 769

        Subscriptions will be approved if the request is from
Lance Stout's avatar
Lance Stout committed
770 771 772
        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
773 774 775
        subscription handling (except for whitelisted JIDs).

        If a subscription is accepted, a request for a mutual
Lance Stout's avatar
Lance Stout committed
776
        subscription will be sent if :attr:`auto_subscribe` is ``True``.
Lance Stout's avatar
Lance Stout committed
777
        """
778 779
        roster = self.roster[pres['to']]
        item = self.roster[pres['to']][pres['from']]
780 781
        if item['whitelisted']:
            item.authorize()
782 783
            if roster.auto_subscribe:
                item.subscribe()
784 785 786 787 788 789 790
        elif roster.auto_authorize:
            item.authorize()
            if roster.auto_subscribe:
                item.subscribe()
        elif roster.auto_authorize == False:
            item.unauthorize()

791 792 793 794 795 796 797 798 799 800 801 802 803 804
    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
805

806
    def _handle_presence(self, presence):
Lance Stout's avatar
Lance Stout committed
807
        """Process incoming presence stanzas.
808 809 810

        Update the roster with presence information.
        """
811 812 813
        if self.roster[presence['from']].ignore_updates:
            return

814 815 816
        if not self.is_component and not presence['to'].bare:
            presence['to'] = self.boundjid

817 818
        self.event('presence', presence)
        self.event('presence_%s' % presence['type'], presence)
819 820 821 822 823 824 825 826 827 828

        # 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

829
    def exception(self, exception):
Lance Stout's avatar
Lance Stout committed
830
        """Process any uncaught exceptions, notably
louiz’'s avatar
louiz’ committed
831 832
        :class:`~slixmpp.exceptions.IqError` and
        :class:`~slixmpp.exceptions.IqTimeout` exceptions.
833

Lance Stout's avatar
Lance Stout committed
834
        :param exception: An unhandled :class:`Exception` object.
835 836 837
        """
        if isinstance(exception, IqError):
            iq = exception.iq
Lance Stout's avatar
Lance Stout committed
838 839
            log.error('%s: %s', iq['error']['condition'],
                                iq['error']['text'])
840 841 842
            log.warning('You should catch IqError exceptions')
        elif isinstance(exception, IqTimeout):
            iq = exception.iq
Lance Stout's avatar
Lance Stout committed
843
            log.error('Request timed out: %s', iq)
844
            log.warning('You should catch IqTimeout exceptions')
845 846 847 848 849
        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
850 851
        else:
            log.exception(exception)