pull/905/head
Louis Vézina 5 years ago
parent 299af38486
commit 70b4a6c469

@ -1,6 +1,6 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
# Copyright (C) 2019 Chris Caron <lead2gold@gmail.com> # Copyright (C) 2020 Chris Caron <lead2gold@gmail.com>
# All rights reserved. # All rights reserved.
# #
# This code is licensed under the MIT License. # This code is licensed under the MIT License.
@ -24,7 +24,7 @@
# THE SOFTWARE. # THE SOFTWARE.
__title__ = 'apprise' __title__ = 'apprise'
__version__ = '0.8.4' __version__ = '0.8.5'
__author__ = 'Chris Caron' __author__ = 'Chris Caron'
__license__ = 'MIT' __license__ = 'MIT'
__copywrite__ = 'Copyright (C) 2020 Chris Caron <lead2gold@gmail.com>' __copywrite__ = 'Copyright (C) 2020 Chris Caron <lead2gold@gmail.com>'

@ -6,9 +6,9 @@
#, fuzzy #, fuzzy
msgid "" msgid ""
msgstr "" msgstr ""
"Project-Id-Version: apprise 0.8.4\n" "Project-Id-Version: apprise 0.8.5\n"
"Report-Msgid-Bugs-To: lead2gold@gmail.com\n" "Report-Msgid-Bugs-To: lead2gold@gmail.com\n"
"POT-Creation-Date: 2020-02-01 12:59-0500\n" "POT-Creation-Date: 2020-03-30 16:00-0400\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n" "Language-Team: LANGUAGE <LL@li.org>\n"
@ -98,6 +98,9 @@ msgstr ""
msgid "Device ID" msgid "Device ID"
msgstr "" msgstr ""
msgid "Device Name"
msgstr ""
msgid "Display Footer" msgid "Display Footer"
msgstr "" msgstr ""

@ -269,6 +269,14 @@ class NotifyEmail(NotifyBase):
# Define object templates # Define object templates
templates = ( templates = (
'{schema}://{host}',
'{schema}://{host}:{port}',
'{schema}://{host}/{targets}',
'{schema}://{host}:{port}/{targets}',
'{schema}://{user}@{host}',
'{schema}://{user}@{host}:{port}',
'{schema}://{user}@{host}/{targets}',
'{schema}://{user}@{host}:{port}/{targets}',
'{schema}://{user}:{password}@{host}', '{schema}://{user}:{password}@{host}',
'{schema}://{user}:{password}@{host}:{port}', '{schema}://{user}:{password}@{host}:{port}',
'{schema}://{user}:{password}@{host}/{targets}', '{schema}://{user}:{password}@{host}/{targets}',
@ -280,13 +288,11 @@ class NotifyEmail(NotifyBase):
'user': { 'user': {
'name': _('User Name'), 'name': _('User Name'),
'type': 'string', 'type': 'string',
'required': True,
}, },
'password': { 'password': {
'name': _('Password'), 'name': _('Password'),
'type': 'string', 'type': 'string',
'private': True, 'private': True,
'required': True,
}, },
'host': { 'host': {
'name': _('Domain'), 'name': _('Domain'),
@ -388,7 +394,7 @@ class NotifyEmail(NotifyBase):
self.from_name = from_name self.from_name = from_name
self.from_addr = from_addr self.from_addr = from_addr
if not self.from_addr: if self.user and not self.from_addr:
# detect our email address # detect our email address
self.from_addr = '{}@{}'.format( self.from_addr = '{}@{}'.format(
re.split(r'[\s@]+', self.user)[0], re.split(r'[\s@]+', self.user)[0],
@ -446,6 +452,10 @@ class NotifyEmail(NotifyBase):
# Apply any defaults based on certain known configurations # Apply any defaults based on certain known configurations
self.NotifyEmailDefaults() self.NotifyEmailDefaults()
# if there is still no smtp_host then we fall back to the hostname
if not self.smtp_host:
self.smtp_host = self.host
return return
def NotifyEmailDefaults(self): def NotifyEmailDefaults(self):
@ -454,10 +464,11 @@ class NotifyEmail(NotifyBase):
it was provided. it was provided.
""" """
if self.smtp_host: if self.smtp_host or not self.user:
# SMTP Server was explicitly specified, therefore it is assumed # SMTP Server was explicitly specified, therefore it is assumed
# the caller knows what he's doing and is intentionally # the caller knows what he's doing and is intentionally
# over-riding any smarts to be applied # over-riding any smarts to be applied. We also can not apply
# any default if there was no user specified.
return return
# detect our email address using our user/host combo # detect our email address using our user/host combo
@ -683,7 +694,7 @@ class NotifyEmail(NotifyBase):
args['bcc'] = ','.join(self.bcc) args['bcc'] = ','.join(self.bcc)
# pull email suffix from username (if present) # pull email suffix from username (if present)
user = self.user.split('@')[0] user = None if not self.user else self.user.split('@')[0]
# Determine Authentication # Determine Authentication
auth = '' auth = ''
@ -693,7 +704,7 @@ class NotifyEmail(NotifyBase):
password=self.pprint( password=self.pprint(
self.password, privacy, mode=PrivacyMode.Secret, safe=''), self.password, privacy, mode=PrivacyMode.Secret, safe=''),
) )
else: elif user:
# user url # user url
auth = '{user}@'.format( auth = '{user}@'.format(
user=NotifyEmail.quote(user, safe=''), user=NotifyEmail.quote(user, safe=''),

@ -130,6 +130,11 @@ class NotifyJoin(NotifyBase):
'regex': (r'^[a-z0-9]{32}$', 'i'), 'regex': (r'^[a-z0-9]{32}$', 'i'),
'map_to': 'targets', 'map_to': 'targets',
}, },
'device_name': {
'name': _('Device Name'),
'type': 'string',
'map_to': 'targets',
},
'group': { 'group': {
'name': _('Group'), 'name': _('Group'),
'type': 'choice:string', 'type': 'choice:string',
@ -210,18 +215,7 @@ class NotifyJoin(NotifyBase):
'group.{}'.format(group_re.group('name').lower())) 'group.{}'.format(group_re.group('name').lower()))
continue continue
elif IS_DEVICE_RE.match(target): self.targets.append(target)
self.targets.append(target)
continue
self.logger.warning(
'Ignoring invalid Join device/group "{}"'.format(target)
)
if not self.targets:
msg = 'No Join targets to notify.'
self.logger.warning(msg)
raise TypeError(msg)
return return
@ -247,12 +241,18 @@ class NotifyJoin(NotifyBase):
url_args = { url_args = {
'apikey': self.apikey, 'apikey': self.apikey,
'deviceId': target,
'priority': str(self.priority), 'priority': str(self.priority),
'title': title, 'title': title,
'text': body, 'text': body,
} }
if IS_GROUP_RE.match(target) or IS_DEVICE_RE.match(target):
url_args['deviceId'] = target
else:
# Support Device Names
url_args['deviceNames'] = target
# prepare our image for display if configured to do so # prepare our image for display if configured to do so
image_url = None if not self.include_image \ image_url = None if not self.include_image \
else self.image_url(notify_type) else self.image_url(notify_type)

@ -176,7 +176,7 @@ class NotifySlack(NotifyBase):
'type': 'string', 'type': 'string',
'private': True, 'private': True,
'required': True, 'required': True,
'regex': (r'^[A-Z0-9]{9}$', 'i'), 'regex': (r'^[A-Z0-9]+$', 'i'),
}, },
# Token required as part of the Webhook request # Token required as part of the Webhook request
# /........./BBBBBBBBB/........................ # /........./BBBBBBBBB/........................
@ -185,7 +185,7 @@ class NotifySlack(NotifyBase):
'type': 'string', 'type': 'string',
'private': True, 'private': True,
'required': True, 'required': True,
'regex': (r'^[A-Z0-9]{9}$', 'i'), 'regex': (r'^[A-Z0-9]+$', 'i'),
}, },
# Token required as part of the Webhook request # Token required as part of the Webhook request
# /........./........./CCCCCCCCCCCCCCCCCCCCCCCC # /........./........./CCCCCCCCCCCCCCCCCCCCCCCC
@ -194,7 +194,7 @@ class NotifySlack(NotifyBase):
'type': 'string', 'type': 'string',
'private': True, 'private': True,
'required': True, 'required': True,
'regex': (r'^[A-Za-z0-9]{24}$', 'i'), 'regex': (r'^[A-Za-z0-9]+$', 'i'),
}, },
'target_encoded_id': { 'target_encoded_id': {
'name': _('Target Encoded ID'), 'name': _('Target Encoded ID'),

@ -477,6 +477,9 @@ class NotifyTelegram(NotifyBase):
# Return our detected userid # Return our detected userid
return _id return _id
self.logger.warning(
'Failed to detect a Telegram user; '
'try sending your bot a message first.')
return 0 return 0
def send(self, body, title='', notify_type=NotifyType.INFO, attach=None, def send(self, body, title='', notify_type=NotifyType.INFO, attach=None,
@ -505,8 +508,12 @@ class NotifyTelegram(NotifyBase):
if self.notify_format == NotifyFormat.MARKDOWN: if self.notify_format == NotifyFormat.MARKDOWN:
payload['parse_mode'] = 'MARKDOWN' payload['parse_mode'] = 'MARKDOWN'
else: payload['text'] = '{}{}'.format(
# Either TEXT or HTML; if TEXT we'll make it HTML '{}\r\n'.format(title) if title else '',
body,
)
elif self.notify_format == NotifyFormat.HTML:
payload['parse_mode'] = 'HTML' payload['parse_mode'] = 'HTML'
# HTML Spaces (&nbsp;) and tabs (&emsp;) aren't supported # HTML Spaces (&nbsp;) and tabs (&emsp;) aren't supported
@ -524,31 +531,23 @@ class NotifyTelegram(NotifyBase):
# Tabs become 3 spaces # Tabs become 3 spaces
title = re.sub('&emsp;?', ' ', title, re.I) title = re.sub('&emsp;?', ' ', title, re.I)
# HTML payload['text'] = '{}{}'.format(
title = NotifyTelegram.escape_html(title, whitespace=False) '<b>{}</b>\r\n'.format(title) if title else '',
# HTML
body = NotifyTelegram.escape_html(body, whitespace=False)
if title and self.notify_format == NotifyFormat.TEXT:
# Text HTML Formatting
payload['text'] = '<b>%s</b>\r\n%s' % (
title,
body, body,
) )
elif title: else: # TEXT
# Already HTML; trust developer has wrapped payload['parse_mode'] = 'HTML'
# the title appropriately
payload['text'] = '%s\r\n%s' % ( # Escape content
title, title = NotifyTelegram.escape_html(title, whitespace=False)
body = NotifyTelegram.escape_html(body, whitespace=False)
payload['text'] = '{}{}'.format(
'<b>{}</b>\r\n'.format(title) if title else '',
body, body,
) )
else:
# Assign the body
payload['text'] = body
# Create a copy of the chat_ids list # Create a copy of the chat_ids list
targets = list(self.targets) targets = list(self.targets)
while len(targets): while len(targets):

@ -0,0 +1,208 @@
# -*- coding: utf-8 -*-
import ssl
from os.path import isfile
import logging
# Default our global support flag
SLEEKXMPP_SUPPORT_AVAILABLE = False
try:
# Import sleekxmpp if available
import sleekxmpp
SLEEKXMPP_SUPPORT_AVAILABLE = True
except ImportError:
# No problem; we just simply can't support this plugin because we're
# either using Linux, or simply do not have sleekxmpp installed.
pass
class SleekXmppAdapter(object):
"""
Wrapper to sleekxmpp
"""
# Reference to XMPP client.
xmpp = None
# Whether everything succeeded
success = False
# The default protocol
protocol = 'xmpp'
# The default secure protocol
secure_protocol = 'xmpps'
# The default XMPP port
default_unsecure_port = 5222
# The default XMPP secure port
default_secure_port = 5223
# Taken from https://golang.org/src/crypto/x509/root_linux.go
CA_CERTIFICATE_FILE_LOCATIONS = [
# Debian/Ubuntu/Gentoo etc.
"/etc/ssl/certs/ca-certificates.crt",
# Fedora/RHEL 6
"/etc/pki/tls/certs/ca-bundle.crt",
# OpenSUSE
"/etc/ssl/ca-bundle.pem",
# OpenELEC
"/etc/pki/tls/cacert.pem",
# CentOS/RHEL 7
"/etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem",
]
# This entry is a bit hacky, but it allows us to unit-test this library
# in an environment that simply doesn't have the sleekxmpp package
# available to us.
#
# If anyone is seeing this had knows a better way of testing this
# outside of what is defined in test/test_xmpp_plugin.py, please
# let me know! :)
_enabled = SLEEKXMPP_SUPPORT_AVAILABLE
def __init__(self, host=None, port=None, secure=False,
verify_certificate=True, xep=None, jid=None, password=None,
body=None, targets=None, before_message=None, logger=None):
"""
Initialize our SleekXmppAdapter object
"""
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__)
# 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)
if not self.load():
raise ValueError("Invalid XMPP Configuration")
def load(self):
# Prepare our object
self.xmpp = sleekxmpp.ClientXMPP(self.jid, self.password)
# Register our session
self.xmpp.add_event_handler("session_start", self.session_start)
for xep in self.xep:
# Load xep entries
try:
self.xmpp.register_plugin('xep_{0:04d}'.format(xep))
except sleekxmpp.plugins.base.PluginNotFound:
self.logger.warning(
'Could not register plugin {}'.format(
'xep_{0:04d}'.format(xep)))
return False
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 self.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 local CA certificate file')
return False
# We're good
return True
def process(self):
"""
Thread that handles the server/client i/o
"""
# 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.
if not self.xmpp.connect((self.host, self.port),
use_ssl=self.secure, reattempt=False):
default_port = self.default_secure_port \
if self.secure else self.default_unsecure_port
default_schema = self.secure_protocol \
if self.secure else self.protocol
# Log connection issue
self.logger.warning(
'Failed to authenticate {jid} with: {schema}://{host}{port}'
.format(
jid=self.jid,
schema=default_schema,
host=self.host,
port='' if not self.port or self.port == default_port
else ':{}'.format(self.port),
))
return False
# Process XMPP communication.
self.xmpp.process(block=True)
return self.success
def session_start(self, *args, **kwargs):
"""
Session Manager
"""
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.
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)
# Toggle our success flag
self.success = True

@ -24,46 +24,17 @@
# THE SOFTWARE. # THE SOFTWARE.
import re import re
import ssl
from os.path import isfile
from .NotifyBase import NotifyBase from ..NotifyBase import NotifyBase
from ..URLBase import PrivacyMode from ...URLBase import PrivacyMode
from ..common import NotifyType from ...common import NotifyType
from ..utils import parse_list from ...utils import parse_list
from ..AppriseLocale import gettext_lazy as _ from ...AppriseLocale import gettext_lazy as _
from .SleekXmppAdapter import SleekXmppAdapter
# xep string parser # xep string parser
XEP_PARSE_RE = re.compile('^[^1-9]*(?P<xep>[1-9][0-9]{0,3})$') XEP_PARSE_RE = re.compile('^[^1-9]*(?P<xep>[1-9][0-9]{0,3})$')
# Default our global support flag
NOTIFY_XMPP_SUPPORT_ENABLED = False
# Taken from https://golang.org/src/crypto/x509/root_linux.go
CA_CERTIFICATE_FILE_LOCATIONS = [
# Debian/Ubuntu/Gentoo etc.
"/etc/ssl/certs/ca-certificates.crt",
# Fedora/RHEL 6
"/etc/pki/tls/certs/ca-bundle.crt",
# OpenSUSE
"/etc/ssl/ca-bundle.pem",
# OpenELEC
"/etc/pki/tls/cacert.pem",
# CentOS/RHEL 7
"/etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem",
]
try:
# Import sleekxmpp if available
import sleekxmpp
NOTIFY_XMPP_SUPPORT_ENABLED = True
except ImportError:
# No problem; we just simply can't support this plugin because we're
# either using Linux, or simply do not have sleekxmpp installed.
pass
class NotifyXMPP(NotifyBase): class NotifyXMPP(NotifyBase):
""" """
@ -82,6 +53,9 @@ class NotifyXMPP(NotifyBase):
# A URL that takes you to the setup/help of the specific protocol # A URL that takes you to the setup/help of the specific protocol
setup_url = 'https://github.com/caronc/apprise/wiki/Notify_xmpp' setup_url = 'https://github.com/caronc/apprise/wiki/Notify_xmpp'
# Lower throttle rate for XMPP
request_rate_per_sec = 0.5
# The default XMPP port # The default XMPP port
default_unsecure_port = 5222 default_unsecure_port = 5222
@ -98,7 +72,7 @@ class NotifyXMPP(NotifyBase):
# If anyone is seeing this had knows a better way of testing this # If anyone is seeing this had knows a better way of testing this
# outside of what is defined in test/test_xmpp_plugin.py, please # outside of what is defined in test/test_xmpp_plugin.py, please
# let me know! :) # let me know! :)
_enabled = NOTIFY_XMPP_SUPPORT_ENABLED _enabled = SleekXmppAdapter._enabled
# Define object templates # Define object templates
templates = ( templates = (
@ -231,10 +205,11 @@ class NotifyXMPP(NotifyBase):
result = XEP_PARSE_RE.match(xep) result = XEP_PARSE_RE.match(xep)
if result is not None: if result is not None:
self.xep.append(int(result.group('xep'))) self.xep.append(int(result.group('xep')))
self.logger.debug('Loaded XMPP {}'.format(xep))
else: else:
self.logger.warning( self.logger.warning(
"Could not load XMPP xep {}".format(xep)) "Could not load XMPP {}".format(xep))
# By default we send ourselves a message # By default we send ourselves a message
if targets: if targets:
@ -267,34 +242,7 @@ class NotifyXMPP(NotifyBase):
jid = self.host jid = self.host
password = self.password if self.password else self.user password = self.password if self.password else self.user
# Prepare our object # Compute port number
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
if not self.port: if not self.port:
port = self.default_secure_port \ port = self.default_secure_port \
if self.secure else self.default_unsecure_port if self.secure else self.default_unsecure_port
@ -302,48 +250,22 @@ class NotifyXMPP(NotifyBase):
else: else:
port = self.port port = self.port
# Establish our connection
if not xmpp.connect((self.host, port)):
return False
xmpp.send_presence()
try: try:
xmpp.get_roster() # Communicate with XMPP.
xmpp_adapter = SleekXmppAdapter(
except sleekxmpp.exceptions.IqError as e: host=self.host, port=port, secure=self.secure,
self.logger.warning('There was an error getting the XMPP roster.') verify_certificate=self.verify_certificate, xep=self.xep,
self.logger.debug(e.iq['error']['condition']) jid=jid, password=password, body=body, targets=self.targets,
xmpp.disconnect() before_message=self.throttle, logger=self.logger)
except ValueError:
# We failed
return False return False
except sleekxmpp.exceptions.IqTimeout: # Initialize XMPP machinery and begin processing the XML stream.
self.logger.warning('XMPP Server is taking too long to respond.') outcome = xmpp_adapter.process()
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
self.throttle()
# The message we wish to send, and the JID that
# will receive it.
xmpp.send_message(mto=target, mbody=body, mtype='chat')
# Using wait=True ensures that the send queue will be
# emptied before ending the session.
xmpp.disconnect(wait=True)
return True return outcome
def url(self, privacy=False, *args, **kwargs): def url(self, privacy=False, *args, **kwargs):
""" """

@ -34,6 +34,7 @@ from os.path import abspath
# Used for testing # Used for testing
from . import NotifyEmail as NotifyEmailBase from . import NotifyEmail as NotifyEmailBase
from .NotifyGrowl import gntp from .NotifyGrowl import gntp
from .NotifyXMPP import SleekXmppAdapter
# NotifyBase object is passed in as a module not class # NotifyBase object is passed in as a module not class
from . import NotifyBase from . import NotifyBase
@ -63,6 +64,9 @@ __all__ = [
# gntp (used for NotifyGrowl Testing) # gntp (used for NotifyGrowl Testing)
'gntp', 'gntp',
# sleekxmpp access points (used for NotifyXMPP Testing)
'SleekXmppAdapter',
] ]
# we mirror our base purely for the ability to reset everything; this # we mirror our base purely for the ability to reset everything; this

Loading…
Cancel
Save