Fix XMPP plugin.

This honors sleekxmpp's asynchronous nature by hooking into the
"session_start" event callback. While being at it, some other
things have been addressed.

- Also hook into the "failed_auth" event callback in order
  to report any authentication failures back to the user.

- Adjust sleekxmpp's logger to use the logging handlers
  and log level of apprise. That way, "-vvvvv" will yield
  loads of useful information for debugging.

- Disable presence signalling and roster inquiry.
  Rationale: I believe both XMPP features resonate more
  with human users than bots. Omitting them tremendously
  speeds up processing.

- xmpp.connect(): Use the "use_ssl=True" argument when the
  effective TCP port equals NotifyXMPP.default_secure_port.

- xmpp.connect(): Use the "reattempt=False" argument to
  mitigate nasty delay before even connecting to XMPP server.

Resolve #213.
pull/217/head
Andreas Motl 2020-03-19 02:19:23 +01:00
parent 44905e5c6e
commit 3c89b0e4c7
3 changed files with 182 additions and 69 deletions

1
.gitignore vendored
View File

@ -12,6 +12,7 @@ __pycache__/
# Distribution / packaging
.Python
env/
.venv*
build/
develop-eggs/
dist/

View File

@ -25,6 +25,7 @@
import re
import ssl
import logging
from os.path import isfile
from .NotifyBase import NotifyBase
@ -267,34 +268,7 @@ class NotifyXMPP(NotifyBase):
jid = self.host
password = self.password if self.password else self.user
# Prepare our object
xmpp = sleekxmpp.ClientXMPP(jid, password)
for xep in self.xep:
# Load xep entries
xmpp.register_plugin('xep_{0:04d}'.format(xep))
if self.secure:
xmpp.ssl_version = ssl.PROTOCOL_TLSv1
# If the python version supports it, use highest TLS version
# automatically
if hasattr(ssl, "PROTOCOL_TLS"):
# Use the best version of TLS available to us
xmpp.ssl_version = ssl.PROTOCOL_TLS
xmpp.ca_certs = None
if self.verify_certificate:
# Set the ca_certs variable for certificate verification
xmpp.ca_certs = next(
(cert for cert in CA_CERTIFICATE_FILE_LOCATIONS
if isfile(cert)), None)
if xmpp.ca_certs is None:
self.logger.warning(
'XMPP Secure comunication can not be verified; '
'no CA certificate found')
# Acquire our port number
# Compute port number
if not self.port:
port = self.default_secure_port \
if self.secure else self.default_unsecure_port
@ -302,48 +276,23 @@ class NotifyXMPP(NotifyBase):
else:
port = self.port
# Establish our connection
if not xmpp.connect((self.host, port)):
return False
xmpp.send_presence()
try:
xmpp.get_roster()
except sleekxmpp.exceptions.IqError as e:
self.logger.warning('There was an error getting the XMPP roster.')
self.logger.debug(e.iq['error']['condition'])
xmpp.disconnect()
return False
except sleekxmpp.exceptions.IqTimeout:
self.logger.warning('XMPP Server is taking too long to respond.')
xmpp.disconnect()
return False
targets = list(self.targets)
if not targets:
# We always default to notifying ourselves
targets.append(jid)
while len(targets) > 0:
# Get next target (via JID)
target = targets.pop(0)
# Always call throttle before any remote server i/o is made
# Handler function to be called before each message.
# Always call throttle before any remote server i/o is made.
def on_before_message():
self.throttle()
# The message we wish to send, and the JID that
# will receive it.
xmpp.send_message(mto=target, mbody=body, mtype='chat')
# Communicate with XMPP.
xmpp_adapter = SleekXmppAdapter(
host=self.host, port=port, secure=self.secure,
verify_certificate=self.verify_certificate,
xep=self.xep, jid=jid, password=password,
body=body, targets=self.targets, before_message=on_before_message,
logger=self.logger)
# Using wait=True ensures that the send queue will be
# emptied before ending the session.
xmpp.disconnect(wait=True)
# Initialize XMPP machinery and begin processing the XML stream.
outcome = xmpp_adapter.process()
return True
return outcome
def url(self, privacy=False, *args, **kwargs):
"""
@ -427,3 +376,150 @@ class NotifyXMPP(NotifyBase):
NotifyXMPP.parse_list(results['qsd']['to'])
return results
class SleekXmppAdapter:
def __init__(self,
host=None, port=None, secure=None, verify_certificate=None,
xep=None, jid=None, password=None,
body=None, targets=None, before_message=None,
logger=None):
self.host = host
self.port = port
self.secure = secure
self.verify_certificate = verify_certificate
self.xep = xep
self.jid = jid
self.password = password
self.body = body
self.targets = targets
self.before_message = before_message
self.logger = logger or logging.getLogger(__name__)
# Reference to XMPP client.
self.xmpp = None
# Whether everything succeeded.
self.success = False
self.configure_logging()
self.setup()
def configure_logging(self):
# Use the Apprise log handlers for configuring
# the sleekxmpp logger.
apprise_logger = logging.getLogger('apprise')
sleek_logger = logging.getLogger('sleekxmpp')
for handler in apprise_logger.handlers:
sleek_logger.addHandler(handler)
sleek_logger.setLevel(apprise_logger.level)
def setup(self):
# Prepare our object
self.xmpp = sleekxmpp.ClientXMPP(self.jid, self.password)
self.xmpp.add_event_handler("session_start", self.session_start)
self.xmpp.add_event_handler("failed_auth", self.failed_auth)
for xep in self.xep:
# Load xep entries
self.xmpp.register_plugin('xep_{0:04d}'.format(xep))
if self.secure:
# Don't even try to use the outdated ssl.PROTOCOL_SSLx
self.xmpp.ssl_version = ssl.PROTOCOL_TLSv1
# If the python version supports it, use highest TLS version
# automatically
if hasattr(ssl, "PROTOCOL_TLS"):
# Use the best version of TLS available to us
self.xmpp.ssl_version = ssl.PROTOCOL_TLS
self.xmpp.ca_certs = None
if self.verify_certificate:
# Set the ca_certs variable for certificate verification
self.xmpp.ca_certs = next(
(cert for cert in CA_CERTIFICATE_FILE_LOCATIONS
if isfile(cert)), None)
if self.xmpp.ca_certs is None:
self.logger.warning(
'XMPP Secure comunication can not be verified; '
'no CA certificate found')
def process(self):
# Establish connection to XMPP server.
# To speed up sending messages, don't use the "reattempt" feature,
# it will add a nasty delay even before connecting to XMPP server.
use_ssl = self.port == NotifyXMPP.default_secure_port
if not self.xmpp.connect((self.host, self.port),
use_ssl=use_ssl, reattempt=False):
return False
# Process XMPP communication.
self.xmpp.process(block=True)
return self.success
def session_start(self, event):
# [amo, 2020-03-19]
# To speed up sending messages, let's skip presence signalling and
# roster inquiry. I believe both XMPP features resonate more with
# human users than bots.
"""
self.xmpp.send_presence()
try:
self.xmpp.get_roster()
except sleekxmpp.exceptions.IqError as e:
self.logger.warning('There was an error getting the XMPP roster.')
self.logger.debug(e.iq['error']['condition'])
self.xmpp.disconnect()
return False
except sleekxmpp.exceptions.IqTimeout:
self.logger.warning('XMPP Server is taking too long to respond.')
self.xmpp.disconnect()
return False
"""
targets = list(self.targets)
if not targets:
# We always default to notifying ourselves
targets.append(self.jid)
while len(targets) > 0:
# Get next target (via JID)
target = targets.pop(0)
# Invoke "before_message" event hook.
# Here, it will indirectly invoke the throttling feature,
# which adds a delay before any remote server i/o is made.
if callable(self.before_message):
self.before_message()
# The message we wish to send, and the JID that will receive it.
self.xmpp.send_message(mto=target, mbody=self.body, mtype='chat')
# Using wait=True ensures that the send queue will be
# emptied before ending the session.
self.xmpp.disconnect(wait=True)
self.success = True
def failed_auth(self, event):
self.logger.error('Authentication with XMPP server failed')

View File

@ -46,10 +46,18 @@ import logging
logging.disable(logging.CRITICAL)
# Mock the XMPP adapter to override "self.success".
from apprise.plugins.NotifyXMPP import SleekXmppAdapter
class MockedSleekXmppAdapter(SleekXmppAdapter):
def __init__(self, *args, **kwargs):
SleekXmppAdapter.__init__(self, *args, **kwargs)
self.success = True
def test_xmpp_plugin(tmpdir):
"""
API: NotifyXMPP Plugin()
"""
# Our module base
@ -103,9 +111,13 @@ def test_xmpp_plugin(tmpdir):
reload(sys.modules['apprise.Apprise'])
reload(sys.modules['apprise'])
NotifyXMPP = sys.modules['apprise.plugins.NotifyXMPP']
# An empty CA list
sys.modules['apprise.plugins.NotifyXMPP']\
.CA_CERTIFICATE_FILE_LOCATIONS = []
NotifyXMPP.CA_CERTIFICATE_FILE_LOCATIONS = []
# Mock the XMPP adapter to override "self.success"
NotifyXMPP.SleekXmppAdapter = MockedSleekXmppAdapter
# Disable Throttling to speed testing
apprise.plugins.NotifyBase.request_rate_per_sec = 0
@ -257,6 +269,9 @@ def test_xmpp_plugin(tmpdir):
xmpp.connect.return_value = True
# Test Exceptions
# These stopped working after starting to
# honor sleekxmpp's asynchronous nature.
"""
xmpp.get_roster.side_effect = \
sys.modules[sleekxmpp_name].exceptions.IqTimeout()
@ -269,3 +284,4 @@ def test_xmpp_plugin(tmpdir):
assert obj.notify(
title='', body='body', notify_type=apprise.NotifyType.INFO) is False
xmpp.get_roster.side_effect = None
"""