summaryrefslogtreecommitdiffhomepage
path: root/libs/apprise
diff options
context:
space:
mode:
authorLouis Vézina <[email protected]>2020-09-14 08:24:26 -0400
committerLouis Vézina <[email protected]>2020-09-14 08:24:26 -0400
commite6b8b1ad195d553bca2ecff7e52f67ba4f5871ec (patch)
treefd56bb19e90afbf87bf845a74d69ca17cda0deff /libs/apprise
parentae731bb78b1d91b3e7f6017830c4014fae4aa6ff (diff)
downloadbazarr-e6b8b1ad195d553bca2ecff7e52f67ba4f5871ec.tar.gz
bazarr-e6b8b1ad195d553bca2ecff7e52f67ba4f5871ec.zip
Updated Apprise to 0.8.8
Diffstat (limited to 'libs/apprise')
-rw-r--r--libs/apprise/Apprise.py54
-rw-r--r--libs/apprise/AppriseAsset.py6
-rw-r--r--libs/apprise/AppriseConfig.py85
-rw-r--r--libs/apprise/URLBase.py167
-rw-r--r--libs/apprise/__init__.py8
-rw-r--r--libs/apprise/assets/themes/default/apprise-logo.pngbin37371 -> 160907 bytes
-rw-r--r--libs/apprise/attachment/AttachFile.py12
-rw-r--r--libs/apprise/attachment/AttachHTTP.py32
-rw-r--r--libs/apprise/cli.py84
-rw-r--r--libs/apprise/common.py29
-rw-r--r--libs/apprise/config/ConfigBase.py287
-rw-r--r--libs/apprise/config/ConfigFile.py34
-rw-r--r--libs/apprise/config/ConfigHTTP.py30
-rw-r--r--libs/apprise/config/__init__.py60
-rw-r--r--libs/apprise/i18n/apprise.pot51
-rw-r--r--libs/apprise/plugins/NotifyBase.py73
-rw-r--r--libs/apprise/plugins/NotifyBoxcar.py18
-rw-r--r--libs/apprise/plugins/NotifyClickSend.py19
-rw-r--r--libs/apprise/plugins/NotifyD7Networks.py24
-rw-r--r--libs/apprise/plugins/NotifyDBus.py44
-rw-r--r--libs/apprise/plugins/NotifyDiscord.py84
-rw-r--r--libs/apprise/plugins/NotifyEmail.py207
-rw-r--r--libs/apprise/plugins/NotifyEmby.py47
-rw-r--r--libs/apprise/plugins/NotifyEnigma2.py27
-rw-r--r--libs/apprise/plugins/NotifyFaast.py30
-rw-r--r--libs/apprise/plugins/NotifyFlock.py35
-rw-r--r--libs/apprise/plugins/NotifyGitter.py20
-rw-r--r--libs/apprise/plugins/NotifyGnome.py32
-rw-r--r--libs/apprise/plugins/NotifyGotify.py49
-rw-r--r--libs/apprise/plugins/NotifyGrowl/__init__.py374
-rw-r--r--libs/apprise/plugins/NotifyGrowl/gntp/__init__.py0
-rw-r--r--libs/apprise/plugins/NotifyGrowl/gntp/cli.py141
-rw-r--r--libs/apprise/plugins/NotifyGrowl/gntp/config.py77
-rw-r--r--libs/apprise/plugins/NotifyGrowl/gntp/core.py511
-rw-r--r--libs/apprise/plugins/NotifyGrowl/gntp/errors.py25
-rw-r--r--libs/apprise/plugins/NotifyGrowl/gntp/notifier.py265
-rw-r--r--libs/apprise/plugins/NotifyGrowl/gntp/shim.py45
-rw-r--r--libs/apprise/plugins/NotifyGrowl/gntp/version.py4
-rw-r--r--libs/apprise/plugins/NotifyIFTTT.py32
-rw-r--r--libs/apprise/plugins/NotifyJSON.py25
-rw-r--r--libs/apprise/plugins/NotifyJoin.py22
-rw-r--r--libs/apprise/plugins/NotifyKavenegar.py18
-rw-r--r--libs/apprise/plugins/NotifyKumulos.py20
-rw-r--r--libs/apprise/plugins/NotifyMSG91.py24
-rw-r--r--libs/apprise/plugins/NotifyMSTeams.py28
-rw-r--r--libs/apprise/plugins/NotifyMailgun.py24
-rw-r--r--libs/apprise/plugins/NotifyMatrix.py62
-rw-r--r--libs/apprise/plugins/NotifyMatterMost.py26
-rw-r--r--libs/apprise/plugins/NotifyMessageBird.py22
-rw-r--r--libs/apprise/plugins/NotifyNexmo.py24
-rw-r--r--libs/apprise/plugins/NotifyNextcloud.py25
-rw-r--r--libs/apprise/plugins/NotifyNotica.py47
-rw-r--r--libs/apprise/plugins/NotifyNotifico.py29
-rw-r--r--libs/apprise/plugins/NotifyProwl.py22
-rw-r--r--libs/apprise/plugins/NotifyPushBullet.py47
-rw-r--r--libs/apprise/plugins/NotifyPushSafer.py27
-rw-r--r--libs/apprise/plugins/NotifyPushed.py20
-rw-r--r--libs/apprise/plugins/NotifyPushjet.py50
-rw-r--r--libs/apprise/plugins/NotifyPushover.py27
-rw-r--r--libs/apprise/plugins/NotifyRocketChat.py31
-rw-r--r--libs/apprise/plugins/NotifyRyver.py45
-rw-r--r--libs/apprise/plugins/NotifySNS.py20
-rw-r--r--libs/apprise/plugins/NotifySendGrid.py64
-rw-r--r--libs/apprise/plugins/NotifySimplePush.py38
-rw-r--r--libs/apprise/plugins/NotifySinch.py19
-rw-r--r--libs/apprise/plugins/NotifySlack.py33
-rw-r--r--libs/apprise/plugins/NotifySyslog.py17
-rw-r--r--libs/apprise/plugins/NotifyTechulusPush.py18
-rw-r--r--libs/apprise/plugins/NotifyTelegram.py73
-rw-r--r--libs/apprise/plugins/NotifyTwilio.py17
-rw-r--r--libs/apprise/plugins/NotifyTwist.py95
-rw-r--r--libs/apprise/plugins/NotifyTwitter.py37
-rw-r--r--libs/apprise/plugins/NotifyWebexTeams.py26
-rw-r--r--libs/apprise/plugins/NotifyWindows.py35
-rw-r--r--libs/apprise/plugins/NotifyXBMC.py30
-rw-r--r--libs/apprise/plugins/NotifyXML.py25
-rw-r--r--libs/apprise/plugins/NotifyXMPP/__init__.py22
-rw-r--r--libs/apprise/plugins/NotifyZulip.py27
-rw-r--r--libs/apprise/plugins/__init__.py84
-rw-r--r--libs/apprise/utils.py251
80 files changed, 2029 insertions, 2689 deletions
diff --git a/libs/apprise/Apprise.py b/libs/apprise/Apprise.py
index bb9504663..b95da22a7 100644
--- a/libs/apprise/Apprise.py
+++ b/libs/apprise/Apprise.py
@@ -33,7 +33,7 @@ from .common import NotifyFormat
from .common import MATCH_ALL_TAG
from .utils import is_exclusive_match
from .utils import parse_list
-from .utils import split_urls
+from .utils import parse_urls
from .logger import logger
from .AppriseAsset import AppriseAsset
@@ -46,13 +46,19 @@ from .plugins.NotifyBase import NotifyBase
from . import plugins
from . import __version__
+# Python v3+ support code made importable so it can remain backwards
+# compatible with Python v2
+from . import py3compat
+ASYNCIO_SUPPORT = not six.PY2
+
class Apprise(object):
"""
Our Notification Manager
"""
- def __init__(self, servers=None, asset=None):
+
+ def __init__(self, servers=None, asset=None, debug=False):
"""
Loads a set of server urls while applying the Asset() module to each
if specified.
@@ -78,6 +84,9 @@ class Apprise(object):
# Initialize our locale object
self.locale = AppriseLocale()
+ # Set our debug flag
+ self.debug = debug
+
@staticmethod
def instantiate(url, asset=None, tag=None, suppress_exceptions=True):
"""
@@ -111,14 +120,10 @@ class Apprise(object):
# Acquire our url tokens
results = plugins.url_to_dict(url)
if results is None:
- # Failed to parse the server URL
- logger.error('Unparseable URL {}.'.format(url))
+ # Failed to parse the server URL; detailed logging handled
+ # inside url_to_dict - nothing to report here.
return None
- logger.trace('URL {} unpacked as:{}{}'.format(
- url, os.linesep, os.linesep.join(
- ['{}="{}"'.format(k, v) for k, v in results.items()])))
-
elif isinstance(url, dict):
# We already have our result set
results = url
@@ -154,11 +159,14 @@ class Apprise(object):
plugin = plugins.SCHEMA_MAP[results['schema']](**results)
# Create log entry of loaded URL
- logger.debug('Loaded URL: {}'.format(plugin.url()))
+ logger.debug('Loaded {} URL: {}'.format(
+ plugins.SCHEMA_MAP[results['schema']].service_name,
+ plugin.url()))
except Exception:
# the arguments are invalid or can not be used.
- logger.error('Could not load URL: %s' % url)
+ logger.error('Could not load {} URL: {}'.format(
+ plugins.SCHEMA_MAP[results['schema']].service_name, url))
return None
else:
@@ -189,7 +197,7 @@ class Apprise(object):
if isinstance(servers, six.string_types):
# build our server list
- servers = split_urls(servers)
+ servers = parse_urls(servers)
if len(servers) == 0:
return False
@@ -226,7 +234,7 @@ class Apprise(object):
# returns None if it fails
instance = Apprise.instantiate(_server, asset=asset, tag=tag)
if not isinstance(instance, NotifyBase):
- # No logging is requird as instantiate() handles failure
+ # No logging is required as instantiate() handles failure
# and/or success reasons for us
return_status = False
continue
@@ -327,6 +335,10 @@ class Apprise(object):
body_format = self.asset.body_format \
if body_format is None else body_format
+ # for asyncio support; we track a list of our servers to notify
+ # sequentially
+ coroutines = []
+
# Iterate over our loaded plugins
for server in self.find(tag):
if status is None:
@@ -384,6 +396,18 @@ class Apprise(object):
# Store entry directly
conversion_map[server.notify_format] = body
+ if ASYNCIO_SUPPORT and server.asset.async_mode:
+ # Build a list of servers requiring notification
+ # that will be triggered asynchronously afterwards
+ coroutines.append(server.async_notify(
+ body=conversion_map[server.notify_format],
+ title=title,
+ notify_type=notify_type,
+ attach=attach))
+
+ # We gather at this point and notify at the end
+ continue
+
try:
# Send notification
if not server.notify(
@@ -405,6 +429,12 @@ class Apprise(object):
logger.exception("Notification Exception")
status = False
+ if coroutines:
+ # perform our async notification(s)
+ if not py3compat.asyncio.notify(coroutines, debug=self.debug):
+ # Toggle our status only if we had a failure
+ status = False
+
return status
def details(self, lang=None):
diff --git a/libs/apprise/AppriseAsset.py b/libs/apprise/AppriseAsset.py
index 9ad834fb6..123da7225 100644
--- a/libs/apprise/AppriseAsset.py
+++ b/libs/apprise/AppriseAsset.py
@@ -99,6 +99,12 @@ class AppriseAsset(object):
# will be the default.
body_format = None
+ # Always attempt to send notifications asynchronous (as the same time
+ # if possible)
+ # This is a Python 3 supported option only. If set to False, then
+ # notifications are sent sequentially (one after another)
+ async_mode = True
+
def __init__(self, **kwargs):
"""
Asset Initialization
diff --git a/libs/apprise/AppriseConfig.py b/libs/apprise/AppriseConfig.py
index 902dfa6dd..fa5e6fba9 100644
--- a/libs/apprise/AppriseConfig.py
+++ b/libs/apprise/AppriseConfig.py
@@ -27,6 +27,7 @@ import six
from . import config
from . import ConfigBase
+from . import CONFIG_FORMATS
from . import URLBase
from .AppriseAsset import AppriseAsset
@@ -46,7 +47,8 @@ class AppriseConfig(object):
"""
- def __init__(self, paths=None, asset=None, cache=True, **kwargs):
+ def __init__(self, paths=None, asset=None, cache=True, recursion=0,
+ insecure_includes=False, **kwargs):
"""
Loads all of the paths specified (if any).
@@ -69,6 +71,29 @@ class AppriseConfig(object):
It's also worth nothing that the cache value is only set to elements
that are not already of subclass ConfigBase()
+
+ recursion defines how deep we recursively handle entries that use the
+ `import` keyword. This keyword requires us to fetch more configuration
+ from another source and add it to our existing compilation. If the
+ file we remotely retrieve also has an `import` reference, we will only
+ advance through it if recursion is set to 2 deep. If set to zero
+ it is off. There is no limit to how high you set this value. It would
+ be recommended to keep it low if you do intend to use it.
+
+ insecure includes by default are disabled. When set to True, all
+ Apprise Config files marked to be in STRICT mode are treated as being
+ in ALWAYS mode.
+
+ Take a file:// based configuration for example, only a file:// based
+ configuration can import another file:// based one. because it is set
+ to STRICT mode. If an http:// based configuration file attempted to
+ import a file:// one it woul fail. However this import would be
+ possible if insecure_includes is set to True.
+
+ There are cases where a self hosting apprise developer may wish to load
+ configuration from memory (in a string format) that contains import
+ entries (even file:// based ones). In these circumstances if you want
+ these includes to be honored, this value must be set to True.
"""
# Initialize a server list of URLs
@@ -81,13 +106,20 @@ class AppriseConfig(object):
# Set our cache flag
self.cache = cache
+ # Initialize our recursion value
+ self.recursion = recursion
+
+ # Initialize our insecure_includes flag
+ self.insecure_includes = insecure_includes
+
if paths is not None:
# Store our path(s)
self.add(paths)
return
- def add(self, configs, asset=None, tag=None, cache=True):
+ def add(self, configs, asset=None, tag=None, cache=True, recursion=None,
+ insecure_includes=None):
"""
Adds one or more config URLs into our list.
@@ -107,6 +139,12 @@ class AppriseConfig(object):
It's also worth nothing that the cache value is only set to elements
that are not already of subclass ConfigBase()
+
+ Optionally override the default recursion value.
+
+ Optionally override the insecure_includes flag.
+ if insecure_includes is set to True then all plugins that are
+ set to a STRICT mode will be a treated as ALWAYS.
"""
# Initialize our return status
@@ -115,6 +153,14 @@ class AppriseConfig(object):
# Initialize our default cache value
cache = cache if cache is not None else self.cache
+ # Initialize our default recursion value
+ recursion = recursion if recursion is not None else self.recursion
+
+ # Initialize our default insecure_includes value
+ insecure_includes = \
+ insecure_includes if insecure_includes is not None \
+ else self.insecure_includes
+
if asset is None:
# prepare default asset
asset = self.asset
@@ -154,7 +200,8 @@ class AppriseConfig(object):
# Instantiate ourselves an object, this function throws or
# returns None if it fails
instance = AppriseConfig.instantiate(
- _config, asset=asset, tag=tag, cache=cache)
+ _config, asset=asset, tag=tag, cache=cache,
+ recursion=recursion, insecure_includes=insecure_includes)
if not isinstance(instance, ConfigBase):
return_status = False
continue
@@ -165,7 +212,8 @@ class AppriseConfig(object):
# Return our status
return return_status
- def add_config(self, content, asset=None, tag=None, format=None):
+ def add_config(self, content, asset=None, tag=None, format=None,
+ recursion=None, insecure_includes=None):
"""
Adds one configuration file in it's raw format. Content gets loaded as
a memory based object and only exists for the life of this
@@ -174,8 +222,22 @@ class AppriseConfig(object):
If you know the format ('yaml' or 'text') you can specify
it for slightly less overhead during this call. Otherwise the
configuration is auto-detected.
+
+ Optionally override the default recursion value.
+
+ Optionally override the insecure_includes flag.
+ if insecure_includes is set to True then all plugins that are
+ set to a STRICT mode will be a treated as ALWAYS.
"""
+ # Initialize our default recursion value
+ recursion = recursion if recursion is not None else self.recursion
+
+ # Initialize our default insecure_includes value
+ insecure_includes = \
+ insecure_includes if insecure_includes is not None \
+ else self.insecure_includes
+
if asset is None:
# prepare default asset
asset = self.asset
@@ -190,7 +252,13 @@ class AppriseConfig(object):
# Create ourselves a ConfigMemory Object to store our configuration
instance = config.ConfigMemory(
- content=content, format=format, asset=asset, tag=tag)
+ content=content, format=format, asset=asset, tag=tag,
+ recursion=recursion, insecure_includes=insecure_includes)
+
+ if instance.config_format not in CONFIG_FORMATS:
+ logger.warning(
+ "The format of the configuration could not be deteced.")
+ return False
# Add our initialized plugin to our server listings
self.configs.append(instance)
@@ -235,6 +303,7 @@ class AppriseConfig(object):
@staticmethod
def instantiate(url, asset=None, tag=None, cache=None,
+ recursion=0, insecure_includes=False,
suppress_exceptions=True):
"""
Returns the instance of a instantiated configuration plugin based on
@@ -279,6 +348,12 @@ class AppriseConfig(object):
# Force an over-ride of the cache value to what we have specified
results['cache'] = cache
+ # Recursion can never be parsed from the URL
+ results['recursion'] = recursion
+
+ # Insecure includes flag can never be parsed from the URL
+ results['insecure_includes'] = insecure_includes
+
if suppress_exceptions:
try:
# Attempt to create an instance of our plugin using the parsed
diff --git a/libs/apprise/URLBase.py b/libs/apprise/URLBase.py
index 4d62b82cd..78109ae48 100644
--- a/libs/apprise/URLBase.py
+++ b/libs/apprise/URLBase.py
@@ -42,6 +42,7 @@ except ImportError:
from urllib.parse import quote as _quote
from urllib.parse import urlencode as _urlencode
+from .AppriseLocale import gettext_lazy as _
from .AppriseAsset import AppriseAsset
from .utils import parse_url
from .utils import parse_bool
@@ -98,6 +99,16 @@ class URLBase(object):
# Throttle
request_rate_per_sec = 0
+ # The connect timeout is the number of seconds Requests will wait for your
+ # client to establish a connection to a remote machine (corresponding to
+ # the connect()) call on the socket.
+ socket_connect_timeout = 4.0
+
+ # The read timeout is the number of seconds the client will wait for the
+ # server to send a response.
+ socket_read_timeout = 4.0
+
+ # Handle
# Maintain a set of tags to associate with this specific notification
tags = set()
@@ -107,6 +118,78 @@ class URLBase(object):
# Logging
logger = logging.getLogger(__name__)
+ # Define a default set of template arguments used for dynamically building
+ # details about our individual plugins for developers.
+
+ # Define object templates
+ templates = ()
+
+ # Provides a mapping of tokens, certain entries are fixed and automatically
+ # configured if found (such as schema, host, user, pass, and port)
+ template_tokens = {}
+
+ # Here is where we define all of the arguments we accept on the url
+ # such as: schema://whatever/?cto=5.0&rto=15
+ # These act the same way as tokens except they are optional and/or
+ # have default values set if mandatory. This rule must be followed
+ template_args = {
+ 'verify': {
+ 'name': _('Verify SSL'),
+ # SSL Certificate Authority Verification
+ 'type': 'bool',
+ # Provide a default
+ 'default': verify_certificate,
+ # look up default using the following parent class value at
+ # runtime.
+ '_lookup_default': 'verify_certificate',
+ },
+ 'rto': {
+ 'name': _('Socket Read Timeout'),
+ 'type': 'float',
+ # Provide a default
+ 'default': socket_read_timeout,
+ # look up default using the following parent class value at
+ # runtime. The variable name identified here (in this case
+ # socket_read_timeout) is checked and it's result is placed
+ # over-top of the 'default'. This is done because once a parent
+ # class inherits this one, the overflow_mode already set as a
+ # default 'could' be potentially over-ridden and changed to a
+ # different value.
+ '_lookup_default': 'socket_read_timeout',
+ },
+ 'cto': {
+ 'name': _('Socket Connect Timeout'),
+ 'type': 'float',
+ # Provide a default
+ 'default': socket_connect_timeout,
+ # look up default using the following parent class value at
+ # runtime. The variable name identified here (in this case
+ # socket_connect_timeout) is checked and it's result is placed
+ # over-top of the 'default'. This is done because once a parent
+ # class inherits this one, the overflow_mode already set as a
+ # default 'could' be potentially over-ridden and changed to a
+ # different value.
+ '_lookup_default': 'socket_connect_timeout',
+ },
+ }
+
+ # kwargs are dynamically built because a prefix causes us to parse the
+ # content slightly differently. The prefix is required and can be either
+ # a (+ or -). Below would handle the +key=value:
+ # {
+ # 'headers': {
+ # 'name': _('HTTP Header'),
+ # 'prefix': '+',
+ # 'type': 'string',
+ # },
+ # },
+ #
+ # In a kwarg situation, the 'key' is always presumed to be treated as
+ # a string. When the 'type' is defined, it is being defined to respect
+ # the 'value'.
+
+ template_kwargs = {}
+
def __init__(self, asset=None, **kwargs):
"""
Initialize some general logging and common server arguments that will
@@ -131,6 +214,9 @@ class URLBase(object):
self.port = int(self.port)
except (TypeError, ValueError):
+ self.logger.warning(
+ 'Invalid port number specified {}'
+ .format(self.port))
self.port = None
self.user = kwargs.get('user')
@@ -143,6 +229,26 @@ class URLBase(object):
# Always unquote the password if it exists
self.password = URLBase.unquote(self.password)
+ # Store our Timeout Variables
+ if 'socket_read_timeout' in kwargs:
+ try:
+ self.socket_read_timeout = \
+ float(kwargs.get('socket_read_timeout'))
+ except (TypeError, ValueError):
+ self.logger.warning(
+ 'Invalid socket read timeout (rto) was specified {}'
+ .format(kwargs.get('socket_read_timeout')))
+
+ if 'socket_connect_timeout' in kwargs:
+ try:
+ self.socket_connect_timeout = \
+ float(kwargs.get('socket_connect_timeout'))
+
+ except (TypeError, ValueError):
+ self.logger.warning(
+ 'Invalid socket connect timeout (cto) was specified {}'
+ .format(kwargs.get('socket_connect_timeout')))
+
if 'tag' in kwargs:
# We want to associate some tags with our notification service.
# the code below gets the 'tag' argument if defined, otherwise
@@ -456,15 +562,41 @@ class URLBase(object):
@property
def app_id(self):
- return self.asset.app_id
+ return self.asset.app_id if self.asset.app_id else ''
@property
def app_desc(self):
- return self.asset.app_desc
+ return self.asset.app_desc if self.asset.app_desc else ''
@property
def app_url(self):
- return self.asset.app_url
+ return self.asset.app_url if self.asset.app_url else ''
+
+ @property
+ def request_timeout(self):
+ """This is primarily used to fullfill the `timeout` keyword argument
+ that is used by requests.get() and requests.put() calls.
+ """
+ return (self.socket_connect_timeout, self.socket_read_timeout)
+
+ def url_parameters(self, *args, **kwargs):
+ """
+ Provides a default set of args to work with. This can greatly
+ simplify URL construction in the acommpanied url() function.
+
+ The following property returns a dictionary (of strings) containing
+ all of the parameters that can be set on a URL and managed through
+ this class.
+ """
+
+ return {
+ # The socket read timeout
+ 'rto': str(self.socket_read_timeout),
+ # The request/socket connect timeout
+ 'cto': str(self.socket_connect_timeout),
+ # Certificate verification
+ 'verify': 'yes' if self.verify_certificate else 'no',
+ }
@staticmethod
def parse_url(url, verify_host=True):
@@ -511,6 +643,14 @@ class URLBase(object):
if 'user' in results['qsd']:
results['user'] = results['qsd']['user']
+ # Store our socket read timeout if specified
+ if 'rto' in results['qsd']:
+ results['socket_read_timeout'] = results['qsd']['rto']
+
+ # Store our socket connect timeout if specified
+ if 'cto' in results['qsd']:
+ results['socket_connect_timeout'] = results['qsd']['cto']
+
return results
@staticmethod
@@ -534,3 +674,24 @@ class URLBase(object):
response = ''
return response
+
+ def schemas(self):
+ """A simple function that returns a set of all schemas associated
+ with this object based on the object.protocol and
+ object.secure_protocol
+ """
+
+ schemas = set([])
+
+ for key in ('protocol', 'secure_protocol'):
+ schema = getattr(self, key, None)
+ if isinstance(schema, six.string_types):
+ schemas.add(schema)
+
+ elif isinstance(schema, (set, list, tuple)):
+ # Support iterables list types
+ for s in schema:
+ if isinstance(s, six.string_types):
+ schemas.add(s)
+
+ return schemas
diff --git a/libs/apprise/__init__.py b/libs/apprise/__init__.py
index 63da23f8c..a2511d286 100644
--- a/libs/apprise/__init__.py
+++ b/libs/apprise/__init__.py
@@ -24,7 +24,7 @@
# THE SOFTWARE.
__title__ = 'apprise'
-__version__ = '0.8.5'
+__version__ = '0.8.8'
__author__ = 'Chris Caron'
__license__ = 'MIT'
__copywrite__ = 'Copyright (C) 2020 Chris Caron <[email protected]>'
@@ -41,6 +41,8 @@ from .common import OverflowMode
from .common import OVERFLOW_MODES
from .common import ConfigFormat
from .common import CONFIG_FORMATS
+from .common import ConfigIncludeMode
+from .common import CONFIG_INCLUDE_MODES
from .URLBase import URLBase
from .URLBase import PrivacyMode
@@ -66,5 +68,7 @@ __all__ = [
# Reference
'NotifyType', 'NotifyImageSize', 'NotifyFormat', 'OverflowMode',
'NOTIFY_TYPES', 'NOTIFY_IMAGE_SIZES', 'NOTIFY_FORMATS', 'OVERFLOW_MODES',
- 'ConfigFormat', 'CONFIG_FORMATS', 'PrivacyMode',
+ 'ConfigFormat', 'CONFIG_FORMATS',
+ 'ConfigIncludeMode', 'CONFIG_INCLUDE_MODES',
+ 'PrivacyMode',
]
diff --git a/libs/apprise/assets/themes/default/apprise-logo.png b/libs/apprise/assets/themes/default/apprise-logo.png
index 7617bb5b2..aa6824bed 100644
--- a/libs/apprise/assets/themes/default/apprise-logo.png
+++ b/libs/apprise/assets/themes/default/apprise-logo.png
Binary files differ
diff --git a/libs/apprise/attachment/AttachFile.py b/libs/apprise/attachment/AttachFile.py
index 478e3d6f3..a8609bd60 100644
--- a/libs/apprise/attachment/AttachFile.py
+++ b/libs/apprise/attachment/AttachFile.py
@@ -57,20 +57,20 @@ class AttachFile(AttachBase):
Returns the URL built dynamically based on specified arguments.
"""
- # Define any arguments set
- args = {}
+ # Define any URL parameters
+ params = {}
if self._mimetype:
# A mime-type was enforced
- args['mime'] = self._mimetype
+ params['mime'] = self._mimetype
if self._name:
# A name was enforced
- args['name'] = self._name
+ params['name'] = self._name
- return 'file://{path}{args}'.format(
+ return 'file://{path}{params}'.format(
path=self.quote(self.dirty_path),
- args='?{}'.format(self.urlencode(args)) if args else '',
+ params='?{}'.format(self.urlencode(params)) if params else '',
)
def download(self, **kwargs):
diff --git a/libs/apprise/attachment/AttachHTTP.py b/libs/apprise/attachment/AttachHTTP.py
index 046babddb..d5396cf85 100644
--- a/libs/apprise/attachment/AttachHTTP.py
+++ b/libs/apprise/attachment/AttachHTTP.py
@@ -47,10 +47,6 @@ class AttachHTTP(AttachBase):
# The default secure protocol
secure_protocol = 'https'
- # The maximum number of seconds to wait for a connection to be established
- # before out-right just giving up
- connection_timeout_sec = 5.0
-
# The number of bytes in memory to read from the remote source at a time
chunk_size = 8192
@@ -129,7 +125,7 @@ class AttachHTTP(AttachBase):
auth=auth,
params=self.qsd,
verify=self.verify_certificate,
- timeout=self.connection_timeout_sec,
+ timeout=self.request_timeout,
stream=True) as r:
# Handle Errors
@@ -215,7 +211,7 @@ class AttachHTTP(AttachBase):
except requests.RequestException as e:
self.logger.error(
- 'A Connection error occured retrieving HTTP '
+ 'A Connection error occurred retrieving HTTP '
'configuration from %s.' % self.host)
self.logger.debug('Socket Exception: %s' % str(e))
@@ -258,10 +254,8 @@ class AttachHTTP(AttachBase):
Returns the URL built dynamically based on specified arguments.
"""
- # Define any arguments set
- args = {
- 'verify': 'yes' if self.verify_certificate else 'no',
- }
+ # Our URL parameters
+ params = self.url_parameters(privacy=privacy, *args, **kwargs)
# Prepare our cache value
if self.cache is not None:
@@ -271,21 +265,21 @@ class AttachHTTP(AttachBase):
cache = int(self.cache)
# Set our cache value
- args['cache'] = cache
+ params['cache'] = cache
if self._mimetype:
# A format was enforced
- args['mime'] = self._mimetype
+ params['mime'] = self._mimetype
if self._name:
# A name was enforced
- args['name'] = self._name
+ params['name'] = self._name
- # Append our headers into our args
- args.update({'+{}'.format(k): v for k, v in self.headers.items()})
+ # Append our headers into our parameters
+ params.update({'+{}'.format(k): v for k, v in self.headers.items()})
# Apply any remaining entries to our URL
- args.update(self.qsd)
+ params.update(self.qsd)
# Determine Authentication
auth = ''
@@ -302,21 +296,21 @@ class AttachHTTP(AttachBase):
default_port = 443 if self.secure else 80
- return '{schema}://{auth}{hostname}{port}{fullpath}?{args}'.format(
+ return '{schema}://{auth}{hostname}{port}{fullpath}?{params}'.format(
schema=self.secure_protocol if self.secure else self.protocol,
auth=auth,
hostname=self.quote(self.host, safe=''),
port='' if self.port is None or self.port == default_port
else ':{}'.format(self.port),
fullpath=self.quote(self.fullpath, safe='/'),
- args=self.urlencode(args),
+ params=self.urlencode(params),
)
@staticmethod
def parse_url(url):
"""
Parses the URL and returns enough arguments that can allow
- us to substantiate this object.
+ us to re-instantiate this object.
"""
results = AttachBase.parse_url(url)
diff --git a/libs/apprise/cli.py b/libs/apprise/cli.py
index 654e597b0..690530000 100644
--- a/libs/apprise/cli.py
+++ b/libs/apprise/cli.py
@@ -32,11 +32,13 @@ from os.path import expanduser
from os.path import expandvars
from . import NotifyType
+from . import NotifyFormat
from . import Apprise
from . import AppriseAsset
from . import AppriseConfig
from .utils import parse_list
from .common import NOTIFY_TYPES
+from .common import NOTIFY_FORMATS
from .logger import logger
from . import __title__
@@ -44,6 +46,10 @@ from . import __version__
from . import __license__
from . import __copywrite__
+# By default we allow looking 1 level down recursivly in Apprise configuration
+# files.
+DEFAULT_RECURSION_DEPTH = 1
+
# Defines our click context settings adding -h to the additional options that
# can be specified to get the help menu to come up
CONTEXT_SETTINGS = dict(help_option_names=['-h', '--help'])
@@ -101,12 +107,19 @@ def print_version_msg():
help='Specify one or more configuration locations.')
@click.option('--attach', '-a', default=None, type=str, multiple=True,
metavar='ATTACHMENT_URL',
- help='Specify one or more configuration locations.')
+ help='Specify one or more attachment.')
@click.option('--notification-type', '-n', default=NotifyType.INFO, type=str,
metavar='TYPE',
- help='Specify the message type (default=info). Possible values'
- ' are "{}", and "{}".'.format(
- '", "'.join(NOTIFY_TYPES[:-1]), NOTIFY_TYPES[-1]))
+ help='Specify the message type (default={}). '
+ 'Possible values are "{}", and "{}".'.format(
+ NotifyType.INFO, '", "'.join(NOTIFY_TYPES[:-1]),
+ NOTIFY_TYPES[-1]))
[email protected]('--input-format', '-i', default=NotifyFormat.TEXT, type=str,
+ metavar='FORMAT',
+ help='Specify the message input format (default={}). '
+ 'Possible values are "{}", and "{}".'.format(
+ NotifyFormat.TEXT, '", "'.join(NOTIFY_FORMATS[:-1]),
+ NOTIFY_FORMATS[-1]))
@click.option('--theme', '-T', default='default', type=str, metavar='THEME',
help='Specify the default theme.')
@click.option('--tag', '-g', default=None, type=str, multiple=True,
@@ -114,19 +127,28 @@ def print_version_msg():
'which services to notify. Use multiple --tag (-g) entries to '
'"OR" the tags together and comma separated to "AND" them. '
'If no tags are specified then all services are notified.')
[email protected]('--disable-async', '-Da', is_flag=True,
+ help='Send all notifications sequentially')
@click.option('--dry-run', '-d', is_flag=True,
help='Perform a trial run but only prints the notification '
'services to-be triggered to stdout. Notifications are never '
'sent using this mode.')
[email protected]('--recursion-depth', '-R', default=DEFAULT_RECURSION_DEPTH,
+ type=int,
+ help='The number of recursive import entries that can be '
+ 'loaded from within Apprise configuration. By default '
+ 'this is set to {}.'.format(DEFAULT_RECURSION_DEPTH))
@click.option('--verbose', '-v', count=True,
help='Makes the operation more talkative. Use multiple v to '
'increase the verbosity. I.e.: -vvvv')
[email protected]('--debug', '-D', is_flag=True, help='Debug mode')
@click.option('--version', '-V', is_flag=True,
help='Display the apprise version and exit.')
@click.argument('urls', nargs=-1,
metavar='SERVER_URL [SERVER_URL2 [SERVER_URL3]]',)
def main(body, title, config, attach, urls, notification_type, theme, tag,
- dry_run, verbose, version):
+ input_format, dry_run, recursion_depth, verbose, disable_async,
+ debug, version):
"""
Send a notification to all of the specified servers identified by their
URLs the content provided within the title, body and notification-type.
@@ -138,6 +160,11 @@ def main(body, title, config, attach, urls, notification_type, theme, tag,
# want to return a specific error code, you must call sys.exit()
# as you will see below.
+ debug = True if debug else False
+ if debug:
+ # Verbosity must be a minimum of 3
+ verbose = 3 if verbose < 3 else verbose
+
# Logging
ch = logging.StreamHandler(sys.stdout)
if verbose > 3:
@@ -166,21 +193,55 @@ def main(body, title, config, attach, urls, notification_type, theme, tag,
ch.setFormatter(formatter)
logger.addHandler(ch)
+ # Update our asyncio logger
+ asyncio_logger = logging.getLogger('asyncio')
+ for handler in logger.handlers:
+ asyncio_logger.addHandler(handler)
+ asyncio_logger.setLevel(logger.level)
+
if version:
print_version_msg()
sys.exit(0)
+ # Simple Error Checking
+ notification_type = notification_type.strip().lower()
+ if notification_type not in NOTIFY_TYPES:
+ logger.error(
+ 'The --notification-type (-n) value of {} is not supported.'
+ .format(notification_type))
+ # 2 is the same exit code returned by Click if there is a parameter
+ # issue. For consistency, we also return a 2
+ sys.exit(2)
+
+ input_format = input_format.strip().lower()
+ if input_format not in NOTIFY_FORMATS:
+ logger.error(
+ 'The --input-format (-i) value of {} is not supported.'
+ .format(input_format))
+ # 2 is the same exit code returned by Click if there is a parameter
+ # issue. For consistency, we also return a 2
+ sys.exit(2)
+
# Prepare our asset
- asset = AppriseAsset(theme=theme)
+ asset = AppriseAsset(
+ body_format=input_format,
+ theme=theme,
+ # Async mode is only used for Python v3+ and allows a user to send
+ # all of their notifications asyncronously. This was made an option
+ # incase there are problems in the future where it's better that
+ # everything run sequentially/syncronously instead.
+ async_mode=disable_async is not True,
+ )
- # Create our object
- a = Apprise(asset=asset)
+ # Create our Apprise object
+ a = Apprise(asset=asset, debug=debug)
# Load our configuration if no URLs or specified configuration was
# identified on the command line
a.add(AppriseConfig(
paths=[f for f in DEFAULT_SEARCH_PATHS if isfile(expanduser(f))]
- if not (config or urls) else config), asset=asset)
+ if not (config or urls) else config,
+ asset=asset, recursion=recursion_depth))
# Load our inventory up
for url in urls:
@@ -234,7 +295,10 @@ def main(body, title, config, attach, urls, notification_type, theme, tag,
# There were no notifications set. This is a result of just having
# empty configuration files and/or being to restrictive when filtering
# by specific tag(s)
- sys.exit(2)
+
+ # Exit code 3 is used since Click uses exit code 2 if there is an
+ # error with the parameters specified
+ sys.exit(3)
elif result is False:
# At least 1 notification service failed to send
diff --git a/libs/apprise/common.py b/libs/apprise/common.py
index 90c65744a..329b5d93f 100644
--- a/libs/apprise/common.py
+++ b/libs/apprise/common.py
@@ -31,15 +31,15 @@ class NotifyType(object):
"""
INFO = 'info'
SUCCESS = 'success'
- FAILURE = 'failure'
WARNING = 'warning'
+ FAILURE = 'failure'
NOTIFY_TYPES = (
NotifyType.INFO,
NotifyType.SUCCESS,
- NotifyType.FAILURE,
NotifyType.WARNING,
+ NotifyType.FAILURE,
)
@@ -129,6 +129,31 @@ CONFIG_FORMATS = (
ConfigFormat.YAML,
)
+
+class ConfigIncludeMode(object):
+ """
+ The different Cofiguration inclusion modes. All Configuration
+ plugins will have one of these associated with it.
+ """
+ # - Configuration inclusion of same type only; hence a file:// can include
+ # a file://
+ # - Cross file inclusion is not allowed unless insecure_includes (a flag)
+ # is set to True. In these cases STRICT acts as type ALWAYS
+ STRICT = 'strict'
+
+ # This configuration type can never be included
+ NEVER = 'never'
+
+ # File configuration can always be included
+ ALWAYS = 'always'
+
+
+CONFIG_INCLUDE_MODES = (
+ ConfigIncludeMode.STRICT,
+ ConfigIncludeMode.NEVER,
+ ConfigIncludeMode.ALWAYS,
+)
+
# This is a reserved tag that is automatically assigned to every
# Notification Plugin
MATCH_ALL_TAG = 'all'
diff --git a/libs/apprise/config/ConfigBase.py b/libs/apprise/config/ConfigBase.py
index 8cd40813d..22efd8e29 100644
--- a/libs/apprise/config/ConfigBase.py
+++ b/libs/apprise/config/ConfigBase.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
#
-# Copyright (C) 2019 Chris Caron <[email protected]>
+# Copyright (C) 2020 Chris Caron <[email protected]>
# All rights reserved.
#
# This code is licensed under the MIT License.
@@ -34,9 +34,12 @@ from ..AppriseAsset import AppriseAsset
from ..URLBase import URLBase
from ..common import ConfigFormat
from ..common import CONFIG_FORMATS
+from ..common import ConfigIncludeMode
from ..utils import GET_SCHEMA_RE
from ..utils import parse_list
from ..utils import parse_bool
+from ..utils import parse_urls
+from . import SCHEMA_MAP
class ConfigBase(URLBase):
@@ -60,7 +63,15 @@ class ConfigBase(URLBase):
# anything else. 128KB (131072B)
max_buffer_size = 131072
- def __init__(self, cache=True, **kwargs):
+ # By default all configuration is not includable using the 'include'
+ # line found in configuration files.
+ allow_cross_includes = ConfigIncludeMode.NEVER
+
+ # the config path manages the handling of relative include
+ config_path = os.getcwd()
+
+ def __init__(self, cache=True, recursion=0, insecure_includes=False,
+ **kwargs):
"""
Initialize some general logging and common server arguments that will
keep things consistent when working with the configurations that
@@ -76,6 +87,29 @@ class ConfigBase(URLBase):
You can alternatively set the cache value to an int identifying the
number of seconds the previously retrieved can exist for before it
should be considered expired.
+
+ recursion defines how deep we recursively handle entries that use the
+ `include` keyword. This keyword requires us to fetch more configuration
+ from another source and add it to our existing compilation. If the
+ file we remotely retrieve also has an `include` reference, we will only
+ advance through it if recursion is set to 2 deep. If set to zero
+ it is off. There is no limit to how high you set this value. It would
+ be recommended to keep it low if you do intend to use it.
+
+ insecure_include by default are disabled. When set to True, all
+ Apprise Config files marked to be in STRICT mode are treated as being
+ in ALWAYS mode.
+
+ Take a file:// based configuration for example, only a file:// based
+ configuration can include another file:// based one. because it is set
+ to STRICT mode. If an http:// based configuration file attempted to
+ include a file:// one it woul fail. However this include would be
+ possible if insecure_includes is set to True.
+
+ There are cases where a self hosting apprise developer may wish to load
+ configuration from memory (in a string format) that contains 'include'
+ entries (even file:// based ones). In these circumstances if you want
+ these 'include' entries to be honored, this value must be set to True.
"""
super(ConfigBase, self).__init__(**kwargs)
@@ -88,6 +122,12 @@ class ConfigBase(URLBase):
# Tracks previously loaded content for speed
self._cached_servers = None
+ # Initialize our recursion value
+ self.recursion = recursion
+
+ # Initialize our insecure_includes flag
+ self.insecure_includes = insecure_includes
+
if 'encoding' in kwargs:
# Store the encoding
self.encoding = kwargs.get('encoding')
@@ -154,15 +194,110 @@ class ConfigBase(URLBase):
# Dynamically load our parse_ function based on our config format
fn = getattr(ConfigBase, 'config_parse_{}'.format(config_format))
- # Execute our config parse function which always returns a list
- self._cached_servers.extend(fn(content=content, asset=asset))
+ # Initialize our asset object
+ asset = asset if isinstance(asset, AppriseAsset) else self.asset
+
+ # Execute our config parse function which always returns a tuple
+ # of our servers and our configuration
+ servers, configs = fn(content=content, asset=asset)
+ self._cached_servers.extend(servers)
+
+ # Configuration files were detected; recursively populate them
+ # If we have been configured to do so
+ for url in configs:
+ if self.recursion > 0:
+
+ # Attempt to acquire the schema at the very least to allow
+ # our configuration based urls.
+ schema = GET_SCHEMA_RE.match(url)
+ if schema is None:
+ # Plan B is to assume we're dealing with a file
+ schema = 'file'
+ if not os.path.isabs(url):
+ # We're dealing with a relative path; prepend
+ # our current config path
+ url = os.path.join(self.config_path, url)
+
+ url = '{}://{}'.format(schema, URLBase.quote(url))
+ else:
+ # Ensure our schema is always in lower case
+ schema = schema.group('schema').lower()
+
+ # Some basic validation
+ if schema not in SCHEMA_MAP:
+ ConfigBase.logger.warning(
+ 'Unsupported include schema {}.'.format(schema))
+ continue
+
+ # Parse our url details of the server object as dictionary
+ # containing all of the information parsed from our URL
+ results = SCHEMA_MAP[schema].parse_url(url)
+ if not results:
+ # Failed to parse the server URL
+ self.logger.warning(
+ 'Unparseable include URL {}'.format(url))
+ continue
+
+ # Handle cross inclusion based on allow_cross_includes rules
+ if (SCHEMA_MAP[schema].allow_cross_includes ==
+ ConfigIncludeMode.STRICT
+ and schema not in self.schemas()
+ and not self.insecure_includes) or \
+ SCHEMA_MAP[schema].allow_cross_includes == \
+ ConfigIncludeMode.NEVER:
+
+ # Prevent the loading if insecure base protocols
+ ConfigBase.logger.warning(
+ 'Including {}:// based configuration is prohibited. '
+ 'Ignoring URL {}'.format(schema, url))
+ continue
+
+ # Prepare our Asset Object
+ results['asset'] = asset
+
+ # No cache is required because we're just lumping this in
+ # and associating it with the cache value we've already
+ # declared (prior to our recursion)
+ results['cache'] = False
+
+ # Recursion can never be parsed from the URL; we decrement
+ # it one level
+ results['recursion'] = self.recursion - 1
+
+ # Insecure Includes flag can never be parsed from the URL
+ results['insecure_includes'] = self.insecure_includes
+
+ try:
+ # Attempt to create an instance of our plugin using the
+ # parsed URL information
+ cfg_plugin = SCHEMA_MAP[results['schema']](**results)
- if len(self._cached_servers):
+ except Exception as e:
+ # the arguments are invalid or can not be used.
+ self.logger.warning(
+ 'Could not load include URL: {}'.format(url))
+ self.logger.debug('Loading Exception: {}'.format(str(e)))
+ continue
+
+ # if we reach here, we can now add this servers found
+ # in this configuration file to our list
+ self._cached_servers.extend(
+ cfg_plugin.servers(asset=asset))
+
+ # We no longer need our configuration object
+ del cfg_plugin
+
+ else:
+ self.logger.debug(
+ 'Recursion limit reached; ignoring Include URL: %s' % url)
+
+ if self._cached_servers:
self.logger.info('Loaded {} entries from {}'.format(
len(self._cached_servers), self.url()))
else:
- self.logger.warning('Failed to load configuration from {}'.format(
- self.url()))
+ self.logger.warning(
+ 'Failed to load Apprise configuration from {}'.format(
+ self.url()))
# Set the time our content was cached at
self._cached_time = time.time()
@@ -282,7 +417,8 @@ class ConfigBase(URLBase):
except TypeError:
# content was not expected string type
- ConfigBase.logger.error('Invalid apprise config specified')
+ ConfigBase.logger.error(
+ 'Invalid Apprise configuration specified.')
return None
# By default set our return value to None since we don't know
@@ -297,7 +433,7 @@ class ConfigBase(URLBase):
if not result:
# Invalid syntax
ConfigBase.logger.error(
- 'Undetectable apprise configuration found '
+ 'Undetectable Apprise configuration found '
'based on line {}.'.format(line))
# Take an early exit
return None
@@ -338,14 +474,14 @@ class ConfigBase(URLBase):
if not config_format:
# We couldn't detect configuration
ConfigBase.logger.error('Could not detect configuration')
- return list()
+ return (list(), list())
if config_format not in CONFIG_FORMATS:
# Invalid configuration type specified
ConfigBase.logger.error(
'An invalid configuration format ({}) was specified'.format(
config_format))
- return list()
+ return (list(), list())
# Dynamically load our parse_ function based on our config format
fn = getattr(ConfigBase, 'config_parse_{}'.format(config_format))
@@ -357,9 +493,14 @@ class ConfigBase(URLBase):
def config_parse_text(content, asset=None):
"""
Parse the specified content as though it were a simple text file only
- containing a list of URLs. Return a list of loaded notification plugins
+ containing a list of URLs.
- Optionally associate an asset with the notification.
+ Return a tuple that looks like (servers, configs) where:
+ - servers contains a list of loaded notification plugins
+ - configs contains a list of additional configuration files
+ referenced.
+
+ You may also optionally associate an asset with the notification.
The file syntax is:
@@ -373,14 +514,25 @@ class ConfigBase(URLBase):
# Or you can use this format (no tags associated)
<URL>
+ # you can also use the keyword 'include' and identify a
+ # configuration location (like this file) which will be included
+ # as additional configuration entries when loaded.
+ include <ConfigURL>
+
"""
- response = list()
+ # A list of loaded Notification Services
+ servers = list()
+
+ # A list of additional configuration files referenced using
+ # the include keyword
+ configs = list()
# Define what a valid line should look like
valid_line_re = re.compile(
r'^\s*(?P<line>([;#]+(?P<comment>.*))|'
r'(\s*(?P<tags>[^=]+)=|=)?\s*'
- r'(?P<url>[a-z0-9]{2,9}://.*))?$', re.I)
+ r'(?P<url>[a-z0-9]{2,9}://.*)|'
+ r'include\s+(?P<config>.+))?\s*$', re.I)
try:
# split our content up to read line by line
@@ -388,28 +540,35 @@ class ConfigBase(URLBase):
except TypeError:
# content was not expected string type
- ConfigBase.logger.error('Invalid apprise text data specified')
- return list()
+ ConfigBase.logger.error(
+ 'Invalid Apprise TEXT based configuration specified.')
+ return (list(), list())
for line, entry in enumerate(content, start=1):
result = valid_line_re.match(entry)
if not result:
# Invalid syntax
ConfigBase.logger.error(
- 'Invalid apprise text format found '
+ 'Invalid Apprise TEXT configuration format found '
'{} on line {}.'.format(entry, line))
# Assume this is a file we shouldn't be parsing. It's owner
# can read the error printed to screen and take action
# otherwise.
- return list()
+ return (list(), list())
- # Store our url read in
- url = result.group('url')
- if not url:
+ url, config = result.group('url'), result.group('config')
+ if not (url or config):
# Comment/empty line; do nothing
continue
+ if config:
+ ConfigBase.logger.debug('Include URL: {}'.format(config))
+
+ # Store our include line
+ configs.append(config.strip())
+ continue
+
# Acquire our url tokens
results = plugins.url_to_dict(url)
if results is None:
@@ -422,11 +581,6 @@ class ConfigBase(URLBase):
# notifications if any were set
results['tag'] = set(parse_list(result.group('tags')))
- ConfigBase.logger.trace(
- 'URL {} unpacked as:{}{}'.format(
- url, os.linesep, os.linesep.join(
- ['{}="{}"'.format(k, v) for k, v in results.items()])))
-
# Prepare our Asset Object
results['asset'] = \
asset if isinstance(asset, AppriseAsset) else AppriseAsset()
@@ -448,23 +602,32 @@ class ConfigBase(URLBase):
continue
# if we reach here, we successfully loaded our data
- response.append(plugin)
+ servers.append(plugin)
# Return what was loaded
- return response
+ return (servers, configs)
@staticmethod
def config_parse_yaml(content, asset=None):
"""
Parse the specified content as though it were a yaml file
- specifically formatted for apprise. Return a list of loaded
- notification plugins.
+ specifically formatted for Apprise.
+
+ Return a tuple that looks like (servers, configs) where:
+ - servers contains a list of loaded notification plugins
+ - configs contains a list of additional configuration files
+ referenced.
- Optionally associate an asset with the notification.
+ You may optionally associate an asset with the notification.
"""
- response = list()
+ # A list of loaded Notification Services
+ servers = list()
+
+ # A list of additional configuration files referenced using
+ # the include keyword
+ configs = list()
try:
# Load our data (safely)
@@ -473,23 +636,24 @@ class ConfigBase(URLBase):
except (AttributeError, yaml.error.MarkedYAMLError) as e:
# Invalid content
ConfigBase.logger.error(
- 'Invalid apprise yaml data specified.')
+ 'Invalid Apprise YAML data specified.')
ConfigBase.logger.debug(
'YAML Exception:{}{}'.format(os.linesep, e))
- return list()
+ return (list(), list())
if not isinstance(result, dict):
# Invalid content
- ConfigBase.logger.error('Invalid apprise yaml structure specified')
- return list()
+ ConfigBase.logger.error(
+ 'Invalid Apprise YAML based configuration specified.')
+ return (list(), list())
# YAML Version
version = result.get('version', 1)
if version != 1:
# Invalid syntax
ConfigBase.logger.error(
- 'Invalid apprise yaml version specified {}.'.format(version))
- return list()
+ 'Invalid Apprise YAML version specified {}.'.format(version))
+ return (list(), list())
#
# global asset object
@@ -537,14 +701,37 @@ class ConfigBase(URLBase):
global_tags = set(parse_list(tags))
#
+ # include root directive
+ #
+ includes = result.get('include', None)
+ if isinstance(includes, six.string_types):
+ # Support a single inline string or multiple ones separated by a
+ # comma and/or space
+ includes = parse_urls(includes)
+
+ elif not isinstance(includes, (list, tuple)):
+ # Not a problem; we simply have no includes
+ includes = list()
+
+ # Iterate over each config URL
+ for no, url in enumerate(includes):
+
+ if isinstance(url, six.string_types):
+ # Support a single inline string or multiple ones separated by
+ # a comma and/or space
+ configs.extend(parse_urls(url))
+
+ elif isinstance(url, dict):
+ # Store the url and ignore arguments associated
+ configs.extend(u for u in url.keys())
+
+ #
# urls root directive
#
urls = result.get('urls', None)
if not isinstance(urls, (list, tuple)):
- # Unsupported
- ConfigBase.logger.error(
- 'Missing "urls" directive in apprise yaml.')
- return list()
+ # Not a problem; we simply have no urls
+ urls = list()
# Iterate over each URL
for no, url in enumerate(urls):
@@ -656,7 +843,7 @@ class ConfigBase(URLBase):
else:
# Unsupported
ConfigBase.logger.warning(
- 'Unsupported apprise yaml entry #{}'.format(no + 1))
+ 'Unsupported Apprise YAML entry #{}'.format(no + 1))
continue
# Track our entries
@@ -669,7 +856,7 @@ class ConfigBase(URLBase):
# Grab our first item
_results = results.pop(0)
- # tag is a special keyword that is managed by apprise object.
+ # tag is a special keyword that is managed by Apprise object.
# The below ensures our tags are set correctly
if 'tag' in _results:
# Tidy our list up
@@ -698,17 +885,19 @@ class ConfigBase(URLBase):
ConfigBase.logger.debug(
'Loaded URL: {}'.format(plugin.url()))
- except Exception:
+ except Exception as e:
# the arguments are invalid or can not be used.
ConfigBase.logger.warning(
- 'Could not load apprise yaml entry #{}, item #{}'
+ 'Could not load Apprise YAML configuration '
+ 'entry #{}, item #{}'
.format(no + 1, entry))
+ ConfigBase.logger.debug('Loading Exception: %s' % str(e))
continue
# if we reach here, we successfully loaded our data
- response.append(plugin)
+ servers.append(plugin)
- return response
+ return (servers, configs)
def pop(self, index=-1):
"""
diff --git a/libs/apprise/config/ConfigFile.py b/libs/apprise/config/ConfigFile.py
index 917eea081..9f8102253 100644
--- a/libs/apprise/config/ConfigFile.py
+++ b/libs/apprise/config/ConfigFile.py
@@ -28,6 +28,7 @@ import io
import os
from .ConfigBase import ConfigBase
from ..common import ConfigFormat
+from ..common import ConfigIncludeMode
from ..AppriseLocale import gettext_lazy as _
@@ -42,6 +43,9 @@ class ConfigFile(ConfigBase):
# The default protocol
protocol = 'file'
+ # Configuration file inclusion can only be of the same type
+ allow_cross_includes = ConfigIncludeMode.STRICT
+
def __init__(self, path, **kwargs):
"""
Initialize File Object
@@ -53,7 +57,10 @@ class ConfigFile(ConfigBase):
super(ConfigFile, self).__init__(**kwargs)
# Store our file path as it was set
- self.path = os.path.expanduser(path)
+ self.path = os.path.abspath(os.path.expanduser(path))
+
+ # Update the config path to be relative to our file we just loaded
+ self.config_path = os.path.dirname(self.path)
return
@@ -69,19 +76,19 @@ class ConfigFile(ConfigBase):
else:
cache = int(self.cache)
- # Define any arguments set
- args = {
+ # Define any URL parameters
+ params = {
'encoding': self.encoding,
'cache': cache,
}
if self.config_format:
# A format was enforced; make sure it's passed back with the url
- args['format'] = self.config_format
+ params['format'] = self.config_format
- return 'file://{path}{args}'.format(
+ return 'file://{path}{params}'.format(
path=self.quote(self.path),
- args='?{}'.format(self.urlencode(args)) if args else '',
+ params='?{}'.format(self.urlencode(params)) if params else '',
)
def read(self, **kwargs):
@@ -91,10 +98,9 @@ class ConfigFile(ConfigBase):
response = None
- path = os.path.expanduser(self.path)
try:
if self.max_buffer_size > 0 and \
- os.path.getsize(path) > self.max_buffer_size:
+ os.path.getsize(self.path) > self.max_buffer_size:
# Content exceeds maximum buffer size
self.logger.error(
@@ -106,7 +112,7 @@ class ConfigFile(ConfigBase):
# getsize() can throw this acception if the file is missing
# and or simply isn't accessible
self.logger.error(
- 'File is not accessible: {}'.format(path))
+ 'File is not accessible: {}'.format(self.path))
return None
# Always call throttle before any server i/o is made
@@ -115,7 +121,7 @@ class ConfigFile(ConfigBase):
try:
# Python 3 just supports open(), however to remain compatible with
# Python 2, we use the io module
- with io.open(path, "rt", encoding=self.encoding) as f:
+ with io.open(self.path, "rt", encoding=self.encoding) as f:
# Store our content for parsing
response = f.read()
@@ -126,7 +132,7 @@ class ConfigFile(ConfigBase):
self.logger.error(
'File not using expected encoding ({}) : {}'.format(
- self.encoding, path))
+ self.encoding, self.path))
return None
except (IOError, OSError):
@@ -136,13 +142,13 @@ class ConfigFile(ConfigBase):
# Could not open and/or read the file; this is not a problem since
# we scan a lot of default paths.
self.logger.error(
- 'File can not be opened for read: {}'.format(path))
+ 'File can not be opened for read: {}'.format(self.path))
return None
# Detect config format based on file extension if it isn't already
# enforced
if self.config_format is None and \
- re.match(r'^.*\.ya?ml\s*$', path, re.I) is not None:
+ re.match(r'^.*\.ya?ml\s*$', self.path, re.I) is not None:
# YAML Filename Detected
self.default_config_format = ConfigFormat.YAML
@@ -163,7 +169,7 @@ class ConfigFile(ConfigBase):
# We're done early; it's not a good URL
return results
- match = re.match(r'file://(?P<path>[^?]+)(\?.*)?', url, re.I)
+ match = re.match(r'[a-z0-9]+://(?P<path>[^?]+)(\?.*)?', url, re.I)
if not match:
return None
diff --git a/libs/apprise/config/ConfigHTTP.py b/libs/apprise/config/ConfigHTTP.py
index 299255d09..c4ad29425 100644
--- a/libs/apprise/config/ConfigHTTP.py
+++ b/libs/apprise/config/ConfigHTTP.py
@@ -28,6 +28,7 @@ import six
import requests
from .ConfigBase import ConfigBase
from ..common import ConfigFormat
+from ..common import ConfigIncludeMode
from ..URLBase import PrivacyMode
from ..AppriseLocale import gettext_lazy as _
@@ -58,16 +59,15 @@ class ConfigHTTP(ConfigBase):
# The default secure protocol
secure_protocol = 'https'
- # The maximum number of seconds to wait for a connection to be established
- # before out-right just giving up
- connection_timeout_sec = 5.0
-
# If an HTTP error occurs, define the number of characters you still want
# to read back. This is useful for debugging purposes, but nothing else.
# The idea behind enforcing this kind of restriction is to prevent abuse
# from queries to services that may be untrusted.
max_error_buffer_size = 2048
+ # Configuration file inclusion can always include this type
+ allow_cross_includes = ConfigIncludeMode.ALWAYS
+
def __init__(self, headers=None, **kwargs):
"""
Initialize HTTP Object
@@ -104,18 +104,20 @@ class ConfigHTTP(ConfigBase):
cache = int(self.cache)
# Define any arguments set
- args = {
- 'verify': 'yes' if self.verify_certificate else 'no',
+ params = {
'encoding': self.encoding,
'cache': cache,
}
+ # Extend our parameters
+ params.update(self.url_parameters(privacy=privacy, *args, **kwargs))
+
if self.config_format:
# A format was enforced; make sure it's passed back with the url
- args['format'] = self.config_format
+ params['format'] = self.config_format
# Append our headers into our args
- args.update({'+{}'.format(k): v for k, v in self.headers.items()})
+ params.update({'+{}'.format(k): v for k, v in self.headers.items()})
# Determine Authentication
auth = ''
@@ -132,14 +134,14 @@ class ConfigHTTP(ConfigBase):
default_port = 443 if self.secure else 80
- return '{schema}://{auth}{hostname}{port}{fullpath}/?{args}'.format(
+ return '{schema}://{auth}{hostname}{port}{fullpath}/?{params}'.format(
schema=self.secure_protocol if self.secure else self.protocol,
auth=auth,
hostname=self.quote(self.host, safe=''),
port='' if self.port is None or self.port == default_port
else ':{}'.format(self.port),
fullpath=self.quote(self.fullpath, safe='/'),
- args=self.urlencode(args),
+ params=self.urlencode(params),
)
def read(self, **kwargs):
@@ -185,7 +187,7 @@ class ConfigHTTP(ConfigBase):
headers=headers,
auth=auth,
verify=self.verify_certificate,
- timeout=self.connection_timeout_sec,
+ timeout=self.request_timeout,
stream=True) as r:
# Handle Errors
@@ -211,7 +213,7 @@ class ConfigHTTP(ConfigBase):
return None
# Store our result (but no more than our buffer length)
- response = r.content[:self.max_buffer_size + 1]
+ response = r.text[:self.max_buffer_size + 1]
# Verify that our content did not exceed the buffer size:
if len(response) > self.max_buffer_size:
@@ -240,7 +242,7 @@ class ConfigHTTP(ConfigBase):
except requests.RequestException as e:
self.logger.error(
- 'A Connection error occured retrieving HTTP '
+ 'A Connection error occurred retrieving HTTP '
'configuration from %s.' % self.host)
self.logger.debug('Socket Exception: %s' % str(e))
@@ -254,7 +256,7 @@ class ConfigHTTP(ConfigBase):
def parse_url(url):
"""
Parses the URL and returns enough arguments that can allow
- us to substantiate this object.
+ us to re-instantiate this object.
"""
results = ConfigBase.parse_url(url)
diff --git a/libs/apprise/config/__init__.py b/libs/apprise/config/__init__.py
index 5c3980318..353123e9a 100644
--- a/libs/apprise/config/__init__.py
+++ b/libs/apprise/config/__init__.py
@@ -23,12 +23,12 @@
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
-import six
import re
-
+import six
from os import listdir
from os.path import dirname
from os.path import abspath
+from ..logger import logger
# Maintains a mapping of all of the configuration services
SCHEMA_MAP = {}
@@ -88,29 +88,39 @@ def __load_matrix(path=abspath(dirname(__file__)), name='apprise.config'):
# not the module:
globals()[plugin_name] = plugin
- # Load protocol(s) if defined
- proto = getattr(plugin, 'protocol', None)
- if isinstance(proto, six.string_types):
- if proto not in SCHEMA_MAP:
- SCHEMA_MAP[proto] = plugin
-
- elif isinstance(proto, (set, list, tuple)):
- # Support iterables list types
- for p in proto:
- if p not in SCHEMA_MAP:
- SCHEMA_MAP[p] = plugin
-
- # Load secure protocol(s) if defined
- protos = getattr(plugin, 'secure_protocol', None)
- if isinstance(protos, six.string_types):
- if protos not in SCHEMA_MAP:
- SCHEMA_MAP[protos] = plugin
-
- if isinstance(protos, (set, list, tuple)):
- # Support iterables list types
- for p in protos:
- if p not in SCHEMA_MAP:
- SCHEMA_MAP[p] = plugin
+ fn = getattr(plugin, 'schemas', None)
+ try:
+ schemas = set([]) if not callable(fn) else fn(plugin)
+
+ except TypeError:
+ # Python v2.x support where functions associated with classes
+ # were considered bound to them and could not be called prior
+ # to the classes initialization. This code can be dropped
+ # once Python v2.x support is dropped. The below code introduces
+ # replication as it already exists and is tested in
+ # URLBase.schemas()
+ schemas = set([])
+ for key in ('protocol', 'secure_protocol'):
+ schema = getattr(plugin, key, None)
+ if isinstance(schema, six.string_types):
+ schemas.add(schema)
+
+ elif isinstance(schema, (set, list, tuple)):
+ # Support iterables list types
+ for s in schema:
+ if isinstance(s, six.string_types):
+ schemas.add(s)
+
+ # map our schema to our plugin
+ for schema in schemas:
+ if schema in SCHEMA_MAP:
+ logger.error(
+ "Config schema ({}) mismatch detected - {} to {}"
+ .format(schema, SCHEMA_MAP[schema], plugin))
+ continue
+
+ # Assign plugin
+ SCHEMA_MAP[schema] = plugin
return SCHEMA_MAP
diff --git a/libs/apprise/i18n/apprise.pot b/libs/apprise/i18n/apprise.pot
index ea3fdfad1..2a0dc5e0a 100644
--- a/libs/apprise/i18n/apprise.pot
+++ b/libs/apprise/i18n/apprise.pot
@@ -6,16 +6,16 @@
#, fuzzy
msgid ""
msgstr ""
-"Project-Id-Version: apprise 0.8.5\n"
+"Project-Id-Version: apprise 0.8.8\n"
"Report-Msgid-Bugs-To: [email protected]\n"
-"POT-Creation-Date: 2020-03-30 16:00-0400\n"
+"POT-Creation-Date: 2020-09-02 07:46-0400\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <[email protected]>\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
-"Generated-By: Babel 2.8.0\n"
+"Generated-By: Babel 2.7.0\n"
msgid "API Key"
msgstr ""
@@ -35,6 +35,9 @@ msgstr ""
msgid "Access Token"
msgstr ""
+msgid "Account Email"
+msgstr ""
+
msgid "Account SID"
msgstr ""
@@ -59,6 +62,9 @@ msgstr ""
msgid "Avatar Image"
msgstr ""
+msgid "Avatar URL"
+msgstr ""
+
msgid "Batch Mode"
msgstr ""
@@ -83,6 +89,12 @@ msgstr ""
msgid "Channels"
msgstr ""
+msgid "Client ID"
+msgstr ""
+
+msgid "Client Secret"
+msgstr ""
+
msgid "Consumer Key"
msgstr ""
@@ -92,9 +104,18 @@ msgstr ""
msgid "Country"
msgstr ""
+msgid "Custom Icon"
+msgstr ""
+
+msgid "Cycles"
+msgstr ""
+
msgid "Detect Bot Owner"
msgstr ""
+msgid "Device API Key"
+msgstr ""
+
msgid "Device ID"
msgstr ""
@@ -161,6 +182,9 @@ msgstr ""
msgid "IRC Colors"
msgstr ""
+msgid "Icon Type"
+msgstr ""
+
msgid "Include Footer"
msgstr ""
@@ -188,6 +212,9 @@ msgstr ""
msgid "Modal"
msgstr ""
+msgid "Mode"
+msgstr ""
+
msgid "Notify Format"
msgstr ""
@@ -269,6 +296,12 @@ msgstr ""
msgid "Server Timeout"
msgstr ""
+msgid "Socket Connect Timeout"
+msgstr ""
+
+msgid "Socket Read Timeout"
+msgstr ""
+
msgid "Sound"
msgstr ""
@@ -281,6 +314,12 @@ msgstr ""
msgid "Source Phone No"
msgstr ""
+msgid "Sticky"
+msgstr ""
+
+msgid "Subtitle"
+msgstr ""
+
msgid "Target Channel"
msgstr ""
@@ -338,6 +377,9 @@ msgstr ""
msgid "Template Data"
msgstr ""
+msgid "Tenant Domain"
+msgstr ""
+
msgid "Text To Speech"
msgstr ""
@@ -368,6 +410,9 @@ msgstr ""
msgid "Use Avatar"
msgstr ""
+msgid "User ID"
+msgstr ""
+
msgid "User Key"
msgstr ""
diff --git a/libs/apprise/plugins/NotifyBase.py b/libs/apprise/plugins/NotifyBase.py
index 7e963b0ce..3a0538bcc 100644
--- a/libs/apprise/plugins/NotifyBase.py
+++ b/libs/apprise/plugins/NotifyBase.py
@@ -24,6 +24,7 @@
# THE SOFTWARE.
import re
+import six
from ..URLBase import URLBase
from ..common import NotifyType
@@ -36,7 +37,17 @@ from ..AppriseLocale import gettext_lazy as _
from ..AppriseAttachment import AppriseAttachment
-class NotifyBase(URLBase):
+if six.PY3:
+ # Wrap our base with the asyncio wrapper
+ from ..py3compat.asyncio import AsyncNotifyBase
+ BASE_OBJECT = AsyncNotifyBase
+
+else:
+ # Python v2.7 (backwards compatibility)
+ BASE_OBJECT = URLBase
+
+
+class NotifyBase(BASE_OBJECT):
"""
This is the base class for all notification services
"""
@@ -80,21 +91,11 @@ class NotifyBase(URLBase):
# use a <b> tag. The below causes the <b>title</b> to get generated:
default_html_tag_id = 'b'
- # Define a default set of template arguments used for dynamically building
- # details about our individual plugins for developers.
-
- # Define object templates
- templates = ()
-
- # Provides a mapping of tokens, certain entries are fixed and automatically
- # configured if found (such as schema, host, user, pass, and port)
- template_tokens = {}
-
# Here is where we define all of the arguments we accept on the url
# such as: schema://whatever/?overflow=upstream&format=text
# These act the same way as tokens except they are optional and/or
# have default values set if mandatory. This rule must be followed
- template_args = {
+ template_args = dict(URLBase.template_args, **{
'overflow': {
'name': _('Overflow Mode'),
'type': 'choice:string',
@@ -119,34 +120,7 @@ class NotifyBase(URLBase):
# runtime.
'_lookup_default': 'notify_format',
},
- 'verify': {
- 'name': _('Verify SSL'),
- # SSL Certificate Authority Verification
- 'type': 'bool',
- # Provide a default
- 'default': URLBase.verify_certificate,
- # look up default using the following parent class value at
- # runtime.
- '_lookup_default': 'verify_certificate',
- },
- }
-
- # kwargs are dynamically built because a prefix causes us to parse the
- # content slightly differently. The prefix is required and can be either
- # a (+ or -). Below would handle the +key=value:
- # {
- # 'headers': {
- # 'name': _('HTTP Header'),
- # 'prefix': '+',
- # 'type': 'string',
- # },
- # },
- #
- # In a kwarg situation, the 'key' is always presumed to be treated as
- # a string. When the 'type' is defined, it is being defined to respect
- # the 'value'.
-
- template_kwargs = {}
+ })
def __init__(self, **kwargs):
"""
@@ -161,7 +135,7 @@ class NotifyBase(URLBase):
# Store the specified format if specified
notify_format = kwargs.get('format', '')
if notify_format.lower() not in NOTIFY_FORMATS:
- msg = 'Invalid notification format %s'.format(notify_format)
+ msg = 'Invalid notification format {}'.format(notify_format)
self.logger.error(msg)
raise TypeError(msg)
@@ -368,6 +342,23 @@ class NotifyBase(URLBase):
raise NotImplementedError(
"send() is not implimented by the child class.")
+ def url_parameters(self, *args, **kwargs):
+ """
+ Provides a default set of parameters to work with. This can greatly
+ simplify URL construction in the acommpanied url() function in all
+ defined plugin services.
+ """
+
+ params = {
+ 'format': self.notify_format,
+ 'overflow': self.overflow_mode,
+ }
+
+ params.update(super(NotifyBase, self).url_parameters(*args, **kwargs))
+
+ # return default parameters
+ return params
+
@staticmethod
def parse_url(url, verify_host=True):
"""Parses the URL and returns it broken apart into a dictionary.
diff --git a/libs/apprise/plugins/NotifyBoxcar.py b/libs/apprise/plugins/NotifyBoxcar.py
index 341c5098c..04b43b194 100644
--- a/libs/apprise/plugins/NotifyBoxcar.py
+++ b/libs/apprise/plugins/NotifyBoxcar.py
@@ -279,6 +279,7 @@ class NotifyBoxcar(NotifyBase):
data=dumps(payload),
headers=headers,
verify=self.verify_certificate,
+ timeout=self.request_timeout,
)
# Boxcar returns 201 (Created) when successful
@@ -304,7 +305,7 @@ class NotifyBoxcar(NotifyBase):
except requests.RequestException as e:
self.logger.warning(
- 'A Connection error occured sending Boxcar '
+ 'A Connection error occurred sending Boxcar '
'notification to %s.' % (host))
self.logger.debug('Socket Exception: %s' % str(e))
@@ -319,15 +320,15 @@ class NotifyBoxcar(NotifyBase):
Returns the URL built dynamically based on specified arguments.
"""
- # Define any arguments set
- args = {
- 'format': self.notify_format,
- 'overflow': self.overflow_mode,
+ # Define any URL parameters
+ params = {
'image': 'yes' if self.include_image else 'no',
- 'verify': 'yes' if self.verify_certificate else 'no',
}
- return '{schema}://{access}/{secret}/{targets}?{args}'.format(
+ # Extend our parameters
+ params.update(self.url_parameters(privacy=privacy, *args, **kwargs))
+
+ return '{schema}://{access}/{secret}/{targets}?{params}'.format(
schema=self.secure_protocol,
access=self.pprint(self.access, privacy, safe=''),
secret=self.pprint(
@@ -335,7 +336,7 @@ class NotifyBoxcar(NotifyBase):
targets='/'.join([
NotifyBoxcar.quote(x, safe='') for x in chain(
self.tags, self.device_tokens) if x != DEFAULT_TAG]),
- args=NotifyBoxcar.urlencode(args),
+ params=NotifyBoxcar.urlencode(params),
)
@staticmethod
@@ -345,7 +346,6 @@ class NotifyBoxcar(NotifyBase):
"""
results = NotifyBase.parse_url(url, verify_host=False)
-
if not results:
# We're done early
return None
diff --git a/libs/apprise/plugins/NotifyClickSend.py b/libs/apprise/plugins/NotifyClickSend.py
index 4bc36dc9c..a7d89c18b 100644
--- a/libs/apprise/plugins/NotifyClickSend.py
+++ b/libs/apprise/plugins/NotifyClickSend.py
@@ -221,6 +221,7 @@ class NotifyClickSend(NotifyBase):
data=dumps(payload),
headers=headers,
verify=self.verify_certificate,
+ timeout=self.request_timeout,
)
if r.status_code != requests.codes.ok:
# We had a problem
@@ -256,7 +257,7 @@ class NotifyClickSend(NotifyBase):
except requests.RequestException as e:
self.logger.warning(
- 'A Connection error occured sending {} ClickSend '
+ 'A Connection error occurred sending {} ClickSend '
'notification(s).'.format(len(payload['messages'])))
self.logger.debug('Socket Exception: %s' % str(e))
@@ -271,14 +272,14 @@ class NotifyClickSend(NotifyBase):
Returns the URL built dynamically based on specified arguments.
"""
- # Define any arguments set
- args = {
- 'format': self.notify_format,
- 'overflow': self.overflow_mode,
- 'verify': 'yes' if self.verify_certificate else 'no',
+ # Define any URL parameters
+ params = {
'batch': 'yes' if self.batch else 'no',
}
+ # Extend our parameters
+ params.update(self.url_parameters(privacy=privacy, *args, **kwargs))
+
# Setup Authentication
auth = '{user}:{password}@'.format(
user=NotifyClickSend.quote(self.user, safe=''),
@@ -286,19 +287,19 @@ class NotifyClickSend(NotifyBase):
self.password, privacy, mode=PrivacyMode.Secret, safe=''),
)
- return '{schema}://{auth}{targets}?{args}'.format(
+ return '{schema}://{auth}{targets}?{params}'.format(
schema=self.secure_protocol,
auth=auth,
targets='/'.join(
[NotifyClickSend.quote(x, safe='') for x in self.targets]),
- args=NotifyClickSend.urlencode(args),
+ params=NotifyClickSend.urlencode(params),
)
@staticmethod
def parse_url(url):
"""
Parses the URL and returns enough arguments that can allow
- us to substantiate this object.
+ us to re-instantiate this object.
"""
results = NotifyBase.parse_url(url, verify_host=False)
diff --git a/libs/apprise/plugins/NotifyD7Networks.py b/libs/apprise/plugins/NotifyD7Networks.py
index e982a38c1..f04082c68 100644
--- a/libs/apprise/plugins/NotifyD7Networks.py
+++ b/libs/apprise/plugins/NotifyD7Networks.py
@@ -304,6 +304,7 @@ class NotifyD7Networks(NotifyBase):
data=dumps(payload),
headers=headers,
verify=self.verify_certificate,
+ timeout=self.request_timeout,
)
if r.status_code not in (
@@ -379,7 +380,7 @@ class NotifyD7Networks(NotifyBase):
except requests.RequestException as e:
self.logger.warning(
- 'A Connection error occured sending D7 Networks:%s ' % (
+ 'A Connection error occurred sending D7 Networks:%s ' % (
', '.join(self.targets)) + 'notification.'
)
self.logger.debug('Socket Exception: %s' % str(e))
@@ -394,38 +395,37 @@ class NotifyD7Networks(NotifyBase):
Returns the URL built dynamically based on specified arguments.
"""
- # Define any arguments set
- args = {
- 'format': self.notify_format,
- 'overflow': self.overflow_mode,
- 'verify': 'yes' if self.verify_certificate else 'no',
+ # Define any URL parameters
+ params = {
'batch': 'yes' if self.batch else 'no',
}
+ # Extend our parameters
+ params.update(self.url_parameters(privacy=privacy, *args, **kwargs))
+
if self.priority != self.template_args['priority']['default']:
- args['priority'] = str(self.priority)
+ params['priority'] = str(self.priority)
if self.source:
- args['from'] = self.source
+ params['from'] = self.source
- return '{schema}://{user}:{password}@{targets}/?{args}'.format(
+ return '{schema}://{user}:{password}@{targets}/?{params}'.format(
schema=self.secure_protocol,
user=NotifyD7Networks.quote(self.user, safe=''),
password=self.pprint(
self.password, privacy, mode=PrivacyMode.Secret, safe=''),
targets='/'.join(
[NotifyD7Networks.quote(x, safe='') for x in self.targets]),
- args=NotifyD7Networks.urlencode(args))
+ params=NotifyD7Networks.urlencode(params))
@staticmethod
def parse_url(url):
"""
Parses the URL and returns enough arguments that can allow
- us to substantiate this object.
+ us to re-instantiate this object.
"""
results = NotifyBase.parse_url(url, verify_host=False)
-
if not results:
# We're done early as we couldn't load the results
return results
diff --git a/libs/apprise/plugins/NotifyDBus.py b/libs/apprise/plugins/NotifyDBus.py
index 37f2b256a..ca501bf9e 100644
--- a/libs/apprise/plugins/NotifyDBus.py
+++ b/libs/apprise/plugins/NotifyDBus.py
@@ -29,7 +29,6 @@ from __future__ import print_function
from .NotifyBase import NotifyBase
from ..common import NotifyImageSize
from ..common import NotifyType
-from ..utils import GET_SCHEMA_RE
from ..utils import parse_bool
from ..AppriseLocale import gettext_lazy as _
@@ -141,7 +140,6 @@ class NotifyDBus(NotifyBase):
# object if we were to reference, we wouldn't be backwards compatible with
# Python v2. So converting the result set back into a list makes us
# compatible
-
protocol = list(MAINLOOP_MAP.keys())
# A URL that takes you to the setup/help of the specific protocol
@@ -153,7 +151,7 @@ class NotifyDBus(NotifyBase):
# Allows the user to specify the NotifyImageSize object
image_size = NotifyImageSize.XY_128
- # The number of seconds to keep the message present for
+ # The number of milliseconds to keep the message present for
message_timeout_ms = 13000
# Limit results to just the first 10 line otherwise there is just to much
@@ -171,7 +169,7 @@ class NotifyDBus(NotifyBase):
# Define object templates
templates = (
- '{schema}://_/',
+ '{schema}://',
)
# Define our template arguments
@@ -355,27 +353,27 @@ class NotifyDBus(NotifyBase):
DBusUrgency.HIGH: 'high',
}
- # Define any arguments set
- args = {
- 'format': self.notify_format,
- 'overflow': self.overflow_mode,
+ # Define any URL parameters
+ params = {
'image': 'yes' if self.include_image else 'no',
'urgency': 'normal' if self.urgency not in _map
else _map[self.urgency],
- 'verify': 'yes' if self.verify_certificate else 'no',
}
+ # Extend our parameters
+ params.update(self.url_parameters(privacy=privacy, *args, **kwargs))
+
# x in (x,y) screen coordinates
if self.x_axis:
- args['x'] = str(self.x_axis)
+ params['x'] = str(self.x_axis)
# y in (x,y) screen coordinates
if self.y_axis:
- args['y'] = str(self.y_axis)
+ params['y'] = str(self.y_axis)
- return '{schema}://_/?{args}'.format(
+ return '{schema}://_/?{params}'.format(
schema=self.schema,
- args=NotifyDBus.urlencode(args),
+ params=NotifyDBus.urlencode(params),
)
@staticmethod
@@ -386,24 +384,8 @@ class NotifyDBus(NotifyBase):
is in place.
"""
- schema = GET_SCHEMA_RE.match(url)
- if schema is None:
- # Content is simply not parseable
- return None
-
- results = NotifyBase.parse_url(url)
- if not results:
- results = {
- 'schema': schema.group('schema').lower(),
- 'user': None,
- 'password': None,
- 'port': None,
- 'host': '_',
- 'fullpath': None,
- 'path': None,
- 'url': url,
- 'qsd': {},
- }
+
+ results = NotifyBase.parse_url(url, verify_host=False)
# Include images with our message
results['include_image'] = \
diff --git a/libs/apprise/plugins/NotifyDiscord.py b/libs/apprise/plugins/NotifyDiscord.py
index 254d9285e..8a8b21f44 100644
--- a/libs/apprise/plugins/NotifyDiscord.py
+++ b/libs/apprise/plugins/NotifyDiscord.py
@@ -28,17 +28,17 @@
# here you'll be able to access the Webhooks menu and create a new one.
#
# When you've completed, you'll get a URL that looks a little like this:
-# https://discordapp.com/api/webhooks/417429632418316298/\
+# https://discord.com/api/webhooks/417429632418316298/\
# JHZ7lQml277CDHmQKMHI8qBe7bk2ZwO5UKjCiOAF7711o33MyqU344Qpgv7YTpadV_js
#
# Simplified, it looks like this:
-# https://discordapp.com/api/webhooks/WEBHOOK_ID/WEBHOOK_TOKEN
+# https://discord.com/api/webhooks/WEBHOOK_ID/WEBHOOK_TOKEN
#
# This plugin will simply work using the url of:
# discord://WEBHOOK_ID/WEBHOOK_TOKEN
#
# API Documentation on Webhooks:
-# - https://discordapp.com/developers/docs/resources/webhook
+# - https://discord.com/developers/docs/resources/webhook
#
import re
import requests
@@ -63,7 +63,7 @@ class NotifyDiscord(NotifyBase):
service_name = 'Discord'
# The services URL
- service_url = 'https://discordapp.com/'
+ service_url = 'https://discord.com/'
# The default secure protocol
secure_protocol = 'discord'
@@ -72,7 +72,7 @@ class NotifyDiscord(NotifyBase):
setup_url = 'https://github.com/caronc/apprise/wiki/Notify_discord'
# Discord Webhook
- notify_url = 'https://discordapp.com/api/webhooks'
+ notify_url = 'https://discord.com/api/webhooks'
# Allows the user to specify the NotifyImageSize object
image_size = NotifyImageSize.XY_256
@@ -119,6 +119,10 @@ class NotifyDiscord(NotifyBase):
'type': 'bool',
'default': True,
},
+ 'avatar_url': {
+ 'name': _('Avatar URL'),
+ 'type': 'string',
+ },
'footer': {
'name': _('Display Footer'),
'type': 'bool',
@@ -139,7 +143,7 @@ class NotifyDiscord(NotifyBase):
def __init__(self, webhook_id, webhook_token, tts=False, avatar=True,
footer=False, footer_logo=True, include_image=False,
- **kwargs):
+ avatar_url=None, **kwargs):
"""
Initialize Discord Object
@@ -177,6 +181,11 @@ class NotifyDiscord(NotifyBase):
# Place a thumbnail image inline with the message body
self.include_image = include_image
+ # Avatar URL
+ # This allows a user to provide an over-ride to the otherwise
+ # dynamically generated avatar url images
+ self.avatar_url = avatar_url
+
return
def send(self, body, title='', notify_type=NotifyType.INFO, attach=None,
@@ -247,8 +256,9 @@ class NotifyDiscord(NotifyBase):
payload['content'] = \
body if not title else "{}\r\n{}".format(title, body)
- if self.avatar and image_url:
- payload['avatar_url'] = image_url
+ if self.avatar and (image_url or self.avatar_url):
+ payload['avatar_url'] = \
+ self.avatar_url if self.avatar_url else image_url
if self.user:
# Optionally override the default username of the webhook
@@ -343,6 +353,7 @@ class NotifyDiscord(NotifyBase):
headers=headers,
files=files,
verify=self.verify_certificate,
+ timeout=self.request_timeout,
)
if r.status_code not in (
requests.codes.ok, requests.codes.no_content):
@@ -370,14 +381,14 @@ class NotifyDiscord(NotifyBase):
except requests.RequestException as e:
self.logger.warning(
- 'A Connection error occured posting {}to Discord.'.format(
+ 'A Connection error occurred posting {}to Discord.'.format(
attach.name if attach else ''))
self.logger.debug('Socket Exception: %s' % str(e))
return False
except (OSError, IOError) as e:
self.logger.warning(
- 'An I/O error occured while reading {}.'.format(
+ 'An I/O error occurred while reading {}.'.format(
attach.name if attach else 'attachment'))
self.logger.debug('I/O Exception: %s' % str(e))
return False
@@ -395,37 +406,36 @@ class NotifyDiscord(NotifyBase):
Returns the URL built dynamically based on specified arguments.
"""
- # Define any arguments set
- args = {
- 'format': self.notify_format,
- 'overflow': self.overflow_mode,
+ # Define any URL parameters
+ params = {
'tts': 'yes' if self.tts else 'no',
'avatar': 'yes' if self.avatar else 'no',
'footer': 'yes' if self.footer else 'no',
'footer_logo': 'yes' if self.footer_logo else 'no',
'image': 'yes' if self.include_image else 'no',
- 'verify': 'yes' if self.verify_certificate else 'no',
}
- return '{schema}://{webhook_id}/{webhook_token}/?{args}'.format(
+ # Extend our parameters
+ params.update(self.url_parameters(privacy=privacy, *args, **kwargs))
+
+ return '{schema}://{webhook_id}/{webhook_token}/?{params}'.format(
schema=self.secure_protocol,
webhook_id=self.pprint(self.webhook_id, privacy, safe=''),
webhook_token=self.pprint(self.webhook_token, privacy, safe=''),
- args=NotifyDiscord.urlencode(args),
+ params=NotifyDiscord.urlencode(params),
)
@staticmethod
def parse_url(url):
"""
Parses the URL and returns enough arguments that can allow
- us to substantiate this object.
+ us to re-instantiate this object.
Syntax:
discord://webhook_id/webhook_token
"""
- results = NotifyBase.parse_url(url)
-
+ results = NotifyBase.parse_url(url, verify_host=False)
if not results:
# We're done early as we couldn't load the results
return results
@@ -459,43 +469,39 @@ class NotifyDiscord(NotifyBase):
# Update Avatar Icon
results['avatar'] = parse_bool(results['qsd'].get('avatar', True))
- # Use Thumbnail
- if 'thumbnail' in results['qsd']:
- # Deprication Notice issued for v0.7.5
- NotifyDiscord.logger.deprecate(
- 'The Discord URL contains the parameter '
- '"thumbnail=" which will be deprecated in an upcoming '
- 'release. Please use "image=" instead.'
- )
+ # Boolean to include an image or not
+ results['include_image'] = parse_bool(results['qsd'].get(
+ 'image', NotifyDiscord.template_args['image']['default']))
- # use image= for consistency with the other plugins but we also
- # support thumbnail= for backwards compatibility.
- results['include_image'] = \
- parse_bool(results['qsd'].get(
- 'image', results['qsd'].get('thumbnail', False)))
+ # Extract avatar url if it was specified
+ if 'avatar_url' in results['qsd']:
+ results['avatar_url'] = \
+ NotifyDiscord.unquote(results['qsd']['avatar_url'])
return results
@staticmethod
def parse_native_url(url):
"""
- Support https://discordapp.com/api/webhooks/WEBHOOK_ID/WEBHOOK_TOKEN
+ Support https://discord.com/api/webhooks/WEBHOOK_ID/WEBHOOK_TOKEN
+ Support Legacy URL as well:
+ https://discordapp.com/api/webhooks/WEBHOOK_ID/WEBHOOK_TOKEN
"""
result = re.match(
- r'^https?://discordapp\.com/api/webhooks/'
+ r'^https?://discord(app)?\.com/api/webhooks/'
r'(?P<webhook_id>[0-9]+)/'
r'(?P<webhook_token>[A-Z0-9_-]+)/?'
- r'(?P<args>\?.+)?$', url, re.I)
+ r'(?P<params>\?.+)?$', url, re.I)
if result:
return NotifyDiscord.parse_url(
- '{schema}://{webhook_id}/{webhook_token}/{args}'.format(
+ '{schema}://{webhook_id}/{webhook_token}/{params}'.format(
schema=NotifyDiscord.secure_protocol,
webhook_id=result.group('webhook_id'),
webhook_token=result.group('webhook_token'),
- args='' if not result.group('args')
- else result.group('args')))
+ params='' if not result.group('params')
+ else result.group('params')))
return None
diff --git a/libs/apprise/plugins/NotifyEmail.py b/libs/apprise/plugins/NotifyEmail.py
index de686c8b3..604fc5b5c 100644
--- a/libs/apprise/plugins/NotifyEmail.py
+++ b/libs/apprise/plugins/NotifyEmail.py
@@ -29,6 +29,9 @@ import smtplib
from email.mime.text import MIMEText
from email.mime.application import MIMEApplication
from email.mime.multipart import MIMEMultipart
+from email.utils import formataddr
+from email.header import Header
+from email import charset
from socket import error as SocketError
from datetime import datetime
@@ -38,10 +41,12 @@ from ..URLBase import PrivacyMode
from ..common import NotifyFormat
from ..common import NotifyType
from ..utils import is_email
-from ..utils import parse_list
-from ..utils import GET_EMAIL_RE
+from ..utils import parse_emails
from ..AppriseLocale import gettext_lazy as _
+# Globally Default encoding mode set to Quoted Printable.
+charset.add_charset('utf-8', charset.QP, charset.QP, 'utf-8')
+
class WebBaseLogin(object):
"""
@@ -116,6 +121,21 @@ EMAIL_TEMPLATES = (
},
),
+ # Microsoft Office 365 (Email Server)
+ # You must specify an authenticated sender address in the from= settings
+ # and a valid email in the to= to deliver your emails to
+ (
+ 'Microsoft Office 365',
+ re.compile(
+ r'^[^@]+@(?P<domain>(smtp\.)?office365\.com)$', re.I),
+ {
+ 'port': 587,
+ 'smtp_host': 'smtp.office365.com',
+ 'secure': True,
+ 'secure_mode': SecureMailMode.STARTTLS,
+ },
+ ),
+
# Yahoo Mail
(
'Yahoo Mail',
@@ -380,8 +400,8 @@ class NotifyEmail(NotifyBase):
except (ValueError, TypeError):
self.timeout = self.connect_timeout
- # Acquire targets
- self.targets = parse_list(targets)
+ # Acquire Email 'To'
+ self.targets = list()
# Acquire Carbon Copies
self.cc = set()
@@ -389,9 +409,11 @@ class NotifyEmail(NotifyBase):
# Acquire Blind Carbon Copies
self.bcc = set()
+ # For tracking our email -> name lookups
+ self.names = {}
+
# Now we want to construct the To and From email
# addresses from the URL provided
- self.from_name = from_name
self.from_addr = from_addr
if self.user and not self.from_addr:
@@ -401,15 +423,18 @@ class NotifyEmail(NotifyBase):
self.host,
)
- if not is_email(self.from_addr):
+ result = is_email(self.from_addr)
+ if not result:
# Parse Source domain based on from_addr
msg = 'Invalid ~From~ email specified: {}'.format(self.from_addr)
self.logger.warning(msg)
raise TypeError(msg)
- # If our target email list is empty we want to add ourselves to it
- if len(self.targets) == 0:
- self.targets.append(self.from_addr)
+ # Store our email address
+ self.from_addr = result['full_email']
+
+ # Set our from name
+ self.from_name = from_name if from_name else result['name']
# Now detect the SMTP Server
self.smtp_host = \
@@ -425,11 +450,35 @@ class NotifyEmail(NotifyBase):
self.logger.warning(msg)
raise TypeError(msg)
- # Validate recipients (cc:) and drop bad ones:
- for recipient in parse_list(cc):
+ if targets:
+ # Validate recipients (to:) and drop bad ones:
+ for recipient in parse_emails(targets):
+ result = is_email(recipient)
+ if result:
+ self.targets.append(
+ (result['name'] if result['name'] else False,
+ result['full_email']))
+ continue
+
+ self.logger.warning(
+ 'Dropped invalid To email '
+ '({}) specified.'.format(recipient),
+ )
- if GET_EMAIL_RE.match(recipient):
- self.cc.add(recipient)
+ else:
+ # If our target email list is empty we want to add ourselves to it
+ self.targets.append(
+ (self.from_name if self.from_name else False, self.from_addr))
+
+ # Validate recipients (cc:) and drop bad ones:
+ for recipient in parse_emails(cc):
+ email = is_email(recipient)
+ if email:
+ self.cc.add(email['full_email'])
+
+ # Index our name (if one exists)
+ self.names[email['full_email']] = \
+ email['name'] if email['name'] else False
continue
self.logger.warning(
@@ -438,10 +487,14 @@ class NotifyEmail(NotifyBase):
)
# Validate recipients (bcc:) and drop bad ones:
- for recipient in parse_list(bcc):
-
- if GET_EMAIL_RE.match(recipient):
- self.bcc.add(recipient)
+ for recipient in parse_emails(bcc):
+ email = is_email(recipient)
+ if email:
+ self.bcc.add(email['full_email'])
+
+ # Index our name (if one exists)
+ self.names[email['full_email']] = \
+ email['name'] if email['name'] else False
continue
self.logger.warning(
@@ -529,36 +582,57 @@ class NotifyEmail(NotifyBase):
Perform Email Notification
"""
- from_name = self.from_name
- if not from_name:
- from_name = self.app_desc
+ # Initialize our default from name
+ from_name = self.from_name if self.from_name else self.app_desc
# error tracking (used for function return)
has_error = False
+ if not self.targets:
+ # There is no one to email; we're done
+ self.logger.warning(
+ 'There are no Email recipients to notify')
+ return False
+
# Create a copy of the targets list
emails = list(self.targets)
while len(emails):
# Get our email to notify
- to_addr = emails.pop(0)
-
- if not is_email(to_addr):
- self.logger.warning(
- 'Invalid ~To~ email specified: {}'.format(to_addr))
- has_error = True
- continue
+ to_name, to_addr = emails.pop(0)
# Strip target out of cc list if in To or Bcc
cc = (self.cc - self.bcc - set([to_addr]))
+
# Strip target out of bcc list if in To
bcc = (self.bcc - set([to_addr]))
+ try:
+ # Format our cc addresses to support the Name field
+ cc = [formataddr(
+ (self.names.get(addr, False), addr), charset='utf-8')
+ for addr in cc]
+
+ # Format our bcc addresses to support the Name field
+ bcc = [formataddr(
+ (self.names.get(addr, False), addr), charset='utf-8')
+ for addr in bcc]
+
+ except TypeError:
+ # Python v2.x Support (no charset keyword)
+ # Format our cc addresses to support the Name field
+ cc = [formataddr(
+ (self.names.get(addr, False), addr)) for addr in cc]
+
+ # Format our bcc addresses to support the Name field
+ bcc = [formataddr(
+ (self.names.get(addr, False), addr)) for addr in bcc]
+
self.logger.debug(
'Email From: {} <{}>'.format(from_name, self.from_addr))
self.logger.debug('Email To: {}'.format(to_addr))
- if len(cc):
+ if cc:
self.logger.debug('Email Cc: {}'.format(', '.join(cc)))
- if len(bcc):
+ if bcc:
self.logger.debug('Email Bcc: {}'.format(', '.join(bcc)))
self.logger.debug('Login ID: {}'.format(self.user))
self.logger.debug(
@@ -566,15 +640,25 @@ class NotifyEmail(NotifyBase):
# Prepare Email Message
if self.notify_format == NotifyFormat.HTML:
- content = MIMEText(body, 'html')
+ content = MIMEText(body, 'html', 'utf-8')
else:
- content = MIMEText(body, 'plain')
+ content = MIMEText(body, 'plain', 'utf-8')
base = MIMEMultipart() if attach else content
- base['Subject'] = title
- base['From'] = '{} <{}>'.format(from_name, self.from_addr)
- base['To'] = to_addr
+ base['Subject'] = Header(title, 'utf-8')
+ try:
+ base['From'] = formataddr(
+ (from_name if from_name else False, self.from_addr),
+ charset='utf-8')
+ base['To'] = formataddr((to_name, to_addr), charset='utf-8')
+
+ except TypeError:
+ # Python v2.x Support (no charset keyword)
+ base['From'] = formataddr(
+ (from_name if from_name else False, self.from_addr))
+ base['To'] = formataddr((to_name, to_addr))
+
base['Cc'] = ','.join(cc)
base['Date'] = \
datetime.utcnow().strftime("%a, %d %b %Y %H:%M:%S +0000")
@@ -608,7 +692,8 @@ class NotifyEmail(NotifyBase):
app.add_header(
'Content-Disposition',
'attachment; filename="{}"'.format(
- attachment.name))
+ Header(attachment.name, 'utf-8')),
+ )
base.attach(app)
@@ -653,7 +738,7 @@ class NotifyEmail(NotifyBase):
except (SocketError, smtplib.SMTPException, RuntimeError) as e:
self.logger.warning(
- 'A Connection error occured sending Email '
+ 'A Connection error occurred sending Email '
'notification to {}.'.format(self.smtp_host))
self.logger.debug('Socket Exception: %s' % str(e))
@@ -672,26 +757,34 @@ class NotifyEmail(NotifyBase):
Returns the URL built dynamically based on specified arguments.
"""
- # Define any arguments set
- args = {
- 'format': self.notify_format,
- 'overflow': self.overflow_mode,
+ # Define an URL parameters
+ params = {
'from': self.from_addr,
- 'name': self.from_name,
'mode': self.secure_mode,
'smtp': self.smtp_host,
'timeout': self.timeout,
'user': self.user,
- 'verify': 'yes' if self.verify_certificate else 'no',
}
+ # Extend our parameters
+ params.update(self.url_parameters(privacy=privacy, *args, **kwargs))
+
+ if self.from_name:
+ params['name'] = self.from_name
+
if len(self.cc) > 0:
# Handle our Carbon Copy Addresses
- args['cc'] = ','.join(self.cc)
+ params['cc'] = ','.join(
+ ['{}{}'.format(
+ '' if not e not in self.names
+ else '{}:'.format(self.names[e]), e) for e in self.cc])
if len(self.bcc) > 0:
# Handle our Blind Carbon Copy Addresses
- args['bcc'] = ','.join(self.bcc)
+ params['bcc'] = ','.join(
+ ['{}{}'.format(
+ '' if not e not in self.names
+ else '{}:'.format(self.names[e]), e) for e in self.bcc])
# pull email suffix from username (if present)
user = None if not self.user else self.user.split('@')[0]
@@ -717,28 +810,31 @@ class NotifyEmail(NotifyBase):
# a simple boolean check as to whether we display our target emails
# or not
has_targets = \
- not (len(self.targets) == 1 and self.targets[0] == self.from_addr)
+ not (len(self.targets) == 1
+ and self.targets[0][1] == self.from_addr)
- return '{schema}://{auth}{hostname}{port}/{targets}?{args}'.format(
+ return '{schema}://{auth}{hostname}{port}/{targets}?{params}'.format(
schema=self.secure_protocol if self.secure else self.protocol,
auth=auth,
- hostname=NotifyEmail.quote(self.host, safe=''),
+ # never encode hostname since we're expecting it to be a valid one
+ hostname=self.host,
port='' if self.port is None or self.port == default_port
else ':{}'.format(self.port),
targets='' if not has_targets else '/'.join(
- [NotifyEmail.quote(x, safe='') for x in self.targets]),
- args=NotifyEmail.urlencode(args),
+ [NotifyEmail.quote('{}{}'.format(
+ '' if not e[0] else '{}:'.format(e[0]), e[1]),
+ safe='') for e in self.targets]),
+ params=NotifyEmail.urlencode(params),
)
@staticmethod
def parse_url(url):
"""
Parses the URL and returns enough arguments that can allow
- us to substantiate this object.
+ us to re-instantiate this object.
"""
results = NotifyBase.parse_url(url)
-
if not results:
# We're done early as we couldn't load the results
return results
@@ -761,8 +857,7 @@ class NotifyEmail(NotifyBase):
# Attempt to detect 'to' email address
if 'to' in results['qsd'] and len(results['qsd']['to']):
- results['targets'] += \
- NotifyEmail.parse_list(results['qsd']['to'])
+ results['targets'].append(results['qsd']['to'])
if 'name' in results['qsd'] and len(results['qsd']['name']):
# Extract from name to associate with from address
@@ -783,13 +878,11 @@ class NotifyEmail(NotifyBase):
# Handle Carbon Copy Addresses
if 'cc' in results['qsd'] and len(results['qsd']['cc']):
- results['cc'] = \
- NotifyEmail.parse_list(results['qsd']['cc'])
+ results['cc'] = results['qsd']['cc']
# Handle Blind Carbon Copy Addresses
if 'bcc' in results['qsd'] and len(results['qsd']['bcc']):
- results['bcc'] = \
- NotifyEmail.parse_list(results['qsd']['bcc'])
+ results['bcc'] = results['qsd']['bcc']
results['from_addr'] = from_addr
results['smtp_host'] = smtp_host
diff --git a/libs/apprise/plugins/NotifyEmby.py b/libs/apprise/plugins/NotifyEmby.py
index c792b49bd..bf9066cc0 100644
--- a/libs/apprise/plugins/NotifyEmby.py
+++ b/libs/apprise/plugins/NotifyEmby.py
@@ -61,9 +61,6 @@ class NotifyEmby(NotifyBase):
# A URL that takes you to the setup/help of the specific protocol
setup_url = 'https://github.com/caronc/apprise/wiki/Notify_emby'
- # Emby uses the http protocol with JSON requests
- emby_default_port = 8096
-
# By default Emby requires you to provide it a device id
# The following was just a random uuid4 generated one. There
# is no real reason to change this, but hey; that's what open
@@ -94,6 +91,7 @@ class NotifyEmby(NotifyBase):
'type': 'int',
'min': 1,
'max': 65535,
+ 'default': 8096
},
'user': {
'name': _('Username'),
@@ -137,6 +135,10 @@ class NotifyEmby(NotifyBase):
# or a modal type box (requires an Okay acknowledgement)
self.modal = modal
+ if not self.port:
+ # Assign default port if one isn't otherwise specified:
+ self.port = self.template_tokens['port']['default']
+
if not self.user:
# User was not specified
msg = 'No Emby username was specified.'
@@ -207,6 +209,7 @@ class NotifyEmby(NotifyBase):
headers=headers,
data=dumps(payload),
verify=self.verify_certificate,
+ timeout=self.request_timeout,
)
if r.status_code != requests.codes.ok:
@@ -229,7 +232,7 @@ class NotifyEmby(NotifyBase):
except requests.RequestException as e:
self.logger.warning(
- 'A Connection error occured authenticating a user with Emby '
+ 'A Connection error occurred authenticating a user with Emby '
'at %s.' % self.host)
self.logger.debug('Socket Exception: %s' % str(e))
@@ -370,6 +373,7 @@ class NotifyEmby(NotifyBase):
url,
headers=headers,
verify=self.verify_certificate,
+ timeout=self.request_timeout,
)
if r.status_code != requests.codes.ok:
@@ -392,7 +396,7 @@ class NotifyEmby(NotifyBase):
except requests.RequestException as e:
self.logger.warning(
- 'A Connection error occured querying Emby '
+ 'A Connection error occurred querying Emby '
'for session information at %s.' % self.host)
self.logger.debug('Socket Exception: %s' % str(e))
@@ -449,6 +453,7 @@ class NotifyEmby(NotifyBase):
url,
headers=headers,
verify=self.verify_certificate,
+ timeout=self.request_timeout,
)
if r.status_code not in (
@@ -477,7 +482,7 @@ class NotifyEmby(NotifyBase):
except requests.RequestException as e:
self.logger.warning(
- 'A Connection error occured querying Emby '
+ 'A Connection error occurred querying Emby '
'to logoff user %s at %s.' % (self.user, self.host))
self.logger.debug('Socket Exception: %s' % str(e))
@@ -550,6 +555,7 @@ class NotifyEmby(NotifyBase):
data=dumps(payload),
headers=headers,
verify=self.verify_certificate,
+ timeout=self.request_timeout,
)
if r.status_code not in (
requests.codes.ok,
@@ -577,7 +583,7 @@ class NotifyEmby(NotifyBase):
except requests.RequestException as e:
self.logger.warning(
- 'A Connection error occured sending Emby '
+ 'A Connection error occurred sending Emby '
'notification to %s.' % self.host)
self.logger.debug('Socket Exception: %s' % str(e))
@@ -592,14 +598,14 @@ class NotifyEmby(NotifyBase):
Returns the URL built dynamically based on specified arguments.
"""
- # Define any arguments set
- args = {
- 'format': self.notify_format,
- 'overflow': self.overflow_mode,
+ # Define any URL parameters
+ params = {
'modal': 'yes' if self.modal else 'no',
- 'verify': 'yes' if self.verify_certificate else 'no',
}
+ # Extend our parameters
+ params.update(self.url_parameters(privacy=privacy, *args, **kwargs))
+
# Determine Authentication
auth = ''
if self.user and self.password:
@@ -613,13 +619,14 @@ class NotifyEmby(NotifyBase):
user=NotifyEmby.quote(self.user, safe=''),
)
- return '{schema}://{auth}{hostname}{port}/?{args}'.format(
+ return '{schema}://{auth}{hostname}{port}/?{params}'.format(
schema=self.secure_protocol if self.secure else self.protocol,
auth=auth,
- hostname=NotifyEmby.quote(self.host, safe=''),
- port='' if self.port is None or self.port == self.emby_default_port
+ hostname=self.host,
+ port='' if self.port is None
+ or self.port == self.template_tokens['port']['default']
else ':{}'.format(self.port),
- args=NotifyEmby.urlencode(args),
+ params=NotifyEmby.urlencode(params),
)
@property
@@ -655,7 +662,7 @@ class NotifyEmby(NotifyBase):
def parse_url(url):
"""
Parses the URL and returns enough arguments that can allow
- us to substantiate this object.
+ us to re-instantiate this object.
"""
results = NotifyBase.parse_url(url)
@@ -663,10 +670,6 @@ class NotifyEmby(NotifyBase):
# We're done early
return results
- # Assign Default Emby Port
- if not results['port']:
- results['port'] = NotifyEmby.emby_default_port
-
# Modal type popup (default False)
results['modal'] = parse_bool(results['qsd'].get('modal', False))
@@ -679,7 +682,7 @@ class NotifyEmby(NotifyBase):
try:
self.logout()
- except LookupError:
+ except LookupError: # pragma: no cover
# Python v3.5 call to requests can sometimes throw the exception
# "/usr/lib64/python3.7/socket.py", line 748, in getaddrinfo
# LookupError: unknown encoding: idna
diff --git a/libs/apprise/plugins/NotifyEnigma2.py b/libs/apprise/plugins/NotifyEnigma2.py
index 3397f6532..1a8e97fcd 100644
--- a/libs/apprise/plugins/NotifyEnigma2.py
+++ b/libs/apprise/plugins/NotifyEnigma2.py
@@ -184,16 +184,16 @@ class NotifyEnigma2(NotifyBase):
Returns the URL built dynamically based on specified arguments.
"""
- # Define any arguments set
- args = {
- 'format': self.notify_format,
- 'overflow': self.overflow_mode,
- 'verify': 'yes' if self.verify_certificate else 'no',
+ # Define any URL parameters
+ params = {
'timeout': str(self.timeout),
}
- # Append our headers into our args
- args.update({'+{}'.format(k): v for k, v in self.headers.items()})
+ # Append our headers into our parameters
+ params.update({'+{}'.format(k): v for k, v in self.headers.items()})
+
+ # Extend our parameters
+ params.update(self.url_parameters(privacy=privacy, *args, **kwargs))
# Determine Authentication
auth = ''
@@ -210,14 +210,15 @@ class NotifyEnigma2(NotifyBase):
default_port = 443 if self.secure else 80
- return '{schema}://{auth}{hostname}{port}{fullpath}?{args}'.format(
+ return '{schema}://{auth}{hostname}{port}{fullpath}?{params}'.format(
schema=self.secure_protocol if self.secure else self.protocol,
auth=auth,
- hostname=NotifyEnigma2.quote(self.host, safe=''),
+ # never encode hostname since we're expecting it to be a valid one
+ hostname=self.host,
port='' if self.port is None or self.port == default_port
else ':{}'.format(self.port),
fullpath=NotifyEnigma2.quote(self.fullpath, safe='/'),
- args=NotifyEnigma2.urlencode(args),
+ params=NotifyEnigma2.urlencode(params),
)
def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs):
@@ -269,6 +270,7 @@ class NotifyEnigma2(NotifyBase):
headers=headers,
auth=auth,
verify=self.verify_certificate,
+ timeout=self.request_timeout,
)
if r.status_code != requests.codes.ok:
@@ -313,7 +315,7 @@ class NotifyEnigma2(NotifyBase):
except requests.RequestException as e:
self.logger.warning(
- 'A Connection error occured sending Enigma2 '
+ 'A Connection error occurred sending Enigma2 '
'notification to %s.' % self.host)
self.logger.debug('Socket Exception: %s' % str(e))
@@ -326,11 +328,10 @@ class NotifyEnigma2(NotifyBase):
def parse_url(url):
"""
Parses the URL and returns enough arguments that can allow
- us to substantiate this object.
+ us to re-instantiate this object.
"""
results = NotifyBase.parse_url(url)
-
if not results:
# We're done early as we couldn't load the results
return results
diff --git a/libs/apprise/plugins/NotifyFaast.py b/libs/apprise/plugins/NotifyFaast.py
index 4c7b1ad70..d34b4800d 100644
--- a/libs/apprise/plugins/NotifyFaast.py
+++ b/libs/apprise/plugins/NotifyFaast.py
@@ -29,6 +29,7 @@ from ..common import NotifyImageSize
from ..common import NotifyType
from ..utils import parse_bool
from ..AppriseLocale import gettext_lazy as _
+from ..utils import validate_regex
class NotifyFaast(NotifyBase):
@@ -86,7 +87,12 @@ class NotifyFaast(NotifyBase):
super(NotifyFaast, self).__init__(**kwargs)
# Store the Authentication Token
- self.authtoken = authtoken
+ self.authtoken = validate_regex(authtoken)
+ if not self.authtoken:
+ msg = 'An invalid Faast Authentication Token ' \
+ '({}) was specified.'.format(authtoken)
+ self.logger.warning(msg)
+ raise TypeError(msg)
# Associate an image with our post
self.include_image = include_image
@@ -131,6 +137,7 @@ class NotifyFaast(NotifyBase):
data=payload,
headers=headers,
verify=self.verify_certificate,
+ timeout=self.request_timeout,
)
if r.status_code != requests.codes.ok:
# We had a problem
@@ -154,7 +161,7 @@ class NotifyFaast(NotifyBase):
except requests.RequestException as e:
self.logger.warning(
- 'A Connection error occured sending Faast notification.',
+ 'A Connection error occurred sending Faast notification.',
)
self.logger.debug('Socket Exception: %s' % str(e))
@@ -168,29 +175,28 @@ class NotifyFaast(NotifyBase):
Returns the URL built dynamically based on specified arguments.
"""
- # Define any arguments set
- args = {
- 'format': self.notify_format,
- 'overflow': self.overflow_mode,
+ # Define any URL parameters
+ params = {
'image': 'yes' if self.include_image else 'no',
- 'verify': 'yes' if self.verify_certificate else 'no',
}
- return '{schema}://{authtoken}/?{args}'.format(
+ # Extend our parameters
+ params.update(self.url_parameters(privacy=privacy, *args, **kwargs))
+
+ return '{schema}://{authtoken}/?{params}'.format(
schema=self.protocol,
authtoken=self.pprint(self.authtoken, privacy, safe=''),
- args=NotifyFaast.urlencode(args),
+ params=NotifyFaast.urlencode(params),
)
@staticmethod
def parse_url(url):
"""
Parses the URL and returns enough arguments that can allow
- us to substantiate this object.
+ us to re-instantiate this object.
"""
- results = NotifyBase.parse_url(url)
-
+ results = NotifyBase.parse_url(url, verify_host=False)
if not results:
# We're done early as we couldn't load the results
return results
diff --git a/libs/apprise/plugins/NotifyFlock.py b/libs/apprise/plugins/NotifyFlock.py
index 4f751e011..2c68cc1c6 100644
--- a/libs/apprise/plugins/NotifyFlock.py
+++ b/libs/apprise/plugins/NotifyFlock.py
@@ -100,7 +100,7 @@ class NotifyFlock(NotifyBase):
'token': {
'name': _('Access Key'),
'type': 'string',
- 'regex': (r'^[a-z0-9-]{24}$', 'i'),
+ 'regex': (r'^[a-z0-9-]+$', 'i'),
'private': True,
'required': True,
},
@@ -112,14 +112,14 @@ class NotifyFlock(NotifyBase):
'name': _('To User ID'),
'type': 'string',
'prefix': '@',
- 'regex': (r'^[A-Z0-9_]{12}$', 'i'),
+ 'regex': (r'^[A-Z0-9_]+$', 'i'),
'map_to': 'targets',
},
'to_channel': {
'name': _('To Channel ID'),
'type': 'string',
'prefix': '#',
- 'regex': (r'^[A-Z0-9_]{12}$', 'i'),
+ 'regex': (r'^[A-Z0-9_]+$', 'i'),
'map_to': 'targets',
},
'targets': {
@@ -269,6 +269,7 @@ class NotifyFlock(NotifyBase):
data=dumps(payload),
headers=headers,
verify=self.verify_certificate,
+ timeout=self.request_timeout,
)
if r.status_code != requests.codes.ok:
# We had a problem
@@ -294,7 +295,7 @@ class NotifyFlock(NotifyBase):
except requests.RequestException as e:
self.logger.warning(
- 'A Connection error occured sending Flock notification.'
+ 'A Connection error occurred sending Flock notification.'
)
self.logger.debug('Socket Exception: %s' % str(e))
@@ -308,31 +309,31 @@ class NotifyFlock(NotifyBase):
Returns the URL built dynamically based on specified arguments.
"""
- args = {
- 'format': self.notify_format,
- 'overflow': self.overflow_mode,
+ # Define any URL parameters
+ params = {
'image': 'yes' if self.include_image else 'no',
- 'verify': 'yes' if self.verify_certificate else 'no',
}
- return '{schema}://{token}/{targets}?{args}'\
+ # Extend our parameters
+ params.update(self.url_parameters(privacy=privacy, *args, **kwargs))
+
+ return '{schema}://{token}/{targets}?{params}'\
.format(
schema=self.secure_protocol,
token=self.pprint(self.token, privacy, safe=''),
targets='/'.join(
[NotifyFlock.quote(target, safe='')
for target in self.targets]),
- args=NotifyFlock.urlencode(args),
+ params=NotifyFlock.urlencode(params),
)
@staticmethod
def parse_url(url):
"""
Parses the URL and returns enough arguments that can allow
- us to substantiate this object.
+ us to re-instantiate this object.
"""
- results = NotifyBase.parse_url(url)
-
+ results = NotifyBase.parse_url(url, verify_host=False)
if not results:
# We're done early as we couldn't load the results
return results
@@ -363,14 +364,14 @@ class NotifyFlock(NotifyBase):
result = re.match(
r'^https?://api\.flock\.com/hooks/sendMessage/'
r'(?P<token>[a-z0-9-]{24})/?'
- r'(?P<args>\?.+)?$', url, re.I)
+ r'(?P<params>\?.+)?$', url, re.I)
if result:
return NotifyFlock.parse_url(
- '{schema}://{token}/{args}'.format(
+ '{schema}://{token}/{params}'.format(
schema=NotifyFlock.secure_protocol,
token=result.group('token'),
- args='' if not result.group('args')
- else result.group('args')))
+ params='' if not result.group('params')
+ else result.group('params')))
return None
diff --git a/libs/apprise/plugins/NotifyGitter.py b/libs/apprise/plugins/NotifyGitter.py
index 83e13fc76..d94d41469 100644
--- a/libs/apprise/plugins/NotifyGitter.py
+++ b/libs/apprise/plugins/NotifyGitter.py
@@ -309,6 +309,7 @@ class NotifyGitter(NotifyBase):
data=payload,
headers=headers,
verify=self.verify_certificate,
+ timeout=self.request_timeout,
)
if r.status_code != requests.codes.ok:
@@ -366,30 +367,29 @@ class NotifyGitter(NotifyBase):
Returns the URL built dynamically based on specified arguments.
"""
- # Define any arguments set
- args = {
- 'format': self.notify_format,
- 'overflow': self.overflow_mode,
+ # Define any URL parameters
+ params = {
'image': 'yes' if self.include_image else 'no',
- 'verify': 'yes' if self.verify_certificate else 'no',
}
- return '{schema}://{token}/{targets}/?{args}'.format(
+ # Extend our parameters
+ params.update(self.url_parameters(privacy=privacy, *args, **kwargs))
+
+ return '{schema}://{token}/{targets}/?{params}'.format(
schema=self.secure_protocol,
token=self.pprint(self.token, privacy, safe=''),
targets='/'.join(
[NotifyGitter.quote(x, safe='') for x in self.targets]),
- args=NotifyGitter.urlencode(args))
+ params=NotifyGitter.urlencode(params))
@staticmethod
def parse_url(url):
"""
Parses the URL and returns enough arguments that can allow
- us to substantiate this object.
+ us to re-instantiate this object.
"""
- results = NotifyBase.parse_url(url)
-
+ results = NotifyBase.parse_url(url, verify_host=False)
if not results:
# We're done early as we couldn't load the results
return results
diff --git a/libs/apprise/plugins/NotifyGnome.py b/libs/apprise/plugins/NotifyGnome.py
index 012c76fc5..4f5e58606 100644
--- a/libs/apprise/plugins/NotifyGnome.py
+++ b/libs/apprise/plugins/NotifyGnome.py
@@ -113,7 +113,7 @@ class NotifyGnome(NotifyBase):
# Define object templates
templates = (
- '{schema}://_/',
+ '{schema}://',
)
# Define our template arguments
@@ -141,7 +141,7 @@ class NotifyGnome(NotifyBase):
# The urgency of the message
if urgency not in GNOME_URGENCIES:
- self.urgency = GnomeUrgency.NORMAL
+ self.urgency = self.template_args['urgency']['default']
else:
self.urgency = urgency
@@ -214,19 +214,19 @@ class NotifyGnome(NotifyBase):
GnomeUrgency.HIGH: 'high',
}
- # Define any arguments set
- args = {
- 'format': self.notify_format,
- 'overflow': self.overflow_mode,
+ # Define any URL parameters
+ params = {
'image': 'yes' if self.include_image else 'no',
'urgency': 'normal' if self.urgency not in _map
else _map[self.urgency],
- 'verify': 'yes' if self.verify_certificate else 'no',
}
- return '{schema}://_/?{args}'.format(
+ # Extend our parameters
+ params.update(self.url_parameters(privacy=privacy, *args, **kwargs))
+
+ return '{schema}://?{params}'.format(
schema=self.protocol,
- args=NotifyGnome.urlencode(args),
+ params=NotifyGnome.urlencode(params),
)
@staticmethod
@@ -238,19 +238,7 @@ class NotifyGnome(NotifyBase):
"""
- results = NotifyBase.parse_url(url)
- if not results:
- results = {
- 'schema': NotifyGnome.protocol,
- 'user': None,
- 'password': None,
- 'port': None,
- 'host': '_',
- 'fullpath': None,
- 'path': None,
- 'url': url,
- 'qsd': {},
- }
+ results = NotifyBase.parse_url(url, verify_host=False)
# Include images with our message
results['include_image'] = \
diff --git a/libs/apprise/plugins/NotifyGotify.py b/libs/apprise/plugins/NotifyGotify.py
index 954a0a867..a04a69526 100644
--- a/libs/apprise/plugins/NotifyGotify.py
+++ b/libs/apprise/plugins/NotifyGotify.py
@@ -77,10 +77,15 @@ class NotifyGotify(NotifyBase):
# A URL that takes you to the setup/help of the specific protocol
setup_url = 'https://github.com/caronc/apprise/wiki/Notify_gotify'
+ # Disable throttle rate
+ request_rate_per_sec = 0
+
# Define object templates
templates = (
'{schema}://{host}/{token}',
'{schema}://{host}:{port}/{token}',
+ '{schema}://{host}{path}{token}',
+ '{schema}://{host}:{port}{path}{token}',
)
# Define our template tokens
@@ -96,6 +101,13 @@ class NotifyGotify(NotifyBase):
'type': 'string',
'required': True,
},
+ 'path': {
+ 'name': _('Path'),
+ 'type': 'string',
+ 'map_to': 'fullpath',
+ 'default': '/',
+ 'required': True,
+ },
'port': {
'name': _('Port'),
'type': 'int',
@@ -129,6 +141,9 @@ class NotifyGotify(NotifyBase):
self.logger.warning(msg)
raise TypeError(msg)
+ # prepare our fullpath
+ self.fullpath = kwargs.get('fullpath', '/')
+
if priority not in GOTIFY_PRIORITIES:
self.priority = GotifyPriority.NORMAL
@@ -153,7 +168,7 @@ class NotifyGotify(NotifyBase):
url += ':%d' % self.port
# Append our remaining path
- url += '/message'
+ url += '{fullpath}message'.format(fullpath=self.fullpath)
# Define our parameteers
params = {
@@ -188,6 +203,7 @@ class NotifyGotify(NotifyBase):
data=dumps(payload),
headers=headers,
verify=self.verify_certificate,
+ timeout=self.request_timeout,
)
if r.status_code != requests.codes.ok:
# We had a problem
@@ -212,7 +228,7 @@ class NotifyGotify(NotifyBase):
except requests.RequestException as e:
self.logger.warning(
- 'A Connection error occured sending Gotify '
+ 'A Connection error occurred sending Gotify '
'notification to %s.' % self.host)
self.logger.debug('Socket Exception: %s' % str(e))
@@ -226,30 +242,33 @@ class NotifyGotify(NotifyBase):
Returns the URL built dynamically based on specified arguments.
"""
- # Define any arguments set
- args = {
- 'format': self.notify_format,
- 'overflow': self.overflow_mode,
+ # Define any URL parameters
+ params = {
'priority': self.priority,
- 'verify': 'yes' if self.verify_certificate else 'no',
}
+ # Extend our parameters
+ params.update(self.url_parameters(privacy=privacy, *args, **kwargs))
+
+ # Our default port
default_port = 443 if self.secure else 80
- return '{schema}://{hostname}{port}/{token}/?{args}'.format(
+ return '{schema}://{hostname}{port}{fullpath}{token}/?{params}'.format(
schema=self.secure_protocol if self.secure else self.protocol,
- hostname=NotifyGotify.quote(self.host, safe=''),
+ # never encode hostname since we're expecting it to be a valid one
+ hostname=self.host,
port='' if self.port is None or self.port == default_port
else ':{}'.format(self.port),
+ fullpath=NotifyGotify.quote(self.fullpath, safe='/'),
token=self.pprint(self.token, privacy, safe=''),
- args=NotifyGotify.urlencode(args),
+ params=NotifyGotify.urlencode(params),
)
@staticmethod
def parse_url(url):
"""
Parses the URL and returns enough arguments that can allow
- us to substantiate this object.
+ us to re-instantiate this object.
"""
results = NotifyBase.parse_url(url)
@@ -262,13 +281,17 @@ class NotifyGotify(NotifyBase):
# optionally find the provider key
try:
- # The first entry is our token
- results['token'] = entries.pop(0)
+ # The last entry is our token
+ results['token'] = entries.pop()
except IndexError:
# No token was set
results['token'] = None
+ # Re-assemble our full path
+ results['fullpath'] = \
+ '/' if not entries else '/{}/'.format('/'.join(entries))
+
if 'priority' in results['qsd'] and len(results['qsd']['priority']):
_map = {
'l': GotifyPriority.LOW,
diff --git a/libs/apprise/plugins/NotifyGrowl/__init__.py b/libs/apprise/plugins/NotifyGrowl/__init__.py
deleted file mode 100644
index 5fa36795a..000000000
--- a/libs/apprise/plugins/NotifyGrowl/__init__.py
+++ /dev/null
@@ -1,374 +0,0 @@
-# -*- coding: utf-8 -*-
-#
-# Copyright (C) 2019 Chris Caron <[email protected]>
-# All rights reserved.
-#
-# This code is licensed under the MIT License.
-#
-# Permission is hereby granted, free of charge, to any person obtaining a copy
-# of this software and associated documentation files(the "Software"), to deal
-# in the Software without restriction, including without limitation the rights
-# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell
-# copies of the Software, and to permit persons to whom the Software is
-# furnished to do so, subject to the following conditions :
-#
-# The above copyright notice and this permission notice shall be included in
-# all copies or substantial portions of the Software.
-#
-# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE
-# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
-# THE SOFTWARE.
-
-from .gntp import notifier
-from .gntp import errors
-from ..NotifyBase import NotifyBase
-from ...URLBase import PrivacyMode
-from ...common import NotifyImageSize
-from ...common import NotifyType
-from ...utils import parse_bool
-from ...AppriseLocale import gettext_lazy as _
-
-
-# Priorities
-class GrowlPriority(object):
- LOW = -2
- MODERATE = -1
- NORMAL = 0
- HIGH = 1
- EMERGENCY = 2
-
-
-GROWL_PRIORITIES = (
- GrowlPriority.LOW,
- GrowlPriority.MODERATE,
- GrowlPriority.NORMAL,
- GrowlPriority.HIGH,
- GrowlPriority.EMERGENCY,
-)
-
-GROWL_NOTIFICATION_TYPE = "New Messages"
-
-
-class NotifyGrowl(NotifyBase):
- """
- A wrapper to Growl Notifications
-
- """
-
- # The default descriptive name associated with the Notification
- service_name = 'Growl'
-
- # The services URL
- service_url = 'http://growl.info/'
-
- # The default protocol
- protocol = 'growl'
-
- # A URL that takes you to the setup/help of the specific protocol
- setup_url = 'https://github.com/caronc/apprise/wiki/Notify_growl'
-
- # Allows the user to specify the NotifyImageSize object
- image_size = NotifyImageSize.XY_72
-
- # Disable throttle rate for Growl requests since they are normally
- # local anyway
- request_rate_per_sec = 0
-
- # A title can not be used for Growl Messages. Setting this to zero will
- # cause any title (if defined) to get placed into the message body.
- title_maxlen = 0
-
- # Limit results to just the first 10 line otherwise there is just to much
- # content to display
- body_max_line_count = 2
-
- # Default Growl Port
- default_port = 23053
-
- # Define object templates
- # Define object templates
- templates = (
- '{schema}://{host}',
- '{schema}://{host}:{port}',
- '{schema}://{password}@{host}',
- '{schema}://{password}@{host}:{port}',
- )
-
- # Define our template tokens
- template_tokens = dict(NotifyBase.template_tokens, **{
- 'host': {
- 'name': _('Hostname'),
- 'type': 'string',
- 'required': True,
- },
- 'port': {
- 'name': _('Port'),
- 'type': 'int',
- 'min': 1,
- 'max': 65535,
- },
- 'password': {
- 'name': _('Password'),
- 'type': 'string',
- 'private': True,
- },
- })
-
- # Define our template arguments
- template_args = dict(NotifyBase.template_args, **{
- 'priority': {
- 'name': _('Priority'),
- 'type': 'choice:int',
- 'values': GROWL_PRIORITIES,
- 'default': GrowlPriority.NORMAL,
- },
- 'version': {
- 'name': _('Version'),
- 'type': 'choice:int',
- 'values': (1, 2),
- 'default': 2,
- },
- 'image': {
- 'name': _('Include Image'),
- 'type': 'bool',
- 'default': True,
- 'map_to': 'include_image',
- },
- })
-
- def __init__(self, priority=None, version=2, include_image=True, **kwargs):
- """
- Initialize Growl Object
- """
- super(NotifyGrowl, self).__init__(**kwargs)
-
- if not self.port:
- self.port = self.default_port
-
- # The Priority of the message
- if priority not in GROWL_PRIORITIES:
- self.priority = GrowlPriority.NORMAL
-
- else:
- self.priority = priority
-
- # Always default the sticky flag to False
- self.sticky = False
-
- # Store Version
- self.version = version
-
- payload = {
- 'applicationName': self.app_id,
- 'notifications': [GROWL_NOTIFICATION_TYPE, ],
- 'defaultNotifications': [GROWL_NOTIFICATION_TYPE, ],
- 'hostname': self.host,
- 'port': self.port,
- }
-
- if self.password is not None:
- payload['password'] = self.password
-
- self.logger.debug('Growl Registration Payload: %s' % str(payload))
- self.growl = notifier.GrowlNotifier(**payload)
-
- try:
- self.growl.register()
- self.logger.debug(
- 'Growl server registration completed successfully.'
- )
-
- except errors.NetworkError:
- msg = 'A network error occured sending Growl ' \
- 'notification to {}.'.format(self.host)
- self.logger.warning(msg)
- raise TypeError(msg)
-
- except errors.AuthError:
- msg = 'An authentication error occured sending Growl ' \
- 'notification to {}.'.format(self.host)
- self.logger.warning(msg)
- raise TypeError(msg)
-
- except errors.UnsupportedError:
- msg = 'An unsupported error occured sending Growl ' \
- 'notification to {}.'.format(self.host)
- self.logger.warning(msg)
- raise TypeError(msg)
-
- # Track whether or not we want to send an image with our notification
- # or not.
- self.include_image = include_image
-
- return
-
- def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs):
- """
- Perform Growl Notification
- """
-
- icon = None
- if self.version >= 2:
- # URL Based
- icon = None if not self.include_image \
- else self.image_url(notify_type)
-
- else:
- # Raw
- icon = None if not self.include_image \
- else self.image_raw(notify_type)
-
- payload = {
- 'noteType': GROWL_NOTIFICATION_TYPE,
- 'title': title,
- 'description': body,
- 'icon': icon is not None,
- 'sticky': False,
- 'priority': self.priority,
- }
- self.logger.debug('Growl Payload: %s' % str(payload))
-
- # Update icon of payload to be raw data; this is intentionally done
- # here after we spit the debug message above (so we don't try to
- # print the binary contents of an image
- payload['icon'] = icon
-
- # Always call throttle before any remote server i/o is made
- self.throttle()
-
- try:
- response = self.growl.notify(**payload)
- if not isinstance(response, bool):
- self.logger.warning(
- 'Growl notification failed to send with response: %s' %
- str(response),
- )
-
- else:
- self.logger.info('Sent Growl notification.')
-
- except errors.BaseError as e:
- # Since Growl servers listen for UDP broadcasts, it's possible
- # that you will never get to this part of the code since there is
- # no acknowledgement as to whether it accepted what was sent to it
- # or not.
-
- # However, if the host/server is unavailable, you will get to this
- # point of the code.
- self.logger.warning(
- 'A Connection error occured sending Growl '
- 'notification to %s.' % self.host)
- self.logger.debug('Growl Exception: %s' % str(e))
-
- # Return; we're done
- return False
-
- return True
-
- def url(self, privacy=False, *args, **kwargs):
- """
- Returns the URL built dynamically based on specified arguments.
- """
-
- _map = {
- GrowlPriority.LOW: 'low',
- GrowlPriority.MODERATE: 'moderate',
- GrowlPriority.NORMAL: 'normal',
- GrowlPriority.HIGH: 'high',
- GrowlPriority.EMERGENCY: 'emergency',
- }
-
- # Define any arguments set
- args = {
- 'format': self.notify_format,
- 'overflow': self.overflow_mode,
- 'image': 'yes' if self.include_image else 'no',
- 'priority':
- _map[GrowlPriority.NORMAL] if self.priority not in _map
- else _map[self.priority],
- 'version': self.version,
- 'verify': 'yes' if self.verify_certificate else 'no',
- }
-
- auth = ''
- if self.user:
- # The growl password is stored in the user field
- auth = '{password}@'.format(
- password=self.pprint(
- self.user, privacy, mode=PrivacyMode.Secret, safe=''),
- )
-
- return '{schema}://{auth}{hostname}{port}/?{args}'.format(
- schema=self.secure_protocol if self.secure else self.protocol,
- auth=auth,
- hostname=NotifyGrowl.quote(self.host, safe=''),
- port='' if self.port is None or self.port == self.default_port
- else ':{}'.format(self.port),
- args=NotifyGrowl.urlencode(args),
- )
-
- @staticmethod
- def parse_url(url):
- """
- Parses the URL and returns enough arguments that can allow
- us to substantiate this object.
-
- """
- results = NotifyBase.parse_url(url)
-
- if not results:
- # We're done early as we couldn't load the results
- return results
-
- version = None
- if 'version' in results['qsd'] and len(results['qsd']['version']):
- # Allow the user to specify the version of the protocol to use.
- try:
- version = int(
- NotifyGrowl.unquote(
- results['qsd']['version']).strip().split('.')[0])
-
- except (AttributeError, IndexError, TypeError, ValueError):
- NotifyGrowl.logger.warning(
- 'An invalid Growl version of "%s" was specified and will '
- 'be ignored.' % results['qsd']['version']
- )
- pass
-
- if 'priority' in results['qsd'] and len(results['qsd']['priority']):
- _map = {
- 'l': GrowlPriority.LOW,
- 'm': GrowlPriority.MODERATE,
- 'n': GrowlPriority.NORMAL,
- 'h': GrowlPriority.HIGH,
- 'e': GrowlPriority.EMERGENCY,
- }
- try:
- results['priority'] = \
- _map[results['qsd']['priority'][0].lower()]
-
- except KeyError:
- # No priority was set
- pass
-
- # Because of the URL formatting, the password is actually where the
- # username field is. For this reason, we just preform this small hack
- # to make it (the URL) conform correctly. The following strips out the
- # existing password entry (if exists) so that it can be swapped with
- # the new one we specify.
- if results.get('password', None) is None:
- results['password'] = results.get('user', None)
-
- # Include images with our message
- results['include_image'] = \
- parse_bool(results['qsd'].get('image', True))
-
- # Set our version
- if version:
- results['version'] = version
-
- return results
diff --git a/libs/apprise/plugins/NotifyGrowl/gntp/__init__.py b/libs/apprise/plugins/NotifyGrowl/gntp/__init__.py
deleted file mode 100644
index e69de29bb..000000000
--- a/libs/apprise/plugins/NotifyGrowl/gntp/__init__.py
+++ /dev/null
diff --git a/libs/apprise/plugins/NotifyGrowl/gntp/cli.py b/libs/apprise/plugins/NotifyGrowl/gntp/cli.py
deleted file mode 100644
index 0dc61d0a7..000000000
--- a/libs/apprise/plugins/NotifyGrowl/gntp/cli.py
+++ /dev/null
@@ -1,141 +0,0 @@
-# Copyright: 2013 Paul Traylor
-# These sources are released under the terms of the MIT license: see LICENSE
-
-import logging
-import os
-import sys
-from optparse import OptionParser, OptionGroup
-
-from .notifier import GrowlNotifier
-from .shim import RawConfigParser
-from .version import __version__
-
-DEFAULT_CONFIG = os.path.expanduser('~/.gntp')
-
-config = RawConfigParser({
- 'hostname': 'localhost',
- 'password': None,
- 'port': 23053,
-})
-config.read([DEFAULT_CONFIG])
-if not config.has_section('gntp'):
- config.add_section('gntp')
-
-
-class ClientParser(OptionParser):
- def __init__(self):
- OptionParser.__init__(self, version="%%prog %s" % __version__)
-
- group = OptionGroup(self, "Network Options")
- group.add_option("-H", "--host",
- dest="host", default=config.get('gntp', 'hostname'),
- help="Specify a hostname to which to send a remote notification. [%default]")
- group.add_option("--port",
- dest="port", default=config.getint('gntp', 'port'), type="int",
- help="port to listen on [%default]")
- group.add_option("-P", "--password",
- dest='password', default=config.get('gntp', 'password'),
- help="Network password")
- self.add_option_group(group)
-
- group = OptionGroup(self, "Notification Options")
- group.add_option("-n", "--name",
- dest="app", default='Python GNTP Test Client',
- help="Set the name of the application [%default]")
- group.add_option("-s", "--sticky",
- dest='sticky', default=False, action="store_true",
- help="Make the notification sticky [%default]")
- group.add_option("--image",
- dest="icon", default=None,
- help="Icon for notification (URL or /path/to/file)")
- group.add_option("-m", "--message",
- dest="message", default=None,
- help="Sets the message instead of using stdin")
- group.add_option("-p", "--priority",
- dest="priority", default=0, type="int",
- help="-2 to 2 [%default]")
- group.add_option("-d", "--identifier",
- dest="identifier",
- help="Identifier for coalescing")
- group.add_option("-t", "--title",
- dest="title", default=None,
- help="Set the title of the notification [%default]")
- group.add_option("-N", "--notification",
- dest="name", default='Notification',
- help="Set the notification name [%default]")
- group.add_option("--callback",
- dest="callback",
- help="URL callback")
- self.add_option_group(group)
-
- # Extra Options
- self.add_option('-v', '--verbose',
- dest='verbose', default=0, action='count',
- help="Verbosity levels")
-
- def parse_args(self, args=None, values=None):
- values, args = OptionParser.parse_args(self, args, values)
-
- if values.message is None:
- print('Enter a message followed by Ctrl-D')
- try:
- message = sys.stdin.read()
- except KeyboardInterrupt:
- exit()
- else:
- message = values.message
-
- if values.title is None:
- values.title = ' '.join(args)
-
- # If we still have an empty title, use the
- # first bit of the message as the title
- if values.title == '':
- values.title = message[:20]
-
- values.verbose = logging.WARNING - values.verbose * 10
-
- return values, message
-
-
-def main():
- (options, message) = ClientParser().parse_args()
- logging.basicConfig(level=options.verbose)
- if not os.path.exists(DEFAULT_CONFIG):
- logging.info('No config read found at %s', DEFAULT_CONFIG)
-
- growl = GrowlNotifier(
- applicationName=options.app,
- notifications=[options.name],
- defaultNotifications=[options.name],
- hostname=options.host,
- password=options.password,
- port=options.port,
- )
- result = growl.register()
- if result is not True:
- exit(result)
-
- # This would likely be better placed within the growl notifier
- # class but until I make _checkIcon smarter this is "easier"
- if options.icon is not None and not options.icon.startswith('http'):
- logging.info('Loading image %s', options.icon)
- f = open(options.icon)
- options.icon = f.read()
- f.close()
-
- result = growl.notify(
- noteType=options.name,
- title=options.title,
- description=message,
- icon=options.icon,
- sticky=options.sticky,
- priority=options.priority,
- callback=options.callback,
- identifier=options.identifier,
- )
- if result is not True:
- exit(result)
-
-if __name__ == "__main__":
- main()
diff --git a/libs/apprise/plugins/NotifyGrowl/gntp/config.py b/libs/apprise/plugins/NotifyGrowl/gntp/config.py
deleted file mode 100644
index e7dda48ad..000000000
--- a/libs/apprise/plugins/NotifyGrowl/gntp/config.py
+++ /dev/null
@@ -1,77 +0,0 @@
-# Copyright: 2013 Paul Traylor
-# These sources are released under the terms of the MIT license: see LICENSE
-
-"""
-The gntp.config module is provided as an extended GrowlNotifier object that takes
-advantage of the ConfigParser module to allow us to setup some default values
-(such as hostname, password, and port) in a more global way to be shared among
-programs using gntp
-"""
-import logging
-import os
-
-from .gntp import notifier
-from .gntp import shim
-
-__all__ = [
- 'mini',
- 'GrowlNotifier'
-]
-
-logger = logging.getLogger('gntp')
-
-
-class GrowlNotifier(notifier.GrowlNotifier):
- """
- ConfigParser enhanced GrowlNotifier object
-
- For right now, we are only interested in letting users overide certain
- values from ~/.gntp
-
- ::
-
- [gntp]
- hostname = ?
- password = ?
- port = ?
- """
- def __init__(self, *args, **kwargs):
- config = shim.RawConfigParser({
- 'hostname': kwargs.get('hostname', 'localhost'),
- 'password': kwargs.get('password'),
- 'port': kwargs.get('port', 23053),
- })
-
- config.read([os.path.expanduser('~/.gntp')])
-
- # If the file does not exist, then there will be no gntp section defined
- # and the config.get() lines below will get confused. Since we are not
- # saving the config, it should be safe to just add it here so the
- # code below doesn't complain
- if not config.has_section('gntp'):
- logger.info('Error reading ~/.gntp config file')
- config.add_section('gntp')
-
- kwargs['password'] = config.get('gntp', 'password')
- kwargs['hostname'] = config.get('gntp', 'hostname')
- kwargs['port'] = config.getint('gntp', 'port')
-
- super(GrowlNotifier, self).__init__(*args, **kwargs)
-
-
-def mini(description, **kwargs):
- """Single notification function
-
- Simple notification function in one line. Has only one required parameter
- and attempts to use reasonable defaults for everything else
- :param string description: Notification message
- """
- kwargs['notifierFactory'] = GrowlNotifier
- notifier.mini(description, **kwargs)
-
-
-if __name__ == '__main__':
- # If we're running this module directly we're likely running it as a test
- # so extra debugging is useful
- logging.basicConfig(level=logging.INFO)
- mini('Testing mini notification')
diff --git a/libs/apprise/plugins/NotifyGrowl/gntp/core.py b/libs/apprise/plugins/NotifyGrowl/gntp/core.py
deleted file mode 100644
index 99db7570a..000000000
--- a/libs/apprise/plugins/NotifyGrowl/gntp/core.py
+++ /dev/null
@@ -1,511 +0,0 @@
-# Copyright: 2013 Paul Traylor
-# These sources are released under the terms of the MIT license: see LICENSE
-
-import hashlib
-import re
-import time
-
-from . import shim
-from . import errors as errors
-
-__all__ = [
- 'GNTPRegister',
- 'GNTPNotice',
- 'GNTPSubscribe',
- 'GNTPOK',
- 'GNTPError',
- 'parse_gntp',
-]
-
-#GNTP/<version> <messagetype> <encryptionAlgorithmID>[:<ivValue>][ <keyHashAlgorithmID>:<keyHash>.<salt>]
-GNTP_INFO_LINE = re.compile(
- r'GNTP/(?P<version>\d+\.\d+) (?P<messagetype>REGISTER|NOTIFY|SUBSCRIBE|\-OK|\-ERROR)' +
- r' (?P<encryptionAlgorithmID>[A-Z0-9]+(:(?P<ivValue>[A-F0-9]+))?) ?' +
- r'((?P<keyHashAlgorithmID>[A-Z0-9]+):(?P<keyHash>[A-F0-9]+).(?P<salt>[A-F0-9]+))?\r\n',
- re.IGNORECASE
-)
-
-GNTP_INFO_LINE_SHORT = re.compile(
- r'GNTP/(?P<version>\d+\.\d+) (?P<messagetype>REGISTER|NOTIFY|SUBSCRIBE|\-OK|\-ERROR)',
- re.IGNORECASE
-)
-
-GNTP_HEADER = re.compile(r'([\w-]+):(.+)')
-
-GNTP_EOL = shim.b('\r\n')
-GNTP_SEP = shim.b(': ')
-
-
-class _GNTPBuffer(shim.StringIO):
- """GNTP Buffer class"""
- def writeln(self, value=None):
- if value:
- self.write(shim.b(value))
- self.write(GNTP_EOL)
-
- def writeheader(self, key, value):
- if not isinstance(value, str):
- value = str(value)
- self.write(shim.b(key))
- self.write(GNTP_SEP)
- self.write(shim.b(value))
- self.write(GNTP_EOL)
-
-
-class _GNTPBase(object):
- """Base initilization
-
- :param string messagetype: GNTP Message type
- :param string version: GNTP Protocol version
- :param string encription: Encryption protocol
- """
- def __init__(self, messagetype=None, version='1.0', encryption=None):
- self.info = {
- 'version': version,
- 'messagetype': messagetype,
- 'encryptionAlgorithmID': encryption
- }
- self.hash_algo = {
- 'MD5': hashlib.md5,
- 'SHA1': hashlib.sha1,
- 'SHA256': hashlib.sha256,
- 'SHA512': hashlib.sha512,
- }
- self.headers = {}
- self.resources = {}
-
- def __str__(self):
- return self.encode()
-
- def _parse_info(self, data):
- """Parse the first line of a GNTP message to get security and other info values
-
- :param string data: GNTP Message
- :return dict: Parsed GNTP Info line
- """
-
- match = GNTP_INFO_LINE.match(data)
-
- if not match:
- raise errors.ParseError('ERROR_PARSING_INFO_LINE')
-
- info = match.groupdict()
- if info['encryptionAlgorithmID'] == 'NONE':
- info['encryptionAlgorithmID'] = None
-
- return info
-
- def set_password(self, password, encryptAlgo='MD5'):
- """Set a password for a GNTP Message
-
- :param string password: Null to clear password
- :param string encryptAlgo: Supports MD5, SHA1, SHA256, SHA512
- """
- if not password:
- self.info['encryptionAlgorithmID'] = None
- self.info['keyHashAlgorithm'] = None
- return
-
- self.password = shim.b(password)
- self.encryptAlgo = encryptAlgo.upper()
-
- if not self.encryptAlgo in self.hash_algo:
- raise errors.UnsupportedError('INVALID HASH "%s"' % self.encryptAlgo)
-
- hashfunction = self.hash_algo.get(self.encryptAlgo)
-
- password = password.encode('utf8')
- seed = time.ctime().encode('utf8')
- salt = hashfunction(seed).hexdigest()
- saltHash = hashfunction(seed).digest()
- keyBasis = password + saltHash
- key = hashfunction(keyBasis).digest()
- keyHash = hashfunction(key).hexdigest()
-
- self.info['keyHashAlgorithmID'] = self.encryptAlgo
- self.info['keyHash'] = keyHash.upper()
- self.info['salt'] = salt.upper()
-
- def _decode_hex(self, value):
- """Helper function to decode hex string to `proper` hex string
-
- :param string value: Human readable hex string
- :return string: Hex string
- """
- result = ''
- for i in range(0, len(value), 2):
- tmp = int(value[i:i + 2], 16)
- result += chr(tmp)
- return result
-
- def _decode_binary(self, rawIdentifier, identifier):
- rawIdentifier += '\r\n\r\n'
- dataLength = int(identifier['Length'])
- pointerStart = self.raw.find(rawIdentifier) + len(rawIdentifier)
- pointerEnd = pointerStart + dataLength
- data = self.raw[pointerStart:pointerEnd]
- if not len(data) == dataLength:
- raise errors.ParseError('INVALID_DATA_LENGTH Expected: %s Recieved %s' % (dataLength, len(data)))
- return data
-
- def _validate_password(self, password):
- """Validate GNTP Message against stored password"""
- self.password = password
- if password is None:
- raise errors.AuthError('Missing password')
- keyHash = self.info.get('keyHash', None)
- if keyHash is None and self.password is None:
- return True
- if keyHash is None:
- raise errors.AuthError('Invalid keyHash')
- if self.password is None:
- raise errors.AuthError('Missing password')
-
- keyHashAlgorithmID = self.info.get('keyHashAlgorithmID','MD5')
-
- password = self.password.encode('utf8')
- saltHash = self._decode_hex(self.info['salt'])
-
- keyBasis = password + saltHash
- self.key = self.hash_algo[keyHashAlgorithmID](keyBasis).digest()
- keyHash = self.hash_algo[keyHashAlgorithmID](self.key).hexdigest()
-
- if not keyHash.upper() == self.info['keyHash'].upper():
- raise errors.AuthError('Invalid Hash')
- return True
-
- def validate(self):
- """Verify required headers"""
- for header in self._requiredHeaders:
- if not self.headers.get(header, False):
- raise errors.ParseError('Missing Notification Header: ' + header)
-
- def _format_info(self):
- """Generate info line for GNTP Message
-
- :return string:
- """
- info = 'GNTP/%s %s' % (
- self.info.get('version'),
- self.info.get('messagetype'),
- )
- if self.info.get('encryptionAlgorithmID', None):
- info += ' %s:%s' % (
- self.info.get('encryptionAlgorithmID'),
- self.info.get('ivValue'),
- )
- else:
- info += ' NONE'
-
- if self.info.get('keyHashAlgorithmID', None):
- info += ' %s:%s.%s' % (
- self.info.get('keyHashAlgorithmID'),
- self.info.get('keyHash'),
- self.info.get('salt')
- )
-
- return info
-
- def _parse_dict(self, data):
- """Helper function to parse blocks of GNTP headers into a dictionary
-
- :param string data:
- :return dict: Dictionary of parsed GNTP Headers
- """
- d = {}
- for line in data.split('\r\n'):
- match = GNTP_HEADER.match(line)
- if not match:
- continue
-
- key = match.group(1).strip()
- val = match.group(2).strip()
- d[key] = val
- return d
-
- def add_header(self, key, value):
- self.headers[key] = value
-
- def add_resource(self, data):
- """Add binary resource
-
- :param string data: Binary Data
- """
- data = shim.b(data)
- identifier = hashlib.md5(data).hexdigest()
- self.resources[identifier] = data
- return 'x-growl-resource://%s' % identifier
-
- def decode(self, data, password=None):
- """Decode GNTP Message
-
- :param string data:
- """
- self.password = password
- self.raw = shim.u(data)
- parts = self.raw.split('\r\n\r\n')
- self.info = self._parse_info(self.raw)
- self.headers = self._parse_dict(parts[0])
-
- def encode(self):
- """Encode a generic GNTP Message
-
- :return string: GNTP Message ready to be sent. Returned as a byte string
- """
-
- buff = _GNTPBuffer()
-
- buff.writeln(self._format_info())
-
- #Headers
- for k, v in self.headers.items():
- buff.writeheader(k, v)
- buff.writeln()
-
- #Resources
- for resource, data in self.resources.items():
- buff.writeheader('Identifier', resource)
- buff.writeheader('Length', len(data))
- buff.writeln()
- buff.write(data)
- buff.writeln()
- buff.writeln()
-
- return buff.getvalue()
-
-
-class GNTPRegister(_GNTPBase):
- """Represents a GNTP Registration Command
-
- :param string data: (Optional) See decode()
- :param string password: (Optional) Password to use while encoding/decoding messages
- """
- _requiredHeaders = [
- 'Application-Name',
- 'Notifications-Count'
- ]
- _requiredNotificationHeaders = ['Notification-Name']
-
- def __init__(self, data=None, password=None):
- _GNTPBase.__init__(self, 'REGISTER')
- self.notifications = []
-
- if data:
- self.decode(data, password)
- else:
- self.set_password(password)
- self.add_header('Application-Name', 'pygntp')
- self.add_header('Notifications-Count', 0)
-
- def validate(self):
- '''Validate required headers and validate notification headers'''
- for header in self._requiredHeaders:
- if not self.headers.get(header, False):
- raise errors.ParseError('Missing Registration Header: ' + header)
- for notice in self.notifications:
- for header in self._requiredNotificationHeaders:
- if not notice.get(header, False):
- raise errors.ParseError('Missing Notification Header: ' + header)
-
- def decode(self, data, password):
- """Decode existing GNTP Registration message
-
- :param string data: Message to decode
- """
- self.raw = shim.u(data)
- parts = self.raw.split('\r\n\r\n')
- self.info = self._parse_info(self.raw)
- self._validate_password(password)
- self.headers = self._parse_dict(parts[0])
-
- for i, part in enumerate(parts):
- if i == 0:
- continue # Skip Header
- if part.strip() == '':
- continue
- notice = self._parse_dict(part)
- if notice.get('Notification-Name', False):
- self.notifications.append(notice)
- elif notice.get('Identifier', False):
- notice['Data'] = self._decode_binary(part, notice)
- #open('register.png','wblol').write(notice['Data'])
- self.resources[notice.get('Identifier')] = notice
-
- def add_notification(self, name, enabled=True):
- """Add new Notification to Registration message
-
- :param string name: Notification Name
- :param boolean enabled: Enable this notification by default
- """
- notice = {}
- notice['Notification-Name'] = name
- notice['Notification-Enabled'] = enabled
-
- self.notifications.append(notice)
- self.add_header('Notifications-Count', len(self.notifications))
-
- def encode(self):
- """Encode a GNTP Registration Message
-
- :return string: Encoded GNTP Registration message. Returned as a byte string
- """
-
- buff = _GNTPBuffer()
-
- buff.writeln(self._format_info())
-
- #Headers
- for k, v in self.headers.items():
- buff.writeheader(k, v)
- buff.writeln()
-
- #Notifications
- if len(self.notifications) > 0:
- for notice in self.notifications:
- for k, v in notice.items():
- buff.writeheader(k, v)
- buff.writeln()
-
- #Resources
- for resource, data in self.resources.items():
- buff.writeheader('Identifier', resource)
- buff.writeheader('Length', len(data))
- buff.writeln()
- buff.write(data)
- buff.writeln()
- buff.writeln()
-
- return buff.getvalue()
-
-
-class GNTPNotice(_GNTPBase):
- """Represents a GNTP Notification Command
-
- :param string data: (Optional) See decode()
- :param string app: (Optional) Set Application-Name
- :param string name: (Optional) Set Notification-Name
- :param string title: (Optional) Set Notification Title
- :param string password: (Optional) Password to use while encoding/decoding messages
- """
- _requiredHeaders = [
- 'Application-Name',
- 'Notification-Name',
- 'Notification-Title'
- ]
-
- def __init__(self, data=None, app=None, name=None, title=None, password=None):
- _GNTPBase.__init__(self, 'NOTIFY')
-
- if data:
- self.decode(data, password)
- else:
- self.set_password(password)
- if app:
- self.add_header('Application-Name', app)
- if name:
- self.add_header('Notification-Name', name)
- if title:
- self.add_header('Notification-Title', title)
-
- def decode(self, data, password):
- """Decode existing GNTP Notification message
-
- :param string data: Message to decode.
- """
- self.raw = shim.u(data)
- parts = self.raw.split('\r\n\r\n')
- self.info = self._parse_info(self.raw)
- self._validate_password(password)
- self.headers = self._parse_dict(parts[0])
-
- for i, part in enumerate(parts):
- if i == 0:
- continue # Skip Header
- if part.strip() == '':
- continue
- notice = self._parse_dict(part)
- if notice.get('Identifier', False):
- notice['Data'] = self._decode_binary(part, notice)
- #open('notice.png','wblol').write(notice['Data'])
- self.resources[notice.get('Identifier')] = notice
-
-
-class GNTPSubscribe(_GNTPBase):
- """Represents a GNTP Subscribe Command
-
- :param string data: (Optional) See decode()
- :param string password: (Optional) Password to use while encoding/decoding messages
- """
- _requiredHeaders = [
- 'Subscriber-ID',
- 'Subscriber-Name',
- ]
-
- def __init__(self, data=None, password=None):
- _GNTPBase.__init__(self, 'SUBSCRIBE')
- if data:
- self.decode(data, password)
- else:
- self.set_password(password)
-
-
-class GNTPOK(_GNTPBase):
- """Represents a GNTP OK Response
-
- :param string data: (Optional) See _GNTPResponse.decode()
- :param string action: (Optional) Set type of action the OK Response is for
- """
- _requiredHeaders = ['Response-Action']
-
- def __init__(self, data=None, action=None):
- _GNTPBase.__init__(self, '-OK')
- if data:
- self.decode(data)
- if action:
- self.add_header('Response-Action', action)
-
-
-class GNTPError(_GNTPBase):
- """Represents a GNTP Error response
-
- :param string data: (Optional) See _GNTPResponse.decode()
- :param string errorcode: (Optional) Error code
- :param string errordesc: (Optional) Error Description
- """
- _requiredHeaders = ['Error-Code', 'Error-Description']
-
- def __init__(self, data=None, errorcode=None, errordesc=None):
- _GNTPBase.__init__(self, '-ERROR')
- if data:
- self.decode(data)
- if errorcode:
- self.add_header('Error-Code', errorcode)
- self.add_header('Error-Description', errordesc)
-
- def error(self):
- return (self.headers.get('Error-Code', None),
- self.headers.get('Error-Description', None))
-
-
-def parse_gntp(data, password=None):
- """Attempt to parse a message as a GNTP message
-
- :param string data: Message to be parsed
- :param string password: Optional password to be used to verify the message
- """
- data = shim.u(data)
- match = GNTP_INFO_LINE_SHORT.match(data)
- if not match:
- raise errors.ParseError('INVALID_GNTP_INFO')
- info = match.groupdict()
- if info['messagetype'] == 'REGISTER':
- return GNTPRegister(data, password=password)
- elif info['messagetype'] == 'NOTIFY':
- return GNTPNotice(data, password=password)
- elif info['messagetype'] == 'SUBSCRIBE':
- return GNTPSubscribe(data, password=password)
- elif info['messagetype'] == '-OK':
- return GNTPOK(data)
- elif info['messagetype'] == '-ERROR':
- return GNTPError(data)
- raise errors.ParseError('INVALID_GNTP_MESSAGE')
diff --git a/libs/apprise/plugins/NotifyGrowl/gntp/errors.py b/libs/apprise/plugins/NotifyGrowl/gntp/errors.py
deleted file mode 100644
index c006fd680..000000000
--- a/libs/apprise/plugins/NotifyGrowl/gntp/errors.py
+++ /dev/null
@@ -1,25 +0,0 @@
-# Copyright: 2013 Paul Traylor
-# These sources are released under the terms of the MIT license: see LICENSE
-
-class BaseError(Exception):
- pass
-
-
-class ParseError(BaseError):
- errorcode = 500
- errordesc = 'Error parsing the message'
-
-
-class AuthError(BaseError):
- errorcode = 400
- errordesc = 'Error with authorization'
-
-
-class UnsupportedError(BaseError):
- errorcode = 500
- errordesc = 'Currently unsupported by gntp.py'
-
-
-class NetworkError(BaseError):
- errorcode = 500
- errordesc = "Error connecting to growl server"
diff --git a/libs/apprise/plugins/NotifyGrowl/gntp/notifier.py b/libs/apprise/plugins/NotifyGrowl/gntp/notifier.py
deleted file mode 100644
index 38d8328f1..000000000
--- a/libs/apprise/plugins/NotifyGrowl/gntp/notifier.py
+++ /dev/null
@@ -1,265 +0,0 @@
-# Copyright: 2013 Paul Traylor
-# These sources are released under the terms of the MIT license: see LICENSE
-
-"""
-The gntp.notifier module is provided as a simple way to send notifications
-using GNTP
-
-.. note::
- This class is intended to mostly mirror the older Python bindings such
- that you should be able to replace instances of the old bindings with
- this class.
- `Original Python bindings <http://code.google.com/p/growl/source/browse/Bindings/python/Growl.py>`_
-
-"""
-import logging
-import platform
-import socket
-import sys
-
-from .version import __version__
-from . import core
-from . import errors as errors
-from . import shim
-
-__all__ = [
- 'mini',
- 'GrowlNotifier',
-]
-
-logger = logging.getLogger('gntp')
-
-
-class GrowlNotifier(object):
- """Helper class to simplfy sending Growl messages
-
- :param string applicationName: Sending application name
- :param list notification: List of valid notifications
- :param list defaultNotifications: List of notifications that should be enabled
- by default
- :param string applicationIcon: Icon URL
- :param string hostname: Remote host
- :param integer port: Remote port
- """
-
- passwordHash = 'MD5'
- socketTimeout = 3
-
- def __init__(self, applicationName='Python GNTP', notifications=[],
- defaultNotifications=None, applicationIcon=None, hostname='localhost',
- password=None, port=23053):
-
- self.applicationName = applicationName
- self.notifications = list(notifications)
- if defaultNotifications:
- self.defaultNotifications = list(defaultNotifications)
- else:
- self.defaultNotifications = self.notifications
- self.applicationIcon = applicationIcon
-
- self.password = password
- self.hostname = hostname
- self.port = int(port)
-
- def _checkIcon(self, data):
- '''
- Check the icon to see if it's valid
-
- If it's a simple URL icon, then we return True. If it's a data icon
- then we return False
- '''
- logger.info('Checking icon')
- return shim.u(data).startswith('http')
-
- def register(self):
- """Send GNTP Registration
-
- .. warning::
- Before sending notifications to Growl, you need to have
- sent a registration message at least once
- """
- logger.info('Sending registration to %s:%s', self.hostname, self.port)
- register = core.GNTPRegister()
- register.add_header('Application-Name', self.applicationName)
- for notification in self.notifications:
- enabled = notification in self.defaultNotifications
- register.add_notification(notification, enabled)
- if self.applicationIcon:
- if self._checkIcon(self.applicationIcon):
- register.add_header('Application-Icon', self.applicationIcon)
- else:
- resource = register.add_resource(self.applicationIcon)
- register.add_header('Application-Icon', resource)
- if self.password:
- register.set_password(self.password, self.passwordHash)
- self.add_origin_info(register)
- self.register_hook(register)
- return self._send('register', register)
-
- def notify(self, noteType, title, description, icon=None, sticky=False,
- priority=None, callback=None, identifier=None, custom={}):
- """Send a GNTP notifications
-
- .. warning::
- Must have registered with growl beforehand or messages will be ignored
-
- :param string noteType: One of the notification names registered earlier
- :param string title: Notification title (usually displayed on the notification)
- :param string description: The main content of the notification
- :param string icon: Icon URL path
- :param boolean sticky: Sticky notification
- :param integer priority: Message priority level from -2 to 2
- :param string callback: URL callback
- :param dict custom: Custom attributes. Key names should be prefixed with X-
- according to the spec but this is not enforced by this class
-
- .. warning::
- For now, only URL callbacks are supported. In the future, the
- callback argument will also support a function
- """
- logger.info('Sending notification [%s] to %s:%s', noteType, self.hostname, self.port)
- assert noteType in self.notifications
- notice = core.GNTPNotice()
- notice.add_header('Application-Name', self.applicationName)
- notice.add_header('Notification-Name', noteType)
- notice.add_header('Notification-Title', title)
- if self.password:
- notice.set_password(self.password, self.passwordHash)
- if sticky:
- notice.add_header('Notification-Sticky', sticky)
- if priority:
- notice.add_header('Notification-Priority', priority)
- if icon:
- if self._checkIcon(icon):
- notice.add_header('Notification-Icon', icon)
- else:
- resource = notice.add_resource(icon)
- notice.add_header('Notification-Icon', resource)
-
- if description:
- notice.add_header('Notification-Text', description)
- if callback:
- notice.add_header('Notification-Callback-Target', callback)
- if identifier:
- notice.add_header('Notification-Coalescing-ID', identifier)
-
- for key in custom:
- notice.add_header(key, custom[key])
-
- self.add_origin_info(notice)
- self.notify_hook(notice)
-
- return self._send('notify', notice)
-
- def subscribe(self, id, name, port):
- """Send a Subscribe request to a remote machine"""
- sub = core.GNTPSubscribe()
- sub.add_header('Subscriber-ID', id)
- sub.add_header('Subscriber-Name', name)
- sub.add_header('Subscriber-Port', port)
- if self.password:
- sub.set_password(self.password, self.passwordHash)
-
- self.add_origin_info(sub)
- self.subscribe_hook(sub)
-
- return self._send('subscribe', sub)
-
- def add_origin_info(self, packet):
- """Add optional Origin headers to message"""
- packet.add_header('Origin-Machine-Name', platform.node())
- packet.add_header('Origin-Software-Name', 'gntp.py')
- packet.add_header('Origin-Software-Version', __version__)
- packet.add_header('Origin-Platform-Name', platform.system())
- packet.add_header('Origin-Platform-Version', platform.platform())
-
- def register_hook(self, packet):
- pass
-
- def notify_hook(self, packet):
- pass
-
- def subscribe_hook(self, packet):
- pass
-
- def _send(self, messagetype, packet):
- """Send the GNTP Packet"""
-
- packet.validate()
- data = packet.encode()
-
- logger.debug('To : %s:%s <%s>\n%s', self.hostname, self.port, packet.__class__, data)
-
- s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
- s.settimeout(self.socketTimeout)
- try:
- s.connect((self.hostname, self.port))
- s.send(data)
- recv_data = s.recv(1024)
- while not recv_data.endswith(shim.b("\r\n\r\n")):
- recv_data += s.recv(1024)
- except socket.error:
- # Python2.5 and Python3 compatibile exception
- exc = sys.exc_info()[1]
- raise errors.NetworkError(exc)
-
- response = core.parse_gntp(recv_data)
- s.close()
-
- logger.debug('From : %s:%s <%s>\n%s', self.hostname, self.port, response.__class__, response)
-
- if type(response) == core.GNTPOK:
- return True
- logger.error('Invalid response: %s', response.error())
- return response.error()
-
-
-def mini(description, applicationName='PythonMini', noteType="Message",
- title="Mini Message", applicationIcon=None, hostname='localhost',
- password=None, port=23053, sticky=False, priority=None,
- callback=None, notificationIcon=None, identifier=None,
- notifierFactory=GrowlNotifier):
- """Single notification function
-
- Simple notification function in one line. Has only one required parameter
- and attempts to use reasonable defaults for everything else
- :param string description: Notification message
-
- .. warning::
- For now, only URL callbacks are supported. In the future, the
- callback argument will also support a function
- """
- try:
- growl = notifierFactory(
- applicationName=applicationName,
- notifications=[noteType],
- defaultNotifications=[noteType],
- applicationIcon=applicationIcon,
- hostname=hostname,
- password=password,
- port=port,
- )
- result = growl.register()
- if result is not True:
- return result
-
- return growl.notify(
- noteType=noteType,
- title=title,
- description=description,
- icon=notificationIcon,
- sticky=sticky,
- priority=priority,
- callback=callback,
- identifier=identifier,
- )
- except Exception:
- # We want the "mini" function to be simple and swallow Exceptions
- # in order to be less invasive
- logger.exception("Growl error")
-
-if __name__ == '__main__':
- # If we're running this module directly we're likely running it as a test
- # so extra debugging is useful
- logging.basicConfig(level=logging.INFO)
- mini('Testing mini notification')
diff --git a/libs/apprise/plugins/NotifyGrowl/gntp/shim.py b/libs/apprise/plugins/NotifyGrowl/gntp/shim.py
deleted file mode 100644
index 46952f06d..000000000
--- a/libs/apprise/plugins/NotifyGrowl/gntp/shim.py
+++ /dev/null
@@ -1,45 +0,0 @@
-# Copyright: 2013 Paul Traylor
-# These sources are released under the terms of the MIT license: see LICENSE
-
-"""
-Python2.5 and Python3.3 compatibility shim
-
-Heavily inspirted by the "six" library.
-https://pypi.python.org/pypi/six
-"""
-
-import sys
-
-PY3 = sys.version_info[0] == 3
-
-if PY3:
- def b(s):
- if isinstance(s, bytes):
- return s
- return s.encode('utf8', 'replace')
-
- def u(s):
- if isinstance(s, bytes):
- return s.decode('utf8', 'replace')
- return s
-
- from io import BytesIO as StringIO
- from configparser import RawConfigParser
-else:
- def b(s):
- if isinstance(s, unicode): # noqa
- return s.encode('utf8', 'replace')
- return s
-
- def u(s):
- if isinstance(s, unicode): # noqa
- return s
- if isinstance(s, int):
- s = str(s)
- return unicode(s, "utf8", "replace") # noqa
-
- from StringIO import StringIO
- from ConfigParser import RawConfigParser
-
-b.__doc__ = "Ensure we have a byte string"
-u.__doc__ = "Ensure we have a unicode string"
diff --git a/libs/apprise/plugins/NotifyGrowl/gntp/version.py b/libs/apprise/plugins/NotifyGrowl/gntp/version.py
deleted file mode 100644
index 2166aacaa..000000000
--- a/libs/apprise/plugins/NotifyGrowl/gntp/version.py
+++ /dev/null
@@ -1,4 +0,0 @@
-# Copyright: 2013 Paul Traylor
-# These sources are released under the terms of the MIT license: see LICENSE
-
-__version__ = '1.0.2'
diff --git a/libs/apprise/plugins/NotifyIFTTT.py b/libs/apprise/plugins/NotifyIFTTT.py
index 0b1d42c0d..e6b40acd2 100644
--- a/libs/apprise/plugins/NotifyIFTTT.py
+++ b/libs/apprise/plugins/NotifyIFTTT.py
@@ -242,6 +242,7 @@ class NotifyIFTTT(NotifyBase):
data=dumps(payload),
headers=headers,
verify=self.verify_certificate,
+ timeout=self.request_timeout,
)
self.logger.debug(
u"IFTTT HTTP response headers: %r" % r.headers)
@@ -274,7 +275,7 @@ class NotifyIFTTT(NotifyBase):
except requests.RequestException as e:
self.logger.warning(
- 'A Connection error occured sending IFTTT:%s ' % (
+ 'A Connection error occurred sending IFTTT:%s ' % (
event) + 'notification.'
)
self.logger.debug('Socket Exception: %s' % str(e))
@@ -290,34 +291,29 @@ class NotifyIFTTT(NotifyBase):
Returns the URL built dynamically based on specified arguments.
"""
- # Define any arguments set
- args = {
- 'format': self.notify_format,
- 'overflow': self.overflow_mode,
- 'verify': 'yes' if self.verify_certificate else 'no',
- }
+ # Our URL parameters
+ params = self.url_parameters(privacy=privacy, *args, **kwargs)
# Store any new key/value pairs added to our list
- args.update({'+{}'.format(k): v for k, v in self.add_tokens})
- args.update({'-{}'.format(k): '' for k in self.del_tokens})
+ params.update({'+{}'.format(k): v for k, v in self.add_tokens})
+ params.update({'-{}'.format(k): '' for k in self.del_tokens})
- return '{schema}://{webhook_id}@{events}/?{args}'.format(
+ return '{schema}://{webhook_id}@{events}/?{params}'.format(
schema=self.secure_protocol,
webhook_id=self.pprint(self.webhook_id, privacy, safe=''),
events='/'.join([NotifyIFTTT.quote(x, safe='')
for x in self.events]),
- args=NotifyIFTTT.urlencode(args),
+ params=NotifyIFTTT.urlencode(params),
)
@staticmethod
def parse_url(url):
"""
Parses the URL and returns enough arguments that can allow
- us to substantiate this object.
+ us to re-instantiate this object.
"""
- results = NotifyBase.parse_url(url)
-
+ results = NotifyBase.parse_url(url, verify_host=False)
if not results:
# We're done early as we couldn't load the results
return results
@@ -356,16 +352,16 @@ class NotifyIFTTT(NotifyBase):
r'^https?://maker\.ifttt\.com/use/'
r'(?P<webhook_id>[A-Z0-9_-]+)'
r'/?(?P<events>([A-Z0-9_-]+/?)+)?'
- r'/?(?P<args>\?.+)?$', url, re.I)
+ r'/?(?P<params>\?.+)?$', url, re.I)
if result:
return NotifyIFTTT.parse_url(
- '{schema}://{webhook_id}{events}{args}'.format(
+ '{schema}://{webhook_id}{events}{params}'.format(
schema=NotifyIFTTT.secure_protocol,
webhook_id=result.group('webhook_id'),
events='' if not result.group('events')
else '@{}'.format(result.group('events')),
- args='' if not result.group('args')
- else result.group('args')))
+ params='' if not result.group('params')
+ else result.group('params')))
return None
diff --git a/libs/apprise/plugins/NotifyJSON.py b/libs/apprise/plugins/NotifyJSON.py
index ad772ca8f..d8a55ac82 100644
--- a/libs/apprise/plugins/NotifyJSON.py
+++ b/libs/apprise/plugins/NotifyJSON.py
@@ -128,15 +128,11 @@ class NotifyJSON(NotifyBase):
Returns the URL built dynamically based on specified arguments.
"""
- # Define any arguments set
- args = {
- 'format': self.notify_format,
- 'overflow': self.overflow_mode,
- 'verify': 'yes' if self.verify_certificate else 'no',
- }
+ # Our URL parameters
+ params = self.url_parameters(privacy=privacy, *args, **kwargs)
- # Append our headers into our args
- args.update({'+{}'.format(k): v for k, v in self.headers.items()})
+ # Append our headers into our parameters
+ params.update({'+{}'.format(k): v for k, v in self.headers.items()})
# Determine Authentication
auth = ''
@@ -153,14 +149,15 @@ class NotifyJSON(NotifyBase):
default_port = 443 if self.secure else 80
- return '{schema}://{auth}{hostname}{port}{fullpath}/?{args}'.format(
+ return '{schema}://{auth}{hostname}{port}{fullpath}/?{params}'.format(
schema=self.secure_protocol if self.secure else self.protocol,
auth=auth,
- hostname=NotifyJSON.quote(self.host, safe=''),
+ # never encode hostname since we're expecting it to be a valid one
+ hostname=self.host,
port='' if self.port is None or self.port == default_port
else ':{}'.format(self.port),
fullpath=NotifyJSON.quote(self.fullpath, safe='/'),
- args=NotifyJSON.urlencode(args),
+ params=NotifyJSON.urlencode(params),
)
def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs):
@@ -215,6 +212,7 @@ class NotifyJSON(NotifyBase):
headers=headers,
auth=auth,
verify=self.verify_certificate,
+ timeout=self.request_timeout,
)
if r.status_code != requests.codes.ok:
# We had a problem
@@ -238,7 +236,7 @@ class NotifyJSON(NotifyBase):
except requests.RequestException as e:
self.logger.warning(
- 'A Connection error occured sending JSON '
+ 'A Connection error occurred sending JSON '
'notification to %s.' % self.host)
self.logger.debug('Socket Exception: %s' % str(e))
@@ -251,11 +249,10 @@ class NotifyJSON(NotifyBase):
def parse_url(url):
"""
Parses the URL and returns enough arguments that can allow
- us to substantiate this object.
+ us to re-instantiate this object.
"""
results = NotifyBase.parse_url(url)
-
if not results:
# We're done early as we couldn't load the results
return results
diff --git a/libs/apprise/plugins/NotifyJoin.py b/libs/apprise/plugins/NotifyJoin.py
index 278ddaef8..d5e3f9471 100644
--- a/libs/apprise/plugins/NotifyJoin.py
+++ b/libs/apprise/plugins/NotifyJoin.py
@@ -280,6 +280,7 @@ class NotifyJoin(NotifyBase):
data=payload,
headers=headers,
verify=self.verify_certificate,
+ timeout=self.request_timeout,
)
if r.status_code != requests.codes.ok:
@@ -308,7 +309,7 @@ class NotifyJoin(NotifyBase):
except requests.RequestException as e:
self.logger.warning(
- 'A Connection error occured sending Join:%s '
+ 'A Connection error occurred sending Join:%s '
'notification.' % target
)
self.logger.debug('Socket Exception: %s' % str(e))
@@ -331,33 +332,32 @@ class NotifyJoin(NotifyBase):
JoinPriority.EMERGENCY: 'emergency',
}
- # Define any arguments set
- args = {
- 'format': self.notify_format,
- 'overflow': self.overflow_mode,
+ # Define any URL parameters
+ params = {
'priority':
_map[self.template_args['priority']['default']]
if self.priority not in _map else _map[self.priority],
'image': 'yes' if self.include_image else 'no',
- 'verify': 'yes' if self.verify_certificate else 'no',
}
- return '{schema}://{apikey}/{targets}/?{args}'.format(
+ # Extend our parameters
+ params.update(self.url_parameters(privacy=privacy, *args, **kwargs))
+
+ return '{schema}://{apikey}/{targets}/?{params}'.format(
schema=self.secure_protocol,
apikey=self.pprint(self.apikey, privacy, safe=''),
targets='/'.join([NotifyJoin.quote(x, safe='')
for x in self.targets]),
- args=NotifyJoin.urlencode(args))
+ params=NotifyJoin.urlencode(params))
@staticmethod
def parse_url(url):
"""
Parses the URL and returns enough arguments that can allow
- us to substantiate this object.
+ us to re-instantiate this object.
"""
- results = NotifyBase.parse_url(url)
-
+ results = NotifyBase.parse_url(url, verify_host=False)
if not results:
# We're done early as we couldn't load the results
return results
diff --git a/libs/apprise/plugins/NotifyKavenegar.py b/libs/apprise/plugins/NotifyKavenegar.py
index bf9b75252..cd5726367 100644
--- a/libs/apprise/plugins/NotifyKavenegar.py
+++ b/libs/apprise/plugins/NotifyKavenegar.py
@@ -263,6 +263,7 @@ class NotifyKavenegar(NotifyBase):
params=payload,
headers=headers,
verify=self.verify_certificate,
+ timeout=self.request_timeout,
)
if r.status_code not in (
@@ -310,7 +311,7 @@ class NotifyKavenegar(NotifyBase):
except requests.RequestException as e:
self.logger.warning(
- 'A Connection error occured sending Kavenegar:%s ' % (
+ 'A Connection error occurred sending Kavenegar:%s ' % (
', '.join(self.targets)) + 'notification.'
)
self.logger.debug('Socket Exception: %s' % str(e))
@@ -325,30 +326,25 @@ class NotifyKavenegar(NotifyBase):
Returns the URL built dynamically based on specified arguments.
"""
- # Define any arguments set
- args = {
- 'format': self.notify_format,
- 'overflow': self.overflow_mode,
- 'verify': 'yes' if self.verify_certificate else 'no',
- }
+ # Our URL parameters
+ params = self.url_parameters(privacy=privacy, *args, **kwargs)
- return '{schema}://{source}{apikey}/{targets}?{args}'.format(
+ return '{schema}://{source}{apikey}/{targets}?{params}'.format(
schema=self.secure_protocol,
source='' if not self.source else '{}@'.format(self.source),
apikey=self.pprint(self.apikey, privacy, safe=''),
targets='/'.join(
[NotifyKavenegar.quote(x, safe='') for x in self.targets]),
- args=NotifyKavenegar.urlencode(args))
+ params=NotifyKavenegar.urlencode(params))
@staticmethod
def parse_url(url):
"""
Parses the URL and returns enough arguments that can allow
- us to substantiate this object.
+ us to re-instantiate this object.
"""
results = NotifyBase.parse_url(url, verify_host=False)
-
if not results:
# We're done early as we couldn't load the results
return results
diff --git a/libs/apprise/plugins/NotifyKumulos.py b/libs/apprise/plugins/NotifyKumulos.py
index 4833045f9..8506aef3d 100644
--- a/libs/apprise/plugins/NotifyKumulos.py
+++ b/libs/apprise/plugins/NotifyKumulos.py
@@ -163,6 +163,7 @@ class NotifyKumulos(NotifyBase):
headers=headers,
auth=auth,
verify=self.verify_certificate,
+ timeout=self.request_timeout,
)
if r.status_code != requests.codes.ok:
# We had a problem
@@ -187,7 +188,7 @@ class NotifyKumulos(NotifyBase):
except requests.RequestException as e:
self.logger.warning(
- 'A Connection error occured sending Kumulos '
+ 'A Connection error occurred sending Kumulos '
'notification.')
self.logger.debug('Socket Exception: %s' % str(e))
@@ -199,29 +200,24 @@ class NotifyKumulos(NotifyBase):
Returns the URL built dynamically based on specified arguments.
"""
- # Define any arguments set
- args = {
- 'format': self.notify_format,
- 'overflow': self.overflow_mode,
- 'verify': 'yes' if self.verify_certificate else 'no',
- }
+ # Our URL parameters
+ params = self.url_parameters(privacy=privacy, *args, **kwargs)
- return '{schema}://{apikey}/{serverkey}/?{args}'.format(
+ return '{schema}://{apikey}/{serverkey}/?{params}'.format(
schema=self.secure_protocol,
apikey=self.pprint(self.apikey, privacy, safe=''),
serverkey=self.pprint(self.serverkey, privacy, safe=''),
- args=NotifyKumulos.urlencode(args),
+ params=NotifyKumulos.urlencode(params),
)
@staticmethod
def parse_url(url):
"""
Parses the URL and returns enough arguments that can allow
- us to substantiate this object.
+ us to re-instantiate this object.
"""
- results = NotifyBase.parse_url(url)
-
+ results = NotifyBase.parse_url(url, verify_host=False)
if not results:
# We're done early as we couldn't load the results
return results
diff --git a/libs/apprise/plugins/NotifyMSG91.py b/libs/apprise/plugins/NotifyMSG91.py
index 17676bf74..68176fb93 100644
--- a/libs/apprise/plugins/NotifyMSG91.py
+++ b/libs/apprise/plugins/NotifyMSG91.py
@@ -276,6 +276,7 @@ class NotifyMSG91(NotifyBase):
data=payload,
headers=headers,
verify=self.verify_certificate,
+ timeout=self.request_timeout,
)
if r.status_code != requests.codes.ok:
@@ -302,7 +303,7 @@ class NotifyMSG91(NotifyBase):
except requests.RequestException as e:
self.logger.warning(
- 'A Connection error occured sending MSG91:%s '
+ 'A Connection error occurred sending MSG91:%s '
'notification.' % ','.join(self.targets)
)
self.logger.debug('Socket Exception: %s' % str(e))
@@ -316,34 +317,33 @@ class NotifyMSG91(NotifyBase):
Returns the URL built dynamically based on specified arguments.
"""
- # Define any arguments set
- args = {
- 'format': self.notify_format,
- 'overflow': self.overflow_mode,
- 'verify': 'yes' if self.verify_certificate else 'no',
+ # Define any URL parameters
+ params = {
'route': str(self.route),
}
+ # Extend our parameters
+ params.update(self.url_parameters(privacy=privacy, *args, **kwargs))
+
if self.country:
- args['country'] = str(self.country)
+ params['country'] = str(self.country)
- return '{schema}://{authkey}/{targets}/?{args}'.format(
+ return '{schema}://{authkey}/{targets}/?{params}'.format(
schema=self.secure_protocol,
authkey=self.pprint(self.authkey, privacy, safe=''),
targets='/'.join(
[NotifyMSG91.quote(x, safe='') for x in self.targets]),
- args=NotifyMSG91.urlencode(args))
+ params=NotifyMSG91.urlencode(params))
@staticmethod
def parse_url(url):
"""
Parses the URL and returns enough arguments that can allow
- us to substantiate this object.
+ us to re-instantiate this object.
"""
- results = NotifyBase.parse_url(url)
-
+ results = NotifyBase.parse_url(url, verify_host=False)
if not results:
# We're done early as we couldn't load the results
return results
diff --git a/libs/apprise/plugins/NotifyMSTeams.py b/libs/apprise/plugins/NotifyMSTeams.py
index 2f0815345..b12c5e450 100644
--- a/libs/apprise/plugins/NotifyMSTeams.py
+++ b/libs/apprise/plugins/NotifyMSTeams.py
@@ -240,6 +240,7 @@ class NotifyMSTeams(NotifyBase):
data=dumps(payload),
headers=headers,
verify=self.verify_certificate,
+ timeout=self.request_timeout,
)
if r.status_code != requests.codes.ok:
# We had a problem
@@ -264,7 +265,7 @@ class NotifyMSTeams(NotifyBase):
except requests.RequestException as e:
self.logger.warning(
- 'A Connection error occured sending MSTeams notification.')
+ 'A Connection error occurred sending MSTeams notification.')
self.logger.debug('Socket Exception: %s' % str(e))
# We failed
@@ -277,32 +278,31 @@ class NotifyMSTeams(NotifyBase):
Returns the URL built dynamically based on specified arguments.
"""
- # Define any arguments set
- args = {
- 'format': self.notify_format,
- 'overflow': self.overflow_mode,
+ # Define any URL parameters
+ params = {
'image': 'yes' if self.include_image else 'no',
- 'verify': 'yes' if self.verify_certificate else 'no',
}
+ # Extend our parameters
+ params.update(self.url_parameters(privacy=privacy, *args, **kwargs))
+
return '{schema}://{token_a}/{token_b}/{token_c}/'\
- '?{args}'.format(
+ '?{params}'.format(
schema=self.secure_protocol,
token_a=self.pprint(self.token_a, privacy, safe=''),
token_b=self.pprint(self.token_b, privacy, safe=''),
token_c=self.pprint(self.token_c, privacy, safe=''),
- args=NotifyMSTeams.urlencode(args),
+ params=NotifyMSTeams.urlencode(params),
)
@staticmethod
def parse_url(url):
"""
Parses the URL and returns enough arguments that can allow
- us to substantiate this object.
+ us to re-instantiate this object.
"""
results = NotifyBase.parse_url(url, verify_host=False)
-
if not results:
# We're done early as we couldn't load the results
return results
@@ -359,16 +359,16 @@ class NotifyMSTeams(NotifyBase):
r'IncomingWebhook/'
r'(?P<token_b>[A-Z0-9]+)/'
r'(?P<token_c>[A-Z0-9-]+)/?'
- r'(?P<args>\?.+)?$', url, re.I)
+ r'(?P<params>\?.+)?$', url, re.I)
if result:
return NotifyMSTeams.parse_url(
- '{schema}://{token_a}/{token_b}/{token_c}/{args}'.format(
+ '{schema}://{token_a}/{token_b}/{token_c}/{params}'.format(
schema=NotifyMSTeams.secure_protocol,
token_a=result.group('token_a'),
token_b=result.group('token_b'),
token_c=result.group('token_c'),
- args='' if not result.group('args')
- else result.group('args')))
+ params='' if not result.group('params')
+ else result.group('params')))
return None
diff --git a/libs/apprise/plugins/NotifyMailgun.py b/libs/apprise/plugins/NotifyMailgun.py
index 7dfd1248d..e876a5bda 100644
--- a/libs/apprise/plugins/NotifyMailgun.py
+++ b/libs/apprise/plugins/NotifyMailgun.py
@@ -269,6 +269,7 @@ class NotifyMailgun(NotifyBase):
data=payload,
headers=headers,
verify=self.verify_certificate,
+ timeout=self.request_timeout,
)
if r.status_code != requests.codes.ok:
@@ -298,7 +299,7 @@ class NotifyMailgun(NotifyBase):
except requests.RequestException as e:
self.logger.warning(
- 'A Connection error occured sending Mailgun:%s ' % (
+ 'A Connection error occurred sending Mailgun:%s ' % (
email) + 'notification.'
)
self.logger.debug('Socket Exception: %s' % str(e))
@@ -314,36 +315,35 @@ class NotifyMailgun(NotifyBase):
Returns the URL built dynamically based on specified arguments.
"""
- # Define any arguments set
- args = {
- 'format': self.notify_format,
- 'overflow': self.overflow_mode,
- 'verify': 'yes' if self.verify_certificate else 'no',
+ # Define any URL parameters
+ params = {
'region': self.region_name,
}
+ # Extend our parameters
+ params.update(self.url_parameters(privacy=privacy, *args, **kwargs))
+
if self.from_name is not None:
# from_name specified; pass it back on the url
- args['name'] = self.from_name
+ params['name'] = self.from_name
- return '{schema}://{user}@{host}/{apikey}/{targets}/?{args}'.format(
+ return '{schema}://{user}@{host}/{apikey}/{targets}/?{params}'.format(
schema=self.secure_protocol,
host=self.host,
user=NotifyMailgun.quote(self.user, safe=''),
apikey=self.pprint(self.apikey, privacy, safe=''),
targets='/'.join(
[NotifyMailgun.quote(x, safe='') for x in self.targets]),
- args=NotifyMailgun.urlencode(args))
+ params=NotifyMailgun.urlencode(params))
@staticmethod
def parse_url(url):
"""
Parses the URL and returns enough arguments that can allow
- us to substantiate this object.
+ us to re-instantiate this object.
"""
- results = NotifyBase.parse_url(url)
-
+ results = NotifyBase.parse_url(url, verify_host=False)
if not results:
# We're done early as we couldn't load the results
return results
diff --git a/libs/apprise/plugins/NotifyMatrix.py b/libs/apprise/plugins/NotifyMatrix.py
index 13e7fbd30..dd72352e2 100644
--- a/libs/apprise/plugins/NotifyMatrix.py
+++ b/libs/apprise/plugins/NotifyMatrix.py
@@ -319,6 +319,7 @@ class NotifyMatrix(NotifyBase):
data=dumps(payload),
headers=headers,
verify=self.verify_certificate,
+ timeout=self.request_timeout,
)
if r.status_code != requests.codes.ok:
# We had a problem
@@ -343,7 +344,7 @@ class NotifyMatrix(NotifyBase):
except requests.RequestException as e:
self.logger.warning(
- 'A Connection error occured sending Matrix notification.'
+ 'A Connection error occurred sending Matrix notification.'
)
self.logger.debug('Socket Exception: %s' % str(e))
# Return; we're done
@@ -927,6 +928,7 @@ class NotifyMatrix(NotifyBase):
params=params,
headers=headers,
verify=self.verify_certificate,
+ timeout=self.request_timeout,
)
response = loads(r.content)
@@ -986,7 +988,7 @@ class NotifyMatrix(NotifyBase):
except requests.RequestException as e:
self.logger.warning(
- 'A Connection error occured while registering with Matrix'
+ 'A Connection error occurred while registering with Matrix'
' server.')
self.logger.debug('Socket Exception: %s' % str(e))
# Return; we're done
@@ -1009,15 +1011,15 @@ class NotifyMatrix(NotifyBase):
Returns the URL built dynamically based on specified arguments.
"""
- # Define any arguments set
- args = {
- 'format': self.notify_format,
- 'overflow': self.overflow_mode,
+ # Define any URL parameters
+ params = {
'image': 'yes' if self.include_image else 'no',
- 'verify': 'yes' if self.verify_certificate else 'no',
'mode': self.mode,
}
+ # Extend our parameters
+ params.update(self.url_parameters(privacy=privacy, *args, **kwargs))
+
# Determine Authentication
auth = ''
if self.user and self.password:
@@ -1034,21 +1036,21 @@ class NotifyMatrix(NotifyBase):
default_port = 443 if self.secure else 80
- return '{schema}://{auth}{hostname}{port}/{rooms}?{args}'.format(
+ return '{schema}://{auth}{hostname}{port}/{rooms}?{params}'.format(
schema=self.secure_protocol if self.secure else self.protocol,
auth=auth,
hostname=NotifyMatrix.quote(self.host, safe=''),
port='' if self.port is None
or self.port == default_port else ':{}'.format(self.port),
rooms=NotifyMatrix.quote('/'.join(self.rooms)),
- args=NotifyMatrix.urlencode(args),
+ params=NotifyMatrix.urlencode(params),
)
@staticmethod
def parse_url(url):
"""
Parses the URL and returns enough arguments that can allow
- us to substantiate this object.
+ us to re-instantiate this object.
"""
results = NotifyBase.parse_url(url, verify_host=False)
@@ -1067,34 +1069,12 @@ class NotifyMatrix(NotifyBase):
if 'to' in results['qsd'] and len(results['qsd']['to']):
results['targets'] += NotifyMatrix.parse_list(results['qsd']['to'])
- # Thumbnail (old way)
- if 'thumbnail' in results['qsd']:
- # Deprication Notice issued for v0.7.5
- NotifyMatrix.logger.deprecate(
- 'The Matrix URL contains the parameter '
- '"thumbnail=" which will be deprecated in an upcoming '
- 'release. Please use "image=" instead.'
- )
-
- # use image= for consistency with the other plugins but we also
- # support thumbnail= for backwards compatibility.
- results['include_image'] = \
- parse_bool(results['qsd'].get(
- 'image', results['qsd'].get('thumbnail', False)))
-
- # Webhook (old way)
- if 'webhook' in results['qsd']:
- # Deprication Notice issued for v0.7.5
- NotifyMatrix.logger.deprecate(
- 'The Matrix URL contains the parameter '
- '"webhook=" which will be deprecated in an upcoming '
- 'release. Please use "mode=" instead.'
- )
+ # Boolean to include an image or not
+ results['include_image'] = parse_bool(results['qsd'].get(
+ 'image', NotifyMatrix.template_args['image']['default']))
- # use mode= for consistency with the other plugins but we also
- # support webhook= for backwards compatibility.
- results['mode'] = results['qsd'].get(
- 'mode', results['qsd'].get('webhook'))
+ # Get our mode
+ results['mode'] = results['qsd'].get('mode')
# t2bot detection... look for just a hostname, and/or just a user/host
# if we match this; we can go ahead and set the mode (but only if
@@ -1117,16 +1097,16 @@ class NotifyMatrix(NotifyBase):
result = re.match(
r'^https?://webhooks\.t2bot\.io/api/v1/matrix/hook/'
r'(?P<webhook_token>[A-Z0-9_-]+)/?'
- r'(?P<args>\?.+)?$', url, re.I)
+ r'(?P<params>\?.+)?$', url, re.I)
if result:
mode = 'mode={}'.format(MatrixWebhookMode.T2BOT)
return NotifyMatrix.parse_url(
- '{schema}://{webhook_token}/{args}'.format(
+ '{schema}://{webhook_token}/{params}'.format(
schema=NotifyMatrix.secure_protocol,
webhook_token=result.group('webhook_token'),
- args='?{}'.format(mode) if not result.group('args')
- else '{}&{}'.format(result.group('args'), mode)))
+ params='?{}'.format(mode) if not result.group('params')
+ else '{}&{}'.format(result.group('params'), mode)))
return None
diff --git a/libs/apprise/plugins/NotifyMatterMost.py b/libs/apprise/plugins/NotifyMatterMost.py
index 84bb93edd..edd8202d6 100644
--- a/libs/apprise/plugins/NotifyMatterMost.py
+++ b/libs/apprise/plugins/NotifyMatterMost.py
@@ -227,6 +227,7 @@ class NotifyMatterMost(NotifyBase):
data=dumps(payload),
headers=headers,
verify=self.verify_certificate,
+ timeout=self.request_timeout,
)
if r.status_code != requests.codes.ok:
@@ -259,7 +260,7 @@ class NotifyMatterMost(NotifyBase):
except requests.RequestException as e:
self.logger.warning(
- 'A Connection error occured sending MatterMost '
+ 'A Connection error occurred sending MatterMost '
'notification{}.'.format(
'' if not channel
else ' to channel {}'.format(channel)))
@@ -277,19 +278,19 @@ class NotifyMatterMost(NotifyBase):
Returns the URL built dynamically based on specified arguments.
"""
- # Define any arguments set
- args = {
- 'format': self.notify_format,
- 'overflow': self.overflow_mode,
+ # Define any URL parameters
+ params = {
'image': 'yes' if self.include_image else 'no',
- 'verify': 'yes' if self.verify_certificate else 'no',
}
+ # Extend our parameters
+ params.update(self.url_parameters(privacy=privacy, *args, **kwargs))
+
if self.channels:
# historically the value only accepted one channel and is
# therefore identified as 'channel'. Channels have always been
# optional, so that is why this setting is nested in an if block
- args['channel'] = ','.join(self.channels)
+ params['channel'] = ','.join(self.channels)
default_port = 443 if self.secure else self.default_port
default_schema = self.secure_protocol if self.secure else self.protocol
@@ -303,27 +304,28 @@ class NotifyMatterMost(NotifyBase):
return \
'{schema}://{botname}{hostname}{port}{fullpath}{authtoken}' \
- '/?{args}'.format(
+ '/?{params}'.format(
schema=default_schema,
botname=botname,
- hostname=NotifyMatterMost.quote(self.host, safe=''),
+ # never encode hostname since we're expecting it to be a valid
+ # one
+ hostname=self.host,
port='' if not self.port or self.port == default_port
else ':{}'.format(self.port),
fullpath='/' if not self.fullpath else '{}/'.format(
NotifyMatterMost.quote(self.fullpath, safe='/')),
authtoken=self.pprint(self.authtoken, privacy, safe=''),
- args=NotifyMatterMost.urlencode(args),
+ params=NotifyMatterMost.urlencode(params),
)
@staticmethod
def parse_url(url):
"""
Parses the URL and returns enough arguments that can allow
- us to substantiate this object.
+ us to re-instantiate this object.
"""
results = NotifyBase.parse_url(url)
-
if not results:
# We're done early as we couldn't load the results
return results
diff --git a/libs/apprise/plugins/NotifyMessageBird.py b/libs/apprise/plugins/NotifyMessageBird.py
index 78ac9d58a..1032f49b8 100644
--- a/libs/apprise/plugins/NotifyMessageBird.py
+++ b/libs/apprise/plugins/NotifyMessageBird.py
@@ -234,6 +234,7 @@ class NotifyMessageBird(NotifyBase):
data=payload,
headers=headers,
verify=self.verify_certificate,
+ timeout=self.request_timeout,
)
# Sample output of a successful transmission
@@ -297,7 +298,7 @@ class NotifyMessageBird(NotifyBase):
except requests.RequestException as e:
self.logger.warning(
- 'A Connection error occured sending MessageBird:%s ' % (
+ 'A Connection error occurred sending MessageBird:%s ' % (
target) + 'notification.'
)
self.logger.debug('Socket Exception: %s' % str(e))
@@ -313,31 +314,26 @@ class NotifyMessageBird(NotifyBase):
Returns the URL built dynamically based on specified arguments.
"""
- # Define any arguments set
- args = {
- 'format': self.notify_format,
- 'overflow': self.overflow_mode,
- 'verify': 'yes' if self.verify_certificate else 'no',
- }
+ # Our URL parameters
+ params = self.url_parameters(privacy=privacy, *args, **kwargs)
- return '{schema}://{apikey}/{source}/{targets}/?{args}'.format(
+ return '{schema}://{apikey}/{source}/{targets}/?{params}'.format(
schema=self.secure_protocol,
apikey=self.pprint(self.apikey, privacy, safe=''),
source=self.source,
targets='/'.join(
[NotifyMessageBird.quote(x, safe='') for x in self.targets]),
- args=NotifyMessageBird.urlencode(args))
+ params=NotifyMessageBird.urlencode(params))
@staticmethod
def parse_url(url):
"""
Parses the URL and returns enough arguments that can allow
- us to substantiate this object.
+ us to re-instantiate this object.
"""
- results = NotifyBase.parse_url(url)
-
+ results = NotifyBase.parse_url(url, verify_host=False)
if not results:
# We're done early as we couldn't load the results
return results
@@ -352,7 +348,7 @@ class NotifyMessageBird(NotifyBase):
except IndexError:
# No path specified... this URL is potentially un-parseable; we can
# hope for a from= entry
- pass
+ results['source'] = None
# The hostname is our authentication key
results['apikey'] = NotifyMessageBird.unquote(results['host'])
diff --git a/libs/apprise/plugins/NotifyNexmo.py b/libs/apprise/plugins/NotifyNexmo.py
index 5fd662ad7..05c9f7fcd 100644
--- a/libs/apprise/plugins/NotifyNexmo.py
+++ b/libs/apprise/plugins/NotifyNexmo.py
@@ -82,7 +82,7 @@ class NotifyNexmo(NotifyBase):
'name': _('API Key'),
'type': 'string',
'required': True,
- 'regex': (r'^AC[a-z0-9]{8}$', 'i'),
+ 'regex': (r'^[a-z0-9]+$', 'i'),
'private': True,
},
'secret': {
@@ -90,7 +90,7 @@ class NotifyNexmo(NotifyBase):
'type': 'string',
'private': True,
'required': True,
- 'regex': (r'^[a-z0-9]{16}$', 'i'),
+ 'regex': (r'^[a-z0-9]+$', 'i'),
},
'from_phone': {
'name': _('From Phone No'),
@@ -280,6 +280,7 @@ class NotifyNexmo(NotifyBase):
data=payload,
headers=headers,
verify=self.verify_certificate,
+ timeout=self.request_timeout,
)
if r.status_code != requests.codes.ok:
@@ -308,7 +309,7 @@ class NotifyNexmo(NotifyBase):
except requests.RequestException as e:
self.logger.warning(
- 'A Connection error occured sending Nexmo:%s '
+ 'A Connection error occurred sending Nexmo:%s '
'notification.' % target
)
self.logger.debug('Socket Exception: %s' % str(e))
@@ -324,15 +325,15 @@ class NotifyNexmo(NotifyBase):
Returns the URL built dynamically based on specified arguments.
"""
- # Define any arguments set
- args = {
- 'format': self.notify_format,
- 'overflow': self.overflow_mode,
- 'verify': 'yes' if self.verify_certificate else 'no',
+ # Define any URL parameters
+ params = {
'ttl': str(self.ttl),
}
- return '{schema}://{key}:{secret}@{source}/{targets}/?{args}'.format(
+ # Extend our parameters
+ params.update(self.url_parameters(privacy=privacy, *args, **kwargs))
+
+ return '{schema}://{key}:{secret}@{source}/{targets}/?{params}'.format(
schema=self.secure_protocol,
key=self.pprint(self.apikey, privacy, safe=''),
secret=self.pprint(
@@ -340,17 +341,16 @@ class NotifyNexmo(NotifyBase):
source=NotifyNexmo.quote(self.source, safe=''),
targets='/'.join(
[NotifyNexmo.quote(x, safe='') for x in self.targets]),
- args=NotifyNexmo.urlencode(args))
+ params=NotifyNexmo.urlencode(params))
@staticmethod
def parse_url(url):
"""
Parses the URL and returns enough arguments that can allow
- us to substantiate this object.
+ us to re-instantiate this object.
"""
results = NotifyBase.parse_url(url, verify_host=False)
-
if not results:
# We're done early as we couldn't load the results
return results
diff --git a/libs/apprise/plugins/NotifyNextcloud.py b/libs/apprise/plugins/NotifyNextcloud.py
index 33211f64a..240ed0aa1 100644
--- a/libs/apprise/plugins/NotifyNextcloud.py
+++ b/libs/apprise/plugins/NotifyNextcloud.py
@@ -185,6 +185,7 @@ class NotifyNextcloud(NotifyBase):
headers=headers,
auth=auth,
verify=self.verify_certificate,
+ timeout=self.request_timeout,
)
if r.status_code != requests.codes.ok:
# We had a problem
@@ -210,7 +211,7 @@ class NotifyNextcloud(NotifyBase):
except requests.RequestException as e:
self.logger.warning(
- 'A Connection error occured sending Nextcloud '
+ 'A Connection error occurred sending Nextcloud '
'notification.',
)
self.logger.debug('Socket Exception: %s' % str(e))
@@ -226,15 +227,11 @@ class NotifyNextcloud(NotifyBase):
Returns the URL built dynamically based on specified arguments.
"""
- # Define any arguments set
- args = {
- 'format': self.notify_format,
- 'overflow': self.overflow_mode,
- 'verify': 'yes' if self.verify_certificate else 'no',
- }
+ # Create URL parameters from our headers
+ params = {'+{}'.format(k): v for k, v in self.headers.items()}
- # Append our headers into our args
- args.update({'+{}'.format(k): v for k, v in self.headers.items()})
+ # Our URL parameters
+ params = self.url_parameters(privacy=privacy, *args, **kwargs)
# Determine Authentication
auth = ''
@@ -251,24 +248,26 @@ class NotifyNextcloud(NotifyBase):
default_port = 443 if self.secure else 80
- return '{schema}://{auth}{hostname}{port}/{targets}?{args}' \
+ return '{schema}://{auth}{hostname}{port}/{targets}?{params}' \
.format(
schema=self.secure_protocol
if self.secure else self.protocol,
auth=auth,
- hostname=NotifyNextcloud.quote(self.host, safe=''),
+ # never encode hostname since we're expecting it to be a
+ # valid one
+ hostname=self.host,
port='' if self.port is None or self.port == default_port
else ':{}'.format(self.port),
targets='/'.join([NotifyNextcloud.quote(x)
for x in self.targets]),
- args=NotifyNextcloud.urlencode(args),
+ params=NotifyNextcloud.urlencode(params),
)
@staticmethod
def parse_url(url):
"""
Parses the URL and returns enough arguments that can allow
- us to substantiate this object.
+ us to re-instantiate this object.
"""
diff --git a/libs/apprise/plugins/NotifyNotica.py b/libs/apprise/plugins/NotifyNotica.py
index 038c421d3..3dcc0172a 100644
--- a/libs/apprise/plugins/NotifyNotica.py
+++ b/libs/apprise/plugins/NotifyNotica.py
@@ -103,6 +103,14 @@ class NotifyNotica(NotifyBase):
'{schema}://{user}@{host}:{port}/{token}',
'{schema}://{user}:{password}@{host}/{token}',
'{schema}://{user}:{password}@{host}:{port}/{token}',
+
+ # Self-hosted notica servers (with custom path)
+ '{schema}://{host}{path}{token}',
+ '{schema}://{host}:{port}{path}{token}',
+ '{schema}://{user}@{host}{path}{token}',
+ '{schema}://{user}@{host}:{port}{path}{token}',
+ '{schema}://{user}:{password}@{host}{path}{token}',
+ '{schema}://{user}:{password}@{host}:{port}{path}{token}',
)
# Define our template tokens
@@ -133,6 +141,12 @@ class NotifyNotica(NotifyBase):
'type': 'string',
'private': True,
},
+ 'path': {
+ 'name': _('Path'),
+ 'type': 'string',
+ 'map_to': 'fullpath',
+ 'default': '/',
+ },
})
# Define any kwargs we're using
@@ -228,6 +242,7 @@ class NotifyNotica(NotifyBase):
headers=headers,
auth=auth,
verify=self.verify_certificate,
+ timeout=self.request_timeout,
)
if r.status_code != requests.codes.ok:
# We had a problem
@@ -251,7 +266,7 @@ class NotifyNotica(NotifyBase):
except requests.RequestException as e:
self.logger.warning(
- 'A Connection error occured sending Notica notification.',
+ 'A Connection error occurred sending Notica notification.',
)
self.logger.debug('Socket Exception: %s' % str(e))
@@ -265,25 +280,21 @@ class NotifyNotica(NotifyBase):
Returns the URL built dynamically based on specified arguments.
"""
- # Define any arguments set
- args = {
- 'format': self.notify_format,
- 'overflow': self.overflow_mode,
- 'verify': 'yes' if self.verify_certificate else 'no',
- }
+ # Our URL parameters
+ params = self.url_parameters(privacy=privacy, *args, **kwargs)
if self.mode == NoticaMode.OFFICIAL:
# Official URLs are easy to assemble
- return '{schema}://{token}/?{args}'.format(
+ return '{schema}://{token}/?{params}'.format(
schema=self.protocol,
token=self.pprint(self.token, privacy, safe=''),
- args=NotifyNotica.urlencode(args),
+ params=NotifyNotica.urlencode(params),
)
# If we reach here then we are assembling a self hosted URL
- # Append our headers into our args
- args.update({'+{}'.format(k): v for k, v in self.headers.items()})
+ # Append URL parameters from our headers
+ params.update({'+{}'.format(k): v for k, v in self.headers.items()})
# Authorization can be used for self-hosted sollutions
auth = ''
@@ -302,7 +313,7 @@ class NotifyNotica(NotifyBase):
default_port = 443 if self.secure else 80
- return '{schema}://{auth}{hostname}{port}{fullpath}{token}/?{args}' \
+ return '{schema}://{auth}{hostname}{port}{fullpath}{token}/?{params}' \
.format(
schema=self.secure_protocol
if self.secure else self.protocol,
@@ -313,14 +324,14 @@ class NotifyNotica(NotifyBase):
fullpath=NotifyNotica.quote(
self.fullpath, safe='/'),
token=self.pprint(self.token, privacy, safe=''),
- args=NotifyNotica.urlencode(args),
+ params=NotifyNotica.urlencode(params),
)
@staticmethod
def parse_url(url):
"""
Parses the URL and returns enough arguments that can allow
- us to substantiate this object.
+ us to re-instantiate this object.
"""
results = NotifyBase.parse_url(url, verify_host=False)
@@ -367,14 +378,14 @@ class NotifyNotica(NotifyBase):
result = re.match(
r'^https?://notica\.us/?'
- r'\??(?P<token>[^&]+)([&\s]*(?P<args>.+))?$', url, re.I)
+ r'\??(?P<token>[^&]+)([&\s]*(?P<params>.+))?$', url, re.I)
if result:
return NotifyNotica.parse_url(
- '{schema}://{token}/{args}'.format(
+ '{schema}://{token}/{params}'.format(
schema=NotifyNotica.protocol,
token=result.group('token'),
- args='' if not result.group('args')
- else '?{}'.format(result.group('args'))))
+ params='' if not result.group('params')
+ else '?{}'.format(result.group('params'))))
return None
diff --git a/libs/apprise/plugins/NotifyNotifico.py b/libs/apprise/plugins/NotifyNotifico.py
index c76180ff9..b0970e193 100644
--- a/libs/apprise/plugins/NotifyNotifico.py
+++ b/libs/apprise/plugins/NotifyNotifico.py
@@ -199,20 +199,20 @@ class NotifyNotifico(NotifyBase):
Returns the URL built dynamically based on specified arguments.
"""
- # Define any arguments set
- args = {
- 'format': self.notify_format,
- 'overflow': self.overflow_mode,
- 'verify': 'yes' if self.verify_certificate else 'no',
+ # Define any URL parameters
+ params = {
'color': 'yes' if self.color else 'no',
'prefix': 'yes' if self.prefix else 'no',
}
- return '{schema}://{proj}/{hook}/?{args}'.format(
+ # Extend our parameters
+ params.update(self.url_parameters(privacy=privacy, *args, **kwargs))
+
+ return '{schema}://{proj}/{hook}/?{params}'.format(
schema=self.secure_protocol,
proj=self.pprint(self.project_id, privacy, safe=''),
hook=self.pprint(self.msghook, privacy, safe=''),
- args=NotifyNotifico.urlencode(args),
+ params=NotifyNotifico.urlencode(params),
)
def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs):
@@ -288,6 +288,7 @@ class NotifyNotifico(NotifyBase):
params=payload,
headers=headers,
verify=self.verify_certificate,
+ timeout=self.request_timeout,
)
if r.status_code != requests.codes.ok:
# We had a problem
@@ -311,7 +312,7 @@ class NotifyNotifico(NotifyBase):
except requests.RequestException as e:
self.logger.warning(
- 'A Connection error occured sending Notifico '
+ 'A Connection error occurred sending Notifico '
'notification.')
self.logger.debug('Socket Exception: %s' % str(e))
@@ -324,11 +325,11 @@ class NotifyNotifico(NotifyBase):
def parse_url(url):
"""
Parses the URL and returns enough arguments that can allow
- us to substantiate this object.
+ us to re-instantiate this object.
"""
- results = NotifyBase.parse_url(url)
+ results = NotifyBase.parse_url(url, verify_host=False)
if not results:
# We're done early as we couldn't load the results
return results
@@ -364,15 +365,15 @@ class NotifyNotifico(NotifyBase):
r'^https?://n\.tkte\.ch/h/'
r'(?P<proj>[0-9]+)/'
r'(?P<hook>[A-Z0-9]+)/?'
- r'(?P<args>\?.+)?$', url, re.I)
+ r'(?P<params>\?.+)?$', url, re.I)
if result:
return NotifyNotifico.parse_url(
- '{schema}://{proj}/{hook}/{args}'.format(
+ '{schema}://{proj}/{hook}/{params}'.format(
schema=NotifyNotifico.secure_protocol,
proj=result.group('proj'),
hook=result.group('hook'),
- args='' if not result.group('args')
- else result.group('args')))
+ params='' if not result.group('params')
+ else result.group('params')))
return None
diff --git a/libs/apprise/plugins/NotifyProwl.py b/libs/apprise/plugins/NotifyProwl.py
index 3f6ca7927..8341064d3 100644
--- a/libs/apprise/plugins/NotifyProwl.py
+++ b/libs/apprise/plugins/NotifyProwl.py
@@ -191,6 +191,7 @@ class NotifyProwl(NotifyBase):
data=payload,
headers=headers,
verify=self.verify_certificate,
+ timeout=self.request_timeout,
)
if r.status_code != requests.codes.ok:
# We had a problem
@@ -215,7 +216,7 @@ class NotifyProwl(NotifyBase):
except requests.RequestException as e:
self.logger.warning(
- 'A Connection error occured sending Prowl notification.')
+ 'A Connection error occurred sending Prowl notification.')
self.logger.debug('Socket Exception: %s' % str(e))
# Return; we're done
@@ -236,31 +237,30 @@ class NotifyProwl(NotifyBase):
ProwlPriority.EMERGENCY: 'emergency',
}
- # Define any arguments set
- args = {
- 'format': self.notify_format,
- 'overflow': self.overflow_mode,
+ # Define any URL parameters
+ params = {
'priority': 'normal' if self.priority not in _map
else _map[self.priority],
- 'verify': 'yes' if self.verify_certificate else 'no',
}
- return '{schema}://{apikey}/{providerkey}/?{args}'.format(
+ # Extend our parameters
+ params.update(self.url_parameters(privacy=privacy, *args, **kwargs))
+
+ return '{schema}://{apikey}/{providerkey}/?{params}'.format(
schema=self.secure_protocol,
apikey=self.pprint(self.apikey, privacy, safe=''),
providerkey=self.pprint(self.providerkey, privacy, safe=''),
- args=NotifyProwl.urlencode(args),
+ params=NotifyProwl.urlencode(params),
)
@staticmethod
def parse_url(url):
"""
Parses the URL and returns enough arguments that can allow
- us to substantiate this object.
+ us to re-instantiate this object.
"""
- results = NotifyBase.parse_url(url)
-
+ results = NotifyBase.parse_url(url, verify_host=False)
if not results:
# We're done early as we couldn't load the results
return results
diff --git a/libs/apprise/plugins/NotifyPushBullet.py b/libs/apprise/plugins/NotifyPushBullet.py
index 4a3dd8494..9bae32f96 100644
--- a/libs/apprise/plugins/NotifyPushBullet.py
+++ b/libs/apprise/plugins/NotifyPushBullet.py
@@ -28,7 +28,7 @@ from json import dumps
from json import loads
from .NotifyBase import NotifyBase
-from ..utils import GET_EMAIL_RE
+from ..utils import is_email
from ..common import NotifyType
from ..utils import parse_list
from ..utils import validate_regex
@@ -230,22 +230,29 @@ class NotifyPushBullet(NotifyBase):
'body': body,
}
- if recipient is PUSHBULLET_SEND_TO_ALL:
+ # Check if an email was defined
+ match = is_email(recipient)
+ if match:
+ payload['email'] = match['full_email']
+ self.logger.debug(
+ "PushBullet recipient {} parsed as an email address"
+ .format(recipient))
+
+ elif recipient is PUSHBULLET_SEND_TO_ALL:
# Send to all
pass
- elif GET_EMAIL_RE.match(recipient):
- payload['email'] = recipient
- self.logger.debug(
- "Recipient '%s' is an email address" % recipient)
-
elif recipient[0] == '#':
payload['channel_tag'] = recipient[1:]
- self.logger.debug("Recipient '%s' is a channel" % recipient)
+ self.logger.debug(
+ "PushBullet recipient {} parsed as a channel"
+ .format(recipient))
else:
payload['device_iden'] = recipient
- self.logger.debug("Recipient '%s' is a device" % recipient)
+ self.logger.debug(
+ "PushBullet recipient {} parsed as a device"
+ .format(recipient))
okay, response = self._send(
self.notify_url.format('pushes'), payload)
@@ -315,6 +322,7 @@ class NotifyPushBullet(NotifyBase):
files=files,
auth=auth,
verify=self.verify_certificate,
+ timeout=self.request_timeout,
)
try:
@@ -352,14 +360,14 @@ class NotifyPushBullet(NotifyBase):
except requests.RequestException as e:
self.logger.warning(
- 'A Connection error occured communicating with PushBullet.')
+ 'A Connection error occurred communicating with PushBullet.')
self.logger.debug('Socket Exception: %s' % str(e))
return False, response
except (OSError, IOError) as e:
self.logger.warning(
- 'An I/O error occured while reading {}.'.format(
+ 'An I/O error occurred while reading {}.'.format(
payload.name if payload else 'attachment'))
self.logger.debug('I/O Exception: %s' % str(e))
return False, response
@@ -375,12 +383,8 @@ class NotifyPushBullet(NotifyBase):
Returns the URL built dynamically based on specified arguments.
"""
- # Define any arguments set
- args = {
- 'format': self.notify_format,
- 'overflow': self.overflow_mode,
- 'verify': 'yes' if self.verify_certificate else 'no',
- }
+ # Our URL parameters
+ params = self.url_parameters(privacy=privacy, *args, **kwargs)
targets = '/'.join([NotifyPushBullet.quote(x) for x in self.targets])
if targets == PUSHBULLET_SEND_TO_ALL:
@@ -388,21 +392,20 @@ class NotifyPushBullet(NotifyBase):
# it from the recipients list
targets = ''
- return '{schema}://{accesstoken}/{targets}/?{args}'.format(
+ return '{schema}://{accesstoken}/{targets}/?{params}'.format(
schema=self.secure_protocol,
accesstoken=self.pprint(self.accesstoken, privacy, safe=''),
targets=targets,
- args=NotifyPushBullet.urlencode(args))
+ params=NotifyPushBullet.urlencode(params))
@staticmethod
def parse_url(url):
"""
Parses the URL and returns enough arguments that can allow
- us to substantiate this object.
+ us to re-instantiate this object.
"""
- results = NotifyBase.parse_url(url)
-
+ results = NotifyBase.parse_url(url, verify_host=False)
if not results:
# We're done early as we couldn't load the results
return results
diff --git a/libs/apprise/plugins/NotifyPushSafer.py b/libs/apprise/plugins/NotifyPushSafer.py
index 8e056087e..2d74dc371 100644
--- a/libs/apprise/plugins/NotifyPushSafer.py
+++ b/libs/apprise/plugins/NotifyPushSafer.py
@@ -576,7 +576,7 @@ class NotifyPushSafer(NotifyBase):
except (OSError, IOError) as e:
self.logger.warning(
- 'An I/O error occured while reading {}.'.format(
+ 'An I/O error occurred while reading {}.'.format(
attachment.name if attachment else 'attachment'))
self.logger.debug('I/O Exception: %s' % str(e))
return False
@@ -693,6 +693,7 @@ class NotifyPushSafer(NotifyBase):
data=payload,
headers=headers,
verify=self.verify_certificate,
+ timeout=self.request_timeout,
)
try:
@@ -746,7 +747,7 @@ class NotifyPushSafer(NotifyBase):
except requests.RequestException as e:
self.logger.warning(
- 'A Connection error occured communicating with PushSafer.')
+ 'A Connection error occurred communicating with PushSafer.')
self.logger.debug('Socket Exception: %s' % str(e))
return False, response
@@ -756,29 +757,25 @@ class NotifyPushSafer(NotifyBase):
Returns the URL built dynamically based on specified arguments.
"""
- # Define any arguments set
- args = {
- 'format': self.notify_format,
- 'overflow': self.overflow_mode,
- 'verify': 'yes' if self.verify_certificate else 'no',
- }
+ # Our URL parameters
+ params = self.url_parameters(privacy=privacy, *args, **kwargs)
if self.priority is not None:
# Store our priority; but only if it was specified
- args['priority'] = \
+ params['priority'] = \
next((key for key, value in PUSHSAFER_PRIORITY_MAP.items()
if value == self.priority),
DEFAULT_PRIORITY) # pragma: no cover
if self.sound is not None:
# Store our sound; but only if it was specified
- args['sound'] = \
+ params['sound'] = \
next((key for key, value in PUSHSAFER_SOUND_MAP.items()
if value == self.sound), '') # pragma: no cover
if self.vibration is not None:
# Store our vibration; but only if it was specified
- args['vibration'] = str(self.vibration)
+ params['vibration'] = str(self.vibration)
targets = '/'.join([NotifyPushSafer.quote(x) for x in self.targets])
if targets == PUSHSAFER_SEND_TO_ALL:
@@ -786,20 +783,20 @@ class NotifyPushSafer(NotifyBase):
# it from the recipients list
targets = ''
- return '{schema}://{privatekey}/{targets}?{args}'.format(
+ return '{schema}://{privatekey}/{targets}?{params}'.format(
schema=self.secure_protocol if self.secure else self.protocol,
privatekey=self.pprint(self.privatekey, privacy, safe=''),
targets=targets,
- args=NotifyPushSafer.urlencode(args))
+ params=NotifyPushSafer.urlencode(params))
@staticmethod
def parse_url(url):
"""
Parses the URL and returns enough arguments that can allow
- us to substantiate this object.
+ us to re-instantiate this object.
"""
- results = NotifyBase.parse_url(url)
+ results = NotifyBase.parse_url(url, verify_host=False)
if not results:
# We're done early as we couldn't load the results
return results
diff --git a/libs/apprise/plugins/NotifyPushed.py b/libs/apprise/plugins/NotifyPushed.py
index d9428393d..c6dfe6ad1 100644
--- a/libs/apprise/plugins/NotifyPushed.py
+++ b/libs/apprise/plugins/NotifyPushed.py
@@ -267,6 +267,7 @@ class NotifyPushed(NotifyBase):
data=dumps(payload),
headers=headers,
verify=self.verify_certificate,
+ timeout=self.request_timeout,
)
if r.status_code != requests.codes.ok:
@@ -291,7 +292,7 @@ class NotifyPushed(NotifyBase):
except requests.RequestException as e:
self.logger.warning(
- 'A Connection error occured sending Pushed notification.')
+ 'A Connection error occurred sending Pushed notification.')
self.logger.debug('Socket Exception: %s' % str(e))
# Return; we're done
@@ -304,14 +305,10 @@ class NotifyPushed(NotifyBase):
Returns the URL built dynamically based on specified arguments.
"""
- # Define any arguments set
- args = {
- 'format': self.notify_format,
- 'overflow': self.overflow_mode,
- 'verify': 'yes' if self.verify_certificate else 'no',
- }
+ # Our URL parameters
+ params = self.url_parameters(privacy=privacy, *args, **kwargs)
- return '{schema}://{app_key}/{app_secret}/{targets}/?{args}'.format(
+ return '{schema}://{app_key}/{app_secret}/{targets}/?{params}'.format(
schema=self.secure_protocol,
app_key=self.pprint(self.app_key, privacy, safe=''),
app_secret=self.pprint(
@@ -323,17 +320,16 @@ class NotifyPushed(NotifyBase):
# Users are prefixed with an @ symbol
['@{}'.format(x) for x in self.users],
)]),
- args=NotifyPushed.urlencode(args))
+ params=NotifyPushed.urlencode(params))
@staticmethod
def parse_url(url):
"""
Parses the URL and returns enough arguments that can allow
- us to substantiate this object.
+ us to re-instantiate this object.
"""
- results = NotifyBase.parse_url(url)
-
+ results = NotifyBase.parse_url(url, verify_host=False)
if not results:
# We're done early as we couldn't load the results
return results
diff --git a/libs/apprise/plugins/NotifyPushjet.py b/libs/apprise/plugins/NotifyPushjet.py
index 0dcb596d3..49e6596e6 100644
--- a/libs/apprise/plugins/NotifyPushjet.py
+++ b/libs/apprise/plugins/NotifyPushjet.py
@@ -60,10 +60,6 @@ class NotifyPushjet(NotifyBase):
'{schema}://{host}/{secret_key}',
'{schema}://{user}:{password}@{host}:{port}/{secret_key}',
'{schema}://{user}:{password}@{host}/{secret_key}',
-
- # Kept for backwards compatibility; will be depricated eventually
- '{schema}://{secret_key}@{host}',
- '{schema}://{secret_key}@{host}:{port}',
)
# Define our tokens
@@ -123,12 +119,8 @@ class NotifyPushjet(NotifyBase):
Returns the URL built dynamically based on specified arguments.
"""
- # Define any arguments set
- args = {
- 'format': self.notify_format,
- 'overflow': self.overflow_mode,
- 'verify': 'yes' if self.verify_certificate else 'no',
- }
+ # Our URL parameters
+ params = self.url_parameters(privacy=privacy, *args, **kwargs)
default_port = 443 if self.secure else 80
@@ -141,15 +133,16 @@ class NotifyPushjet(NotifyBase):
self.password, privacy, mode=PrivacyMode.Secret, safe=''),
)
- return '{schema}://{auth}{hostname}{port}/{secret}/?{args}'.format(
+ return '{schema}://{auth}{hostname}{port}/{secret}/?{params}'.format(
schema=self.secure_protocol if self.secure else self.protocol,
auth=auth,
- hostname=NotifyPushjet.quote(self.host, safe=''),
+ # never encode hostname since we're expecting it to be a valid one
+ hostname=self.host,
port='' if self.port is None or self.port == default_port
else ':{}'.format(self.port),
secret=self.pprint(
self.secret_key, privacy, mode=PrivacyMode.Secret, safe=''),
- args=NotifyPushjet.urlencode(args),
+ params=NotifyPushjet.urlencode(params),
)
def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs):
@@ -199,6 +192,7 @@ class NotifyPushjet(NotifyBase):
headers=headers,
auth=auth,
verify=self.verify_certificate,
+ timeout=self.request_timeout,
)
if r.status_code != requests.codes.ok:
# We had a problem
@@ -222,7 +216,7 @@ class NotifyPushjet(NotifyBase):
except requests.RequestException as e:
self.logger.warning(
- 'A Connection error occured sending Pushjet '
+ 'A Connection error occurred sending Pushjet '
'notification to %s.' % self.host)
self.logger.debug('Socket Exception: %s' % str(e))
@@ -235,7 +229,7 @@ class NotifyPushjet(NotifyBase):
def parse_url(url):
"""
Parses the URL and returns enough arguments that can allow
- us to substantiate this object.
+ us to re-instantiate this object.
Syntax:
pjet://hostname/secret_key
@@ -246,16 +240,8 @@ class NotifyPushjet(NotifyBase):
pjets://hostname:port/secret_key
pjets://user:pass@hostname/secret_key
pjets://user:pass@hostname:port/secret_key
-
- Legacy (Depricated) Syntax:
- pjet://secret_key@hostname
- pjet://secret_key@hostname:port
- pjets://secret_key@hostname
- pjets://secret_key@hostname:port
-
"""
results = NotifyBase.parse_url(url)
-
if not results:
# We're done early as we couldn't load the results
return results
@@ -276,22 +262,4 @@ class NotifyPushjet(NotifyBase):
results['secret_key'] = \
NotifyPushjet.unquote(results['qsd']['secret'])
- if results.get('secret_key') is None:
- # Deprication Notice issued for v0.7.9
- NotifyPushjet.logger.deprecate(
- 'The Pushjet URL contains secret_key in the user field'
- ' which will be deprecated in an upcoming '
- 'release. Please place this in the path of the URL instead.'
- )
-
- # Store it as it's value based on the user field
- results['secret_key'] = \
- NotifyPushjet.unquote(results.get('user'))
-
- # there is no way http-auth is enabled, be sure to unset the
- # current defined user (if present). This is done due to some
- # logic that takes place in the send() since we support http-auth.
- results['user'] = None
- results['password'] = None
-
return results
diff --git a/libs/apprise/plugins/NotifyPushover.py b/libs/apprise/plugins/NotifyPushover.py
index 48bcb786f..e9fdb7028 100644
--- a/libs/apprise/plugins/NotifyPushover.py
+++ b/libs/apprise/plugins/NotifyPushover.py
@@ -434,6 +434,7 @@ class NotifyPushover(NotifyBase):
files=files,
auth=auth,
verify=self.verify_certificate,
+ timeout=self.request_timeout,
)
if r.status_code != requests.codes.ok:
@@ -461,7 +462,7 @@ class NotifyPushover(NotifyBase):
except requests.RequestException as e:
self.logger.warning(
- 'A Connection error occured sending Pushover:%s ' % (
+ 'A Connection error occurred sending Pushover:%s ' % (
payload['device']) + 'notification.'
)
self.logger.debug('Socket Exception: %s' % str(e))
@@ -470,7 +471,7 @@ class NotifyPushover(NotifyBase):
except (OSError, IOError) as e:
self.logger.warning(
- 'An I/O error occured while reading {}.'.format(
+ 'An I/O error occurred while reading {}.'.format(
attach.name if attach else 'attachment'))
self.logger.debug('I/O Exception: %s' % str(e))
return False
@@ -496,19 +497,20 @@ class NotifyPushover(NotifyBase):
PushoverPriority.EMERGENCY: 'emergency',
}
- # Define any arguments set
- args = {
- 'format': self.notify_format,
- 'overflow': self.overflow_mode,
+ # Define any URL parameters
+ params = {
'priority':
_map[self.template_args['priority']['default']]
if self.priority not in _map else _map[self.priority],
- 'verify': 'yes' if self.verify_certificate else 'no',
}
+
# Only add expire and retry for emergency messages,
# pushover ignores for all other priorities
if self.priority == PushoverPriority.EMERGENCY:
- args.update({'expire': self.expire, 'retry': self.retry})
+ params.update({'expire': self.expire, 'retry': self.retry})
+
+ # Extend our parameters
+ params.update(self.url_parameters(privacy=privacy, *args, **kwargs))
# Escape our devices
devices = '/'.join([NotifyPushover.quote(x, safe='')
@@ -519,22 +521,21 @@ class NotifyPushover(NotifyBase):
# it from the devices list
devices = ''
- return '{schema}://{user_key}@{token}/{devices}/?{args}'.format(
+ return '{schema}://{user_key}@{token}/{devices}/?{params}'.format(
schema=self.secure_protocol,
user_key=self.pprint(self.user_key, privacy, safe=''),
token=self.pprint(self.token, privacy, safe=''),
devices=devices,
- args=NotifyPushover.urlencode(args))
+ params=NotifyPushover.urlencode(params))
@staticmethod
def parse_url(url):
"""
Parses the URL and returns enough arguments that can allow
- us to substantiate this object.
+ us to re-instantiate this object.
"""
- results = NotifyBase.parse_url(url)
-
+ results = NotifyBase.parse_url(url, verify_host=False)
if not results:
# We're done early as we couldn't load the results
return results
diff --git a/libs/apprise/plugins/NotifyRocketChat.py b/libs/apprise/plugins/NotifyRocketChat.py
index cca947edc..9beda2564 100644
--- a/libs/apprise/plugins/NotifyRocketChat.py
+++ b/libs/apprise/plugins/NotifyRocketChat.py
@@ -285,15 +285,15 @@ class NotifyRocketChat(NotifyBase):
Returns the URL built dynamically based on specified arguments.
"""
- # Define any arguments set
- args = {
- 'format': self.notify_format,
- 'overflow': self.overflow_mode,
- 'verify': 'yes' if self.verify_certificate else 'no',
+ # Define any URL parameters
+ params = {
'avatar': 'yes' if self.avatar else 'no',
'mode': self.mode,
}
+ # Extend our parameters
+ params.update(self.url_parameters(privacy=privacy, *args, **kwargs))
+
# Determine Authentication
if self.mode == RocketChatAuthMode.BASIC:
auth = '{user}:{password}@'.format(
@@ -310,10 +310,11 @@ class NotifyRocketChat(NotifyBase):
default_port = 443 if self.secure else 80
- return '{schema}://{auth}{hostname}{port}/{targets}/?{args}'.format(
+ return '{schema}://{auth}{hostname}{port}/{targets}/?{params}'.format(
schema=self.secure_protocol if self.secure else self.protocol,
auth=auth,
- hostname=NotifyRocketChat.quote(self.host, safe=''),
+ # never encode hostname since we're expecting it to be a valid one
+ hostname=self.host,
port='' if self.port is None or self.port == default_port
else ':{}'.format(self.port),
targets='/'.join(
@@ -325,7 +326,7 @@ class NotifyRocketChat(NotifyBase):
# Users
['@{}'.format(x) for x in self.users],
)]),
- args=NotifyRocketChat.urlencode(args),
+ params=NotifyRocketChat.urlencode(params),
)
def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs):
@@ -476,6 +477,7 @@ class NotifyRocketChat(NotifyBase):
data=payload,
headers=headers,
verify=self.verify_certificate,
+ timeout=self.request_timeout,
)
if r.status_code != requests.codes.ok:
# We had a problem
@@ -502,7 +504,7 @@ class NotifyRocketChat(NotifyBase):
except requests.RequestException as e:
self.logger.warning(
- 'A Connection error occured sending Rocket.Chat '
+ 'A Connection error occurred sending Rocket.Chat '
'{}:notification.'.format(self.mode))
self.logger.debug('Socket Exception: %s' % str(e))
@@ -529,6 +531,7 @@ class NotifyRocketChat(NotifyBase):
api_url,
data=payload,
verify=self.verify_certificate,
+ timeout=self.request_timeout,
)
if r.status_code != requests.codes.ok:
# We had a problem
@@ -570,13 +573,13 @@ class NotifyRocketChat(NotifyBase):
# - TypeError = r.content is None
# - AttributeError = r is None
self.logger.warning(
- 'A commuication error occured authenticating {} on '
+ 'A commuication error occurred authenticating {} on '
'Rocket.Chat.'.format(self.user))
return False
except requests.RequestException as e:
self.logger.warning(
- 'A connection error occured authenticating {} on '
+ 'A connection error occurred authenticating {} on '
'Rocket.Chat.'.format(self.user))
self.logger.debug('Socket Exception: %s' % str(e))
return False
@@ -595,6 +598,7 @@ class NotifyRocketChat(NotifyBase):
api_url,
headers=self.headers,
verify=self.verify_certificate,
+ timeout=self.request_timeout,
)
if r.status_code != requests.codes.ok:
# We had a problem
@@ -622,7 +626,7 @@ class NotifyRocketChat(NotifyBase):
except requests.RequestException as e:
self.logger.warning(
- 'A Connection error occured logging off the '
+ 'A Connection error occurred logging off the '
'Rocket.Chat server')
self.logger.debug('Socket Exception: %s' % str(e))
return False
@@ -633,7 +637,7 @@ class NotifyRocketChat(NotifyBase):
def parse_url(url):
"""
Parses the URL and returns enough arguments that can allow
- us to substantiate this object.
+ us to re-instantiate this object.
"""
@@ -665,7 +669,6 @@ class NotifyRocketChat(NotifyBase):
)
results = NotifyBase.parse_url(url)
-
if not results:
# We're done early as we couldn't load the results
return results
diff --git a/libs/apprise/plugins/NotifyRyver.py b/libs/apprise/plugins/NotifyRyver.py
index b34b56686..b825dd774 100644
--- a/libs/apprise/plugins/NotifyRyver.py
+++ b/libs/apprise/plugins/NotifyRyver.py
@@ -236,6 +236,7 @@ class NotifyRyver(NotifyBase):
data=dumps(payload),
headers=headers,
verify=self.verify_certificate,
+ timeout=self.request_timeout,
)
if r.status_code != requests.codes.ok:
@@ -260,7 +261,7 @@ class NotifyRyver(NotifyBase):
except requests.RequestException as e:
self.logger.warning(
- 'A Connection error occured sending Ryver:%s ' % (
+ 'A Connection error occurred sending Ryver:%s ' % (
self.organization) + 'notification.'
)
self.logger.debug('Socket Exception: %s' % str(e))
@@ -273,15 +274,15 @@ class NotifyRyver(NotifyBase):
Returns the URL built dynamically based on specified arguments.
"""
- # Define any arguments set
- args = {
- 'format': self.notify_format,
- 'overflow': self.overflow_mode,
+ # Define any URL parameters
+ params = {
'image': 'yes' if self.include_image else 'no',
'mode': self.mode,
- 'verify': 'yes' if self.verify_certificate else 'no',
}
+ # Extend our parameters
+ params.update(self.url_parameters(privacy=privacy, *args, **kwargs))
+
# Determine if there is a botname present
botname = ''
if self.user:
@@ -289,24 +290,23 @@ class NotifyRyver(NotifyBase):
botname=NotifyRyver.quote(self.user, safe=''),
)
- return '{schema}://{botname}{organization}/{token}/?{args}'.format(
+ return '{schema}://{botname}{organization}/{token}/?{params}'.format(
schema=self.secure_protocol,
botname=botname,
organization=NotifyRyver.quote(self.organization, safe=''),
token=self.pprint(self.token, privacy, safe=''),
- args=NotifyRyver.urlencode(args),
+ params=NotifyRyver.urlencode(params),
)
@staticmethod
def parse_url(url):
"""
Parses the URL and returns enough arguments that can allow
- us to substantiate this object.
+ us to re-instantiate this object.
"""
- results = NotifyBase.parse_url(url)
-
+ results = NotifyBase.parse_url(url, verify_host=False)
if not results:
# We're done early as we couldn't load the results
return results
@@ -323,19 +323,8 @@ class NotifyRyver(NotifyBase):
# no token
results['token'] = None
- if 'webhook' in results['qsd']:
- # Deprication Notice issued for v0.7.5
- NotifyRyver.logger.deprecate(
- 'The Ryver URL contains the parameter '
- '"webhook=" which will be deprecated in an upcoming '
- 'release. Please use "mode=" instead.'
- )
-
- # use mode= for consistency with the other plugins but we also
- # support webhook= for backwards compatibility.
- results['mode'] = results['qsd'].get(
- 'mode', results['qsd'].get(
- 'webhook', RyverWebhookMode.RYVER))
+ # Retrieve the mode
+ results['mode'] = results['qsd'].get('mode', RyverWebhookMode.RYVER)
# use image= for consistency with the other plugins
results['include_image'] = \
@@ -352,15 +341,15 @@ class NotifyRyver(NotifyBase):
result = re.match(
r'^https?://(?P<org>[A-Z0-9_-]+)\.ryver\.com/application/webhook/'
r'(?P<webhook_token>[A-Z0-9]+)/?'
- r'(?P<args>\?.+)?$', url, re.I)
+ r'(?P<params>\?.+)?$', url, re.I)
if result:
return NotifyRyver.parse_url(
- '{schema}://{org}/{webhook_token}/{args}'.format(
+ '{schema}://{org}/{webhook_token}/{params}'.format(
schema=NotifyRyver.secure_protocol,
org=result.group('org'),
webhook_token=result.group('webhook_token'),
- args='' if not result.group('args')
- else result.group('args')))
+ params='' if not result.group('params')
+ else result.group('params')))
return None
diff --git a/libs/apprise/plugins/NotifySNS.py b/libs/apprise/plugins/NotifySNS.py
index 6045c136e..adbbdfbb3 100644
--- a/libs/apprise/plugins/NotifySNS.py
+++ b/libs/apprise/plugins/NotifySNS.py
@@ -342,6 +342,7 @@ class NotifySNS(NotifyBase):
data=payload,
headers=headers,
verify=self.verify_certificate,
+ timeout=self.request_timeout,
)
if r.status_code != requests.codes.ok:
@@ -368,7 +369,7 @@ class NotifySNS(NotifyBase):
except requests.RequestException as e:
self.logger.warning(
- 'A Connection error occured sending AWS '
+ 'A Connection error occurred sending AWS '
'notification to "%s".' % (to),
)
self.logger.debug('Socket Exception: %s' % str(e))
@@ -579,15 +580,11 @@ class NotifySNS(NotifyBase):
Returns the URL built dynamically based on specified arguments.
"""
- # Define any arguments set
- args = {
- 'format': self.notify_format,
- 'overflow': self.overflow_mode,
- 'verify': 'yes' if self.verify_certificate else 'no',
- }
+ # Our URL parameters
+ params = self.url_parameters(privacy=privacy, *args, **kwargs)
return '{schema}://{key_id}/{key_secret}/{region}/{targets}/'\
- '?{args}'.format(
+ '?{params}'.format(
schema=self.secure_protocol,
key_id=self.pprint(self.aws_access_key_id, privacy, safe=''),
key_secret=self.pprint(
@@ -601,18 +598,17 @@ class NotifySNS(NotifyBase):
# Topics are prefixed with a pound/hashtag symbol
['#{}'.format(x) for x in self.topics],
)]),
- args=NotifySNS.urlencode(args),
+ params=NotifySNS.urlencode(params),
)
@staticmethod
def parse_url(url):
"""
Parses the URL and returns enough arguments that can allow
- us to substantiate this object.
+ us to re-instantiate this object.
"""
- results = NotifyBase.parse_url(url)
-
+ results = NotifyBase.parse_url(url, verify_host=False)
if not results:
# We're done early as we couldn't load the results
return results
diff --git a/libs/apprise/plugins/NotifySendGrid.py b/libs/apprise/plugins/NotifySendGrid.py
index 7c0c1a12e..12f829fb3 100644
--- a/libs/apprise/plugins/NotifySendGrid.py
+++ b/libs/apprise/plugins/NotifySendGrid.py
@@ -50,7 +50,7 @@ from .NotifyBase import NotifyBase
from ..common import NotifyFormat
from ..common import NotifyType
from ..utils import parse_list
-from ..utils import GET_EMAIL_RE
+from ..utils import is_email
from ..utils import validate_regex
from ..AppriseLocale import gettext_lazy as _
@@ -170,18 +170,15 @@ class NotifySendGrid(NotifyBase):
self.logger.warning(msg)
raise TypeError(msg)
- self.from_email = from_email
- try:
- result = GET_EMAIL_RE.match(self.from_email)
- if not result:
- # let outer exception handle this
- raise TypeError
-
- except (TypeError, AttributeError):
- msg = 'Invalid ~From~ email specified: {}'.format(self.from_email)
+ result = is_email(from_email)
+ if not result:
+ msg = 'Invalid ~From~ email specified: {}'.format(from_email)
self.logger.warning(msg)
raise TypeError(msg)
+ # Store email address
+ self.from_email = result['full_email']
+
# Acquire Targets (To Emails)
self.targets = list()
@@ -201,8 +198,9 @@ class NotifySendGrid(NotifyBase):
# Validate recipients (to:) and drop bad ones:
for recipient in parse_list(targets):
- if GET_EMAIL_RE.match(recipient):
- self.targets.append(recipient)
+ result = is_email(recipient)
+ if result:
+ self.targets.append(result['full_email'])
continue
self.logger.warning(
@@ -213,8 +211,9 @@ class NotifySendGrid(NotifyBase):
# Validate recipients (cc:) and drop bad ones:
for recipient in parse_list(cc):
- if GET_EMAIL_RE.match(recipient):
- self.cc.add(recipient)
+ result = is_email(recipient)
+ if result:
+ self.cc.add(result['full_email'])
continue
self.logger.warning(
@@ -225,8 +224,9 @@ class NotifySendGrid(NotifyBase):
# Validate recipients (bcc:) and drop bad ones:
for recipient in parse_list(bcc):
- if GET_EMAIL_RE.match(recipient):
- self.bcc.add(recipient)
+ result = is_email(recipient)
+ if result:
+ self.bcc.add(result['full_email'])
continue
self.logger.warning(
@@ -245,41 +245,38 @@ class NotifySendGrid(NotifyBase):
Returns the URL built dynamically based on specified arguments.
"""
- # Define any arguments set
- args = {
- 'format': self.notify_format,
- 'overflow': self.overflow_mode,
- 'verify': 'yes' if self.verify_certificate else 'no',
- }
+ # Our URL parameters
+ params = self.url_parameters(privacy=privacy, *args, **kwargs)
if len(self.cc) > 0:
# Handle our Carbon Copy Addresses
- args['cc'] = ','.join(self.cc)
+ params['cc'] = ','.join(self.cc)
if len(self.bcc) > 0:
# Handle our Blind Carbon Copy Addresses
- args['bcc'] = ','.join(self.bcc)
+ params['bcc'] = ','.join(self.bcc)
if self.template:
# Handle our Template ID if if was specified
- args['template'] = self.template
+ params['template'] = self.template
- # Append our template_data into our args
- args.update({'+{}'.format(k): v
- for k, v in self.template_data.items()})
+ # Append our template_data into our parameter list
+ params.update(
+ {'+{}'.format(k): v for k, v in self.template_data.items()})
# a simple boolean check as to whether we display our target emails
# or not
has_targets = \
not (len(self.targets) == 1 and self.targets[0] == self.from_email)
- return '{schema}://{apikey}:{from_email}/{targets}?{args}'.format(
+ return '{schema}://{apikey}:{from_email}/{targets}?{params}'.format(
schema=self.secure_protocol,
apikey=self.pprint(self.apikey, privacy, safe=''),
- from_email=self.quote(self.from_email, safe='@'),
+ # never encode email since it plays a huge role in our hostname
+ from_email=self.from_email,
targets='' if not has_targets else '/'.join(
[NotifySendGrid.quote(x, safe='') for x in self.targets]),
- args=NotifySendGrid.urlencode(args),
+ params=NotifySendGrid.urlencode(params),
)
def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs):
@@ -361,6 +358,7 @@ class NotifySendGrid(NotifyBase):
data=dumps(payload),
headers=headers,
verify=self.verify_certificate,
+ timeout=self.request_timeout,
)
if r.status_code not in (
requests.codes.ok, requests.codes.accepted):
@@ -390,7 +388,7 @@ class NotifySendGrid(NotifyBase):
except requests.RequestException as e:
self.logger.warning(
- 'A Connection error occured sending SendGrid '
+ 'A Connection error occurred sending SendGrid '
'notification to {}.'.format(target))
self.logger.debug('Socket Exception: %s' % str(e))
@@ -404,7 +402,7 @@ class NotifySendGrid(NotifyBase):
def parse_url(url):
"""
Parses the URL and returns enough arguments that can allow
- us to substantiate this object.
+ us to re-instantiate this object.
"""
diff --git a/libs/apprise/plugins/NotifySimplePush.py b/libs/apprise/plugins/NotifySimplePush.py
index 8093d0e44..dd192e794 100644
--- a/libs/apprise/plugins/NotifySimplePush.py
+++ b/libs/apprise/plugins/NotifySimplePush.py
@@ -142,14 +142,6 @@ class NotifySimplePush(NotifyBase):
# Default Event Name
self.event = None
- # Encrypt Message (providing support is available)
- if self.password and self.user and not CRYPTOGRAPHY_AVAILABLE:
- # Provide the end user at least some notification that they're
- # not getting what they asked for
- self.logger.warning(
- 'SimplePush extended encryption is not supported by this '
- 'system.')
-
# Used/cached in _encrypt() function
self._iv = None
self._iv_hex = None
@@ -189,6 +181,15 @@ class NotifySimplePush(NotifyBase):
Perform SimplePush Notification
"""
+ # Encrypt Message (providing support is available)
+ if self.password and self.user and not CRYPTOGRAPHY_AVAILABLE:
+ # Provide the end user at least some notification that they're
+ # not getting what they asked for
+ self.logger.warning(
+ "Authenticated SimplePush Notifications are not supported by "
+ "this system; `pip install cryptography`.")
+ return False
+
headers = {
'User-Agent': self.app_id,
'Content-type': "application/x-www-form-urlencoded",
@@ -236,6 +237,7 @@ class NotifySimplePush(NotifyBase):
data=payload,
headers=headers,
verify=self.verify_certificate,
+ timeout=self.request_timeout,
)
# Get our SimplePush response (if it's possible)
@@ -272,7 +274,7 @@ class NotifySimplePush(NotifyBase):
except requests.RequestException as e:
self.logger.warning(
- 'A Connection error occured sending SimplePush notification.')
+ 'A Connection error occurred sending SimplePush notification.')
self.logger.debug('Socket Exception: %s' % str(e))
# Return; we're done
@@ -285,15 +287,11 @@ class NotifySimplePush(NotifyBase):
Returns the URL built dynamically based on specified arguments.
"""
- # Define any arguments set
- args = {
- 'format': self.notify_format,
- 'overflow': self.overflow_mode,
- 'verify': 'yes' if self.verify_certificate else 'no',
- }
+ # Our URL parameters
+ params = self.url_parameters(privacy=privacy, *args, **kwargs)
if self.event:
- args['event'] = self.event
+ params['event'] = self.event
# Determine Authentication
auth = ''
@@ -305,21 +303,21 @@ class NotifySimplePush(NotifyBase):
self.password, privacy, mode=PrivacyMode.Secret, safe=''),
)
- return '{schema}://{auth}{apikey}/?{args}'.format(
+ return '{schema}://{auth}{apikey}/?{params}'.format(
schema=self.secure_protocol,
auth=auth,
apikey=self.pprint(self.apikey, privacy, safe=''),
- args=NotifySimplePush.urlencode(args),
+ params=NotifySimplePush.urlencode(params),
)
@staticmethod
def parse_url(url):
"""
Parses the URL and returns enough arguments that can allow
- us to substantiate this object.
+ us to re-instantiate this object.
"""
- results = NotifyBase.parse_url(url)
+ results = NotifyBase.parse_url(url, verify_host=False)
if not results:
# We're done early as we couldn't load the results
return results
diff --git a/libs/apprise/plugins/NotifySinch.py b/libs/apprise/plugins/NotifySinch.py
index 454cdbf73..c3cc32675 100644
--- a/libs/apprise/plugins/NotifySinch.py
+++ b/libs/apprise/plugins/NotifySinch.py
@@ -322,6 +322,7 @@ class NotifySinch(NotifyBase):
data=json.dumps(payload),
headers=headers,
verify=self.verify_certificate,
+ timeout=self.request_timeout,
)
# The responsne might look like:
@@ -383,7 +384,7 @@ class NotifySinch(NotifyBase):
except requests.RequestException as e:
self.logger.warning(
- 'A Connection error occured sending Sinch:%s ' % (
+ 'A Connection error occurred sending Sinch:%s ' % (
target) + 'notification.'
)
self.logger.debug('Socket Exception: %s' % str(e))
@@ -399,15 +400,15 @@ class NotifySinch(NotifyBase):
Returns the URL built dynamically based on specified arguments.
"""
- # Define any arguments set
- args = {
- 'format': self.notify_format,
- 'overflow': self.overflow_mode,
- 'verify': 'yes' if self.verify_certificate else 'no',
+ # Define any URL parameters
+ params = {
'region': self.region,
}
- return '{schema}://{spi}:{token}@{source}/{targets}/?{args}'.format(
+ # Extend our parameters
+ params.update(self.url_parameters(privacy=privacy, *args, **kwargs))
+
+ return '{schema}://{spi}:{token}@{source}/{targets}/?{params}'.format(
schema=self.secure_protocol,
spi=self.pprint(
self.service_plan_id, privacy, mode=PrivacyMode.Tail, safe=''),
@@ -415,13 +416,13 @@ class NotifySinch(NotifyBase):
source=NotifySinch.quote(self.source, safe=''),
targets='/'.join(
[NotifySinch.quote(x, safe='') for x in self.targets]),
- args=NotifySinch.urlencode(args))
+ params=NotifySinch.urlencode(params))
@staticmethod
def parse_url(url):
"""
Parses the URL and returns enough arguments that can allow
- us to substantiate this object.
+ us to re-instantiate this object.
"""
results = NotifyBase.parse_url(url, verify_host=False)
diff --git a/libs/apprise/plugins/NotifySlack.py b/libs/apprise/plugins/NotifySlack.py
index d4e4f6112..3e024a64c 100644
--- a/libs/apprise/plugins/NotifySlack.py
+++ b/libs/apprise/plugins/NotifySlack.py
@@ -505,6 +505,7 @@ class NotifySlack(NotifyBase):
headers=headers,
files=files,
verify=self.verify_certificate,
+ timeout=self.request_timeout,
)
if r.status_code != requests.codes.ok:
@@ -622,14 +623,14 @@ class NotifySlack(NotifyBase):
# }
except requests.RequestException as e:
self.logger.warning(
- 'A Connection error occured posting {}to Slack.'.format(
+ 'A Connection error occurred posting {}to Slack.'.format(
attach.name if attach else ''))
self.logger.debug('Socket Exception: %s' % str(e))
return False
except (OSError, IOError) as e:
self.logger.warning(
- 'An I/O error occured while reading {}.'.format(
+ 'An I/O error occurred while reading {}.'.format(
attach.name if attach else 'attachment'))
self.logger.debug('I/O Exception: %s' % str(e))
return False
@@ -648,15 +649,15 @@ class NotifySlack(NotifyBase):
Returns the URL built dynamically based on specified arguments.
"""
- # Define any arguments set
- args = {
- 'format': self.notify_format,
- 'overflow': self.overflow_mode,
+ # Define any URL parameters
+ params = {
'image': 'yes' if self.include_image else 'no',
'footer': 'yes' if self.include_footer else 'no',
- 'verify': 'yes' if self.verify_certificate else 'no',
}
+ # Extend our parameters
+ params.update(self.url_parameters(privacy=privacy, *args, **kwargs))
+
if self.mode == SlackMode.WEBHOOK:
# Determine if there is a botname present
botname = ''
@@ -666,7 +667,7 @@ class NotifySlack(NotifyBase):
)
return '{schema}://{botname}{token_a}/{token_b}/{token_c}/'\
- '{targets}/?{args}'.format(
+ '{targets}/?{params}'.format(
schema=self.secure_protocol,
botname=botname,
token_a=self.pprint(self.token_a, privacy, safe=''),
@@ -675,23 +676,23 @@ class NotifySlack(NotifyBase):
targets='/'.join(
[NotifySlack.quote(x, safe='')
for x in self.channels]),
- args=NotifySlack.urlencode(args),
+ params=NotifySlack.urlencode(params),
)
# else -> self.mode == SlackMode.BOT:
return '{schema}://{access_token}/{targets}/'\
- '?{args}'.format(
+ '?{params}'.format(
schema=self.secure_protocol,
access_token=self.pprint(self.access_token, privacy, safe=''),
targets='/'.join(
[NotifySlack.quote(x, safe='') for x in self.channels]),
- args=NotifySlack.urlencode(args),
+ params=NotifySlack.urlencode(params),
)
@staticmethod
def parse_url(url):
"""
Parses the URL and returns enough arguments that can allow
- us to substantiate this object.
+ us to re-instantiate this object.
"""
results = NotifyBase.parse_url(url, verify_host=False)
@@ -760,16 +761,16 @@ class NotifySlack(NotifyBase):
r'(?P<token_a>[A-Z0-9]+)/'
r'(?P<token_b>[A-Z0-9]+)/'
r'(?P<token_c>[A-Z0-9]+)/?'
- r'(?P<args>\?.+)?$', url, re.I)
+ r'(?P<params>\?.+)?$', url, re.I)
if result:
return NotifySlack.parse_url(
- '{schema}://{token_a}/{token_b}/{token_c}/{args}'.format(
+ '{schema}://{token_a}/{token_b}/{token_c}/{params}'.format(
schema=NotifySlack.secure_protocol,
token_a=result.group('token_a'),
token_b=result.group('token_b'),
token_c=result.group('token_c'),
- args='' if not result.group('args')
- else result.group('args')))
+ params='' if not result.group('params')
+ else result.group('params')))
return None
diff --git a/libs/apprise/plugins/NotifySyslog.py b/libs/apprise/plugins/NotifySyslog.py
index a6506648f..2457410e2 100644
--- a/libs/apprise/plugins/NotifySyslog.py
+++ b/libs/apprise/plugins/NotifySyslog.py
@@ -233,32 +233,33 @@ class NotifySyslog(NotifyBase):
Returns the URL built dynamically based on specified arguments.
"""
- # Define any arguments set
- args = {
+ # Define any URL parameters
+ params = {
'logperror': 'yes' if self.log_perror else 'no',
'logpid': 'yes' if self.log_pid else 'no',
- 'format': self.notify_format,
- 'overflow': self.overflow_mode,
- 'verify': 'yes' if self.verify_certificate else 'no',
}
- return '{schema}://{facility}/?{args}'.format(
+ # Extend our parameters
+ params.update(self.url_parameters(privacy=privacy, *args, **kwargs))
+
+ return '{schema}://{facility}/?{params}'.format(
facility=self.template_tokens['facility']['default']
if self.facility not in SYSLOG_FACILITY_RMAP
else SYSLOG_FACILITY_RMAP[self.facility],
schema=self.secure_protocol,
- args=NotifySyslog.urlencode(args),
+ params=NotifySyslog.urlencode(params),
)
@staticmethod
def parse_url(url):
"""
Parses the URL and returns enough arguments that can allow
- us to substantiate this object.
+ us to re-instantiate this object.
"""
results = NotifyBase.parse_url(url, verify_host=False)
if not results:
+ # We're done early as we couldn't load the results
return results
# if specified; save hostname into facility
diff --git a/libs/apprise/plugins/NotifyTechulusPush.py b/libs/apprise/plugins/NotifyTechulusPush.py
index 6614decdc..5dcb33e5f 100644
--- a/libs/apprise/plugins/NotifyTechulusPush.py
+++ b/libs/apprise/plugins/NotifyTechulusPush.py
@@ -145,6 +145,7 @@ class NotifyTechulusPush(NotifyBase):
data=dumps(payload),
headers=headers,
verify=self.verify_certificate,
+ timeout=self.request_timeout,
)
if r.status_code not in (
requests.codes.ok, requests.codes.no_content):
@@ -171,7 +172,7 @@ class NotifyTechulusPush(NotifyBase):
except requests.RequestException as e:
self.logger.warning(
- 'A Connection error occured sending Techulus Push '
+ 'A Connection error occurred sending Techulus Push '
'notification.'
)
self.logger.debug('Socket Exception: %s' % str(e))
@@ -185,28 +186,23 @@ class NotifyTechulusPush(NotifyBase):
Returns the URL built dynamically based on specified arguments.
"""
- # Define any arguments set
- args = {
- 'format': self.notify_format,
- 'overflow': self.overflow_mode,
- 'verify': 'yes' if self.verify_certificate else 'no',
- }
+ # Our URL parameters
+ params = self.url_parameters(privacy=privacy, *args, **kwargs)
- return '{schema}://{apikey}/?{args}'.format(
+ return '{schema}://{apikey}/?{params}'.format(
schema=self.secure_protocol,
apikey=self.pprint(self.apikey, privacy, safe=''),
- args=NotifyTechulusPush.urlencode(args),
+ params=NotifyTechulusPush.urlencode(params),
)
@staticmethod
def parse_url(url):
"""
Parses the URL and returns enough arguments that can allow
- us to substantiate this object.
+ us to re-instantiate this object.
"""
results = NotifyBase.parse_url(url, verify_host=False)
-
if not results:
# We're done early as we couldn't load the results
return results
diff --git a/libs/apprise/plugins/NotifyTelegram.py b/libs/apprise/plugins/NotifyTelegram.py
index 0b6a2343f..4bfd2d368 100644
--- a/libs/apprise/plugins/NotifyTelegram.py
+++ b/libs/apprise/plugins/NotifyTelegram.py
@@ -229,23 +229,19 @@ class NotifyTelegram(NotifyBase):
# Parse our list
self.targets = parse_list(targets)
+ # if detect_owner is set to True, we will attempt to determine who
+ # the bot owner is based on the first person who messaged it. This
+ # is not a fool proof way of doing things as over time Telegram removes
+ # the message history for the bot. So what appears (later on) to be
+ # the first message to it, maybe another user who sent it a message
+ # much later. Users who set this flag should update their Apprise
+ # URL later to directly include the user that we should message.
self.detect_owner = detect_owner
if self.user:
# Treat this as a channel too
self.targets.append(self.user)
- if len(self.targets) == 0 and self.detect_owner:
- _id = self.detect_bot_owner()
- if _id:
- # Store our id
- self.targets.append(str(_id))
-
- if len(self.targets) == 0:
- err = 'No chat_id(s) were specified.'
- self.logger.warning(err)
- raise TypeError(err)
-
# Track whether or not we want to send an image with our notification
# or not.
self.include_image = include_image
@@ -325,6 +321,7 @@ class NotifyTelegram(NotifyBase):
files=files,
data=payload,
verify=self.verify_certificate,
+ timeout=self.request_timeout,
)
if r.status_code != requests.codes.ok:
@@ -349,7 +346,7 @@ class NotifyTelegram(NotifyBase):
except requests.RequestException as e:
self.logger.warning(
- 'A connection error occured posting Telegram '
+ 'A connection error occurred posting Telegram '
'attachment.')
self.logger.debug('Socket Exception: %s' % str(e))
@@ -393,6 +390,7 @@ class NotifyTelegram(NotifyBase):
url,
headers=headers,
verify=self.verify_certificate,
+ timeout=self.request_timeout,
)
if r.status_code != requests.codes.ok:
@@ -436,12 +434,12 @@ class NotifyTelegram(NotifyBase):
# - TypeError = r.content is None
# - AttributeError = r is None
self.logger.warning(
- 'A communication error occured detecting the Telegram User.')
+ 'A communication error occurred detecting the Telegram User.')
return 0
except requests.RequestException as e:
self.logger.warning(
- 'A connection error occured detecting the Telegram User.')
+ 'A connection error occurred detecting the Telegram User.')
self.logger.debug('Socket Exception: %s' % str(e))
return 0
@@ -472,7 +470,7 @@ class NotifyTelegram(NotifyBase):
entry = response['result'][0]
_id = entry['message']['from'].get('id', 0)
_user = entry['message']['from'].get('first_name')
- self.logger.info('Detected telegram user %s (userid=%d)' % (
+ self.logger.info('Detected Telegram user %s (userid=%d)' % (
_user, _id))
# Return our detected userid
return _id
@@ -488,6 +486,19 @@ class NotifyTelegram(NotifyBase):
Perform Telegram Notification
"""
+ if len(self.targets) == 0 and self.detect_owner:
+ _id = self.detect_bot_owner()
+ if _id:
+ # Permanently store our id in our target list for next time
+ self.targets.append(str(_id))
+ self.logger.info(
+ 'Update your Telegram Apprise URL to read: '
+ '{}'.format(self.url(privacy=True)))
+
+ if len(self.targets) == 0:
+ self.logger.warning('There were not Telegram chat_ids to notify.')
+ return False
+
headers = {
'User-Agent': self.app_id,
'Content-Type': 'application/json',
@@ -597,6 +608,7 @@ class NotifyTelegram(NotifyBase):
data=dumps(payload),
headers=headers,
verify=self.verify_certificate,
+ timeout=self.request_timeout,
)
if r.status_code != requests.codes.ok:
@@ -631,7 +643,7 @@ class NotifyTelegram(NotifyBase):
except requests.RequestException as e:
self.logger.warning(
- 'A connection error occured sending Telegram:%s ' % (
+ 'A connection error occurred sending Telegram:%s ' % (
payload['chat_id']) + 'notification.'
)
self.logger.debug('Socket Exception: %s' % str(e))
@@ -663,29 +675,29 @@ class NotifyTelegram(NotifyBase):
Returns the URL built dynamically based on specified arguments.
"""
- # Define any arguments set
- args = {
- 'format': self.notify_format,
- 'overflow': self.overflow_mode,
+ # Define any URL parameters
+ params = {
'image': self.include_image,
- 'verify': 'yes' if self.verify_certificate else 'no',
'detect': 'yes' if self.detect_owner else 'no',
}
+ # Extend our parameters
+ params.update(self.url_parameters(privacy=privacy, *args, **kwargs))
+
# No need to check the user token because the user automatically gets
# appended into the list of chat ids
- return '{schema}://{bot_token}/{targets}/?{args}'.format(
+ return '{schema}://{bot_token}/{targets}/?{params}'.format(
schema=self.secure_protocol,
bot_token=self.pprint(self.bot_token, privacy, safe=''),
targets='/'.join(
[NotifyTelegram.quote('@{}'.format(x)) for x in self.targets]),
- args=NotifyTelegram.urlencode(args))
+ params=NotifyTelegram.urlencode(params))
@staticmethod
def parse_url(url):
"""
Parses the URL and returns enough arguments that can allow
- us to substantiate this object.
+ us to re-instantiate this object.
"""
# This is a dirty hack; but it's the only work around to tgram://
@@ -718,17 +730,14 @@ class NotifyTelegram(NotifyBase):
tgram.group('protocol'),
tgram.group('prefix'),
tgram.group('btoken_a'),
- tgram.group('remaining')))
+ tgram.group('remaining')), verify_host=False)
else:
# Try again
- results = NotifyBase.parse_url(
- '%s%s/%s' % (
- tgram.group('protocol'),
- tgram.group('btoken_a'),
- tgram.group('remaining'),
- ),
- )
+ results = NotifyBase.parse_url('%s%s/%s' % (
+ tgram.group('protocol'),
+ tgram.group('btoken_a'),
+ tgram.group('remaining')), verify_host=False)
# The first token is stored in the hostname
bot_token_a = NotifyTelegram.unquote(results['host'])
diff --git a/libs/apprise/plugins/NotifyTwilio.py b/libs/apprise/plugins/NotifyTwilio.py
index db0223a8a..4ab19713f 100644
--- a/libs/apprise/plugins/NotifyTwilio.py
+++ b/libs/apprise/plugins/NotifyTwilio.py
@@ -304,6 +304,7 @@ class NotifyTwilio(NotifyBase):
data=payload,
headers=headers,
verify=self.verify_certificate,
+ timeout=self.request_timeout,
)
if r.status_code not in (
@@ -351,7 +352,7 @@ class NotifyTwilio(NotifyBase):
except requests.RequestException as e:
self.logger.warning(
- 'A Connection error occured sending Twilio:%s ' % (
+ 'A Connection error occurred sending Twilio:%s ' % (
target) + 'notification.'
)
self.logger.debug('Socket Exception: %s' % str(e))
@@ -367,14 +368,10 @@ class NotifyTwilio(NotifyBase):
Returns the URL built dynamically based on specified arguments.
"""
- # Define any arguments set
- args = {
- 'format': self.notify_format,
- 'overflow': self.overflow_mode,
- 'verify': 'yes' if self.verify_certificate else 'no',
- }
+ # Our URL parameters
+ params = self.url_parameters(privacy=privacy, *args, **kwargs)
- return '{schema}://{sid}:{token}@{source}/{targets}/?{args}'.format(
+ return '{schema}://{sid}:{token}@{source}/{targets}/?{params}'.format(
schema=self.secure_protocol,
sid=self.pprint(
self.account_sid, privacy, mode=PrivacyMode.Tail, safe=''),
@@ -382,13 +379,13 @@ class NotifyTwilio(NotifyBase):
source=NotifyTwilio.quote(self.source, safe=''),
targets='/'.join(
[NotifyTwilio.quote(x, safe='') for x in self.targets]),
- args=NotifyTwilio.urlencode(args))
+ params=NotifyTwilio.urlencode(params))
@staticmethod
def parse_url(url):
"""
Parses the URL and returns enough arguments that can allow
- us to substantiate this object.
+ us to re-instantiate this object.
"""
results = NotifyBase.parse_url(url, verify_host=False)
diff --git a/libs/apprise/plugins/NotifyTwist.py b/libs/apprise/plugins/NotifyTwist.py
index 0aafe18a2..39bec5eaa 100644
--- a/libs/apprise/plugins/NotifyTwist.py
+++ b/libs/apprise/plugins/NotifyTwist.py
@@ -36,7 +36,7 @@ from ..URLBase import PrivacyMode
from ..common import NotifyFormat
from ..common import NotifyType
from ..utils import parse_list
-from ..utils import GET_EMAIL_RE
+from ..utils import is_email
from ..AppriseLocale import gettext_lazy as _
@@ -140,12 +140,6 @@ class NotifyTwist(NotifyBase):
# <workspace_id>:<channel_id>
self.channel_ids = set()
- # Initialize our Email Object
- self.email = email if email else '{}@{}'.format(
- self.user,
- self.host,
- )
-
# The token is None if we're not logged in and False if we
# failed to log in. Otherwise it is set to the actual token
self.token = None
@@ -171,26 +165,31 @@ class NotifyTwist(NotifyBase):
# }
self._cached_channels = dict()
- try:
- result = GET_EMAIL_RE.match(self.email)
- if not result:
- # let outer exception handle this
- raise TypeError
-
- if email:
- # Force user/host to be that of the defined email for
- # consistency. This is very important for those initializing
- # this object with the the email object would could potentially
- # cause inconsistency to contents in the NotifyBase() object
- self.user = result.group('fulluser')
- self.host = result.group('domain')
-
- except (TypeError, AttributeError):
+ # Initialize our Email Object
+ self.email = email if email else '{}@{}'.format(
+ self.user,
+ self.host,
+ )
+
+ # Check if it is valid
+ result = is_email(self.email)
+ if not result:
+ # let outer exception handle this
msg = 'The Twist Auth email specified ({}) is invalid.'\
.format(self.email)
self.logger.warning(msg)
raise TypeError(msg)
+ # Re-assign email based on what was parsed
+ self.email = result['full_email']
+ if email:
+ # Force user/host to be that of the defined email for
+ # consistency. This is very important for those initializing
+ # this object with the the email object would could potentially
+ # cause inconsistency to contents in the NotifyBase() object
+ self.user = result['user']
+ self.host = result['domain']
+
if not self.password:
msg = 'No Twist password was specified with account: {}'\
.format(self.email)
@@ -229,28 +228,25 @@ class NotifyTwist(NotifyBase):
Returns the URL built dynamically based on specified arguments.
"""
- # Define any arguments set
- args = {
- 'format': self.notify_format,
- 'overflow': self.overflow_mode,
- 'verify': 'yes' if self.verify_certificate else 'no',
- }
-
- return '{schema}://{password}:{user}@{host}/{targets}/?{args}'.format(
- schema=self.secure_protocol,
- password=self.pprint(
- self.password, privacy, mode=PrivacyMode.Secret, safe=''),
- user=self.quote(self.user, safe=''),
- host=self.host,
- targets='/'.join(
- [NotifyTwist.quote(x, safe='') for x in chain(
- # Channels are prefixed with a pound/hashtag symbol
- ['#{}'.format(x) for x in self.channels],
- # Channel IDs
- self.channel_ids,
- )]),
- args=NotifyTwist.urlencode(args),
- )
+ # Our URL parameters
+ params = self.url_parameters(privacy=privacy, *args, **kwargs)
+
+ return '{schema}://{password}:{user}@{host}/{targets}/' \
+ '?{params}'.format(
+ schema=self.secure_protocol,
+ password=self.pprint(
+ self.password, privacy, mode=PrivacyMode.Secret, safe=''),
+ user=self.quote(self.user, safe=''),
+ host=self.host,
+ targets='/'.join(
+ [NotifyTwist.quote(x, safe='') for x in chain(
+ # Channels are prefixed with a pound/hashtag symbol
+ ['#{}'.format(x) for x in self.channels],
+ # Channel IDs
+ self.channel_ids,
+ )]),
+ params=NotifyTwist.urlencode(params),
+ )
def login(self):
"""
@@ -640,7 +636,9 @@ class NotifyTwist(NotifyBase):
api_url,
data=payload,
headers=headers,
- verify=self.verify_certificate)
+ verify=self.verify_certificate,
+ timeout=self.request_timeout,
+ )
# Get our JSON content if it's possible
try:
@@ -679,7 +677,9 @@ class NotifyTwist(NotifyBase):
api_url,
data=payload,
headers=headers,
- verify=self.verify_certificate)
+ verify=self.verify_certificate,
+ timeout=self.request_timeout
+ )
# Get our JSON content if it's possible
try:
@@ -725,11 +725,10 @@ class NotifyTwist(NotifyBase):
def parse_url(url):
"""
Parses the URL and returns enough arguments that can allow
- us to substantiate this object.
+ us to re-instantiate this object.
"""
results = NotifyBase.parse_url(url)
-
if not results:
# We're done early as we couldn't load the results
return results
diff --git a/libs/apprise/plugins/NotifyTwitter.py b/libs/apprise/plugins/NotifyTwitter.py
index f6e57624a..437cd1e83 100644
--- a/libs/apprise/plugins/NotifyTwitter.py
+++ b/libs/apprise/plugins/NotifyTwitter.py
@@ -73,9 +73,8 @@ class NotifyTwitter(NotifyBase):
# The services URL
service_url = 'https://twitter.com/'
- # The default secure protocol is twitter. 'tweet' is left behind
- # for backwards compatibility of older apprise usage
- secure_protocol = ('twitter', 'tweet')
+ # The default secure protocol is twitter.
+ secure_protocol = 'twitter'
# A URL that takes you to the setup/help of the specific protocol
setup_url = 'https://github.com/caronc/apprise/wiki/Notify_twitter'
@@ -510,7 +509,9 @@ class NotifyTwitter(NotifyBase):
data=payload,
headers=headers,
auth=auth,
- verify=self.verify_certificate)
+ verify=self.verify_certificate,
+ timeout=self.request_timeout,
+ )
if r.status_code != requests.codes.ok:
# We had a problem
@@ -577,21 +578,21 @@ class NotifyTwitter(NotifyBase):
Returns the URL built dynamically based on specified arguments.
"""
- # Define any arguments set
- args = {
- 'format': self.notify_format,
- 'overflow': self.overflow_mode,
+ # Define any URL parameters
+ params = {
'mode': self.mode,
- 'verify': 'yes' if self.verify_certificate else 'no',
}
+ # Extend our parameters
+ params.update(self.url_parameters(privacy=privacy, *args, **kwargs))
+
if len(self.targets) > 0:
- args['to'] = ','.join([NotifyTwitter.quote(x, safe='')
- for x in self.targets])
+ params['to'] = ','.join(
+ [NotifyTwitter.quote(x, safe='') for x in self.targets])
return '{schema}://{ckey}/{csecret}/{akey}/{asecret}' \
- '/{targets}/?{args}'.format(
- schema=self.secure_protocol[0],
+ '/{targets}/?{params}'.format(
+ schema=self.secure_protocol,
ckey=self.pprint(self.ckey, privacy, safe=''),
csecret=self.pprint(
self.csecret, privacy, mode=PrivacyMode.Secret, safe=''),
@@ -601,17 +602,16 @@ class NotifyTwitter(NotifyBase):
targets='/'.join(
[NotifyTwitter.quote('@{}'.format(target), safe='')
for target in self.targets]),
- args=NotifyTwitter.urlencode(args))
+ params=NotifyTwitter.urlencode(params))
@staticmethod
def parse_url(url):
"""
Parses the URL and returns enough arguments that can allow
- us to substantiate this object.
+ us to re-instantiate this object.
"""
results = NotifyBase.parse_url(url, verify_host=False)
-
if not results:
# We're done early as we couldn't load the results
return results
@@ -662,9 +662,4 @@ class NotifyTwitter(NotifyBase):
results['targets'] += \
NotifyTwitter.parse_list(results['qsd']['to'])
- if results.get('schema', 'twitter').lower() == 'tweet':
- # Deprication Notice issued for v0.7.9
- NotifyTwitter.logger.deprecate(
- 'tweet:// has been replaced by twitter://')
-
return results
diff --git a/libs/apprise/plugins/NotifyWebexTeams.py b/libs/apprise/plugins/NotifyWebexTeams.py
index 35d4ffbee..5e8021330 100644
--- a/libs/apprise/plugins/NotifyWebexTeams.py
+++ b/libs/apprise/plugins/NotifyWebexTeams.py
@@ -168,6 +168,7 @@ class NotifyWebexTeams(NotifyBase):
data=dumps(payload),
headers=headers,
verify=self.verify_certificate,
+ timeout=self.request_timeout,
)
if r.status_code not in (
requests.codes.ok, requests.codes.no_content):
@@ -194,7 +195,7 @@ class NotifyWebexTeams(NotifyBase):
except requests.RequestException as e:
self.logger.warning(
- 'A Connection error occured sending Webex Teams '
+ 'A Connection error occurred sending Webex Teams '
'notification.'
)
self.logger.debug('Socket Exception: %s' % str(e))
@@ -208,28 +209,23 @@ class NotifyWebexTeams(NotifyBase):
Returns the URL built dynamically based on specified arguments.
"""
- # Define any arguments set
- args = {
- 'format': self.notify_format,
- 'overflow': self.overflow_mode,
- 'verify': 'yes' if self.verify_certificate else 'no',
- }
+ # Our URL parameters
+ params = self.url_parameters(privacy=privacy, *args, **kwargs)
- return '{schema}://{token}/?{args}'.format(
+ return '{schema}://{token}/?{params}'.format(
schema=self.secure_protocol,
token=self.pprint(self.token, privacy, safe=''),
- args=NotifyWebexTeams.urlencode(args),
+ params=NotifyWebexTeams.urlencode(params),
)
@staticmethod
def parse_url(url):
"""
Parses the URL and returns enough arguments that can allow
- us to substantiate this object.
+ us to re-instantiate this object.
"""
results = NotifyBase.parse_url(url, verify_host=False)
-
if not results:
# We're done early as we couldn't load the results
return results
@@ -248,14 +244,14 @@ class NotifyWebexTeams(NotifyBase):
result = re.match(
r'^https?://api\.ciscospark\.com/v[1-9][0-9]*/webhooks/incoming/'
r'(?P<webhook_token>[A-Z0-9_-]+)/?'
- r'(?P<args>\?.+)?$', url, re.I)
+ r'(?P<params>\?.+)?$', url, re.I)
if result:
return NotifyWebexTeams.parse_url(
- '{schema}://{webhook_token}/{args}'.format(
+ '{schema}://{webhook_token}/{params}'.format(
schema=NotifyWebexTeams.secure_protocol,
webhook_token=result.group('webhook_token'),
- args='' if not result.group('args')
- else result.group('args')))
+ params='' if not result.group('params')
+ else result.group('params')))
return None
diff --git a/libs/apprise/plugins/NotifyWindows.py b/libs/apprise/plugins/NotifyWindows.py
index 50e7e60ae..9c957f9df 100644
--- a/libs/apprise/plugins/NotifyWindows.py
+++ b/libs/apprise/plugins/NotifyWindows.py
@@ -48,7 +48,7 @@ try:
except ImportError:
# No problem; we just simply can't support this plugin because we're
- # either using Linux, or simply do not have pypiwin32 installed.
+ # either using Linux, or simply do not have pywin32 installed.
pass
@@ -91,7 +91,7 @@ class NotifyWindows(NotifyBase):
# Define object templates
templates = (
- '{schema}://_/',
+ '{schema}://',
)
# Define our template arguments
@@ -146,7 +146,8 @@ class NotifyWindows(NotifyBase):
if not self._enabled:
self.logger.warning(
- "Windows Notifications are not supported by this system.")
+ "Windows Notifications are not supported by this system; "
+ "`pip install pywin32`.")
return False
# Always call throttle before any remote server i/o is made
@@ -222,18 +223,18 @@ class NotifyWindows(NotifyBase):
Returns the URL built dynamically based on specified arguments.
"""
- # Define any arguments set
- args = {
- 'format': self.notify_format,
- 'overflow': self.overflow_mode,
+ # Define any URL parameters
+ params = {
'image': 'yes' if self.include_image else 'no',
'duration': str(self.duration),
- 'verify': 'yes' if self.verify_certificate else 'no',
}
- return '{schema}://_/?{args}'.format(
+ # Extend our parameters
+ params.update(self.url_parameters(privacy=privacy, *args, **kwargs))
+
+ return '{schema}://?{params}'.format(
schema=self.protocol,
- args=NotifyWindows.urlencode(args),
+ params=NotifyWindows.urlencode(params),
)
@staticmethod
@@ -245,19 +246,7 @@ class NotifyWindows(NotifyBase):
"""
- results = NotifyBase.parse_url(url)
- if not results:
- results = {
- 'schema': NotifyWindows.protocol,
- 'user': None,
- 'password': None,
- 'port': None,
- 'host': '_',
- 'fullpath': None,
- 'path': None,
- 'url': url,
- 'qsd': {},
- }
+ results = NotifyBase.parse_url(url, verify_host=False)
# Include images with our message
results['include_image'] = \
diff --git a/libs/apprise/plugins/NotifyXBMC.py b/libs/apprise/plugins/NotifyXBMC.py
index d286ac60e..22f4219c0 100644
--- a/libs/apprise/plugins/NotifyXBMC.py
+++ b/libs/apprise/plugins/NotifyXBMC.py
@@ -73,9 +73,6 @@ class NotifyXBMC(NotifyBase):
# Allows the user to specify the NotifyImageSize object
image_size = NotifyImageSize.XY_128
- # The number of seconds to display the popup for
- default_popup_duration_sec = 12
-
# XBMC default protocol version (v2)
xbmc_remote_protocol = 2
@@ -137,8 +134,9 @@ class NotifyXBMC(NotifyBase):
super(NotifyXBMC, self).__init__(**kwargs)
# Number of seconds to display notification for
- self.duration = self.default_popup_duration_sec \
- if not (isinstance(duration, int) and duration > 0) else duration
+ self.duration = self.template_args['duration']['default'] \
+ if not (isinstance(duration, int) and
+ self.template_args['duration']['min'] > 0) else duration
# Build our schema
self.schema = 'https' if self.secure else 'http'
@@ -264,6 +262,7 @@ class NotifyXBMC(NotifyBase):
headers=headers,
auth=auth,
verify=self.verify_certificate,
+ timeout=self.request_timeout,
)
if r.status_code != requests.codes.ok:
# We had a problem
@@ -287,7 +286,7 @@ class NotifyXBMC(NotifyBase):
except requests.RequestException as e:
self.logger.warning(
- 'A Connection error occured sending XBMC/KODI '
+ 'A Connection error occurred sending XBMC/KODI '
'notification.'
)
self.logger.debug('Socket Exception: %s' % str(e))
@@ -302,15 +301,15 @@ class NotifyXBMC(NotifyBase):
Returns the URL built dynamically based on specified arguments.
"""
- # Define any arguments set
- args = {
- 'format': self.notify_format,
- 'overflow': self.overflow_mode,
+ # Define any URL parameters
+ params = {
'image': 'yes' if self.include_image else 'no',
'duration': str(self.duration),
- 'verify': 'yes' if self.verify_certificate else 'no',
}
+ # Extend our parameters
+ params.update(self.url_parameters(privacy=privacy, *args, **kwargs))
+
# Determine Authentication
auth = ''
if self.user and self.password:
@@ -331,20 +330,21 @@ class NotifyXBMC(NotifyBase):
# Append 's' to schema
default_schema += 's'
- return '{schema}://{auth}{hostname}{port}/?{args}'.format(
+ return '{schema}://{auth}{hostname}{port}/?{params}'.format(
schema=default_schema,
auth=auth,
- hostname=NotifyXBMC.quote(self.host, safe=''),
+ # never encode hostname since we're expecting it to be a valid one
+ hostname=self.host,
port='' if not self.port or self.port == default_port
else ':{}'.format(self.port),
- args=NotifyXBMC.urlencode(args),
+ params=NotifyXBMC.urlencode(params),
)
@staticmethod
def parse_url(url):
"""
Parses the URL and returns enough arguments that can allow
- us to substantiate this object.
+ us to re-instantiate this object.
"""
results = NotifyBase.parse_url(url)
diff --git a/libs/apprise/plugins/NotifyXML.py b/libs/apprise/plugins/NotifyXML.py
index 340446c1e..21ddf0b64 100644
--- a/libs/apprise/plugins/NotifyXML.py
+++ b/libs/apprise/plugins/NotifyXML.py
@@ -143,15 +143,11 @@ class NotifyXML(NotifyBase):
Returns the URL built dynamically based on specified arguments.
"""
- # Define any arguments set
- args = {
- 'format': self.notify_format,
- 'overflow': self.overflow_mode,
- 'verify': 'yes' if self.verify_certificate else 'no',
- }
+ # Store our defined headers into our URL parameters
+ params = {'+{}'.format(k): v for k, v in self.headers.items()}
- # Append our headers into our args
- args.update({'+{}'.format(k): v for k, v in self.headers.items()})
+ # Extend our parameters
+ params.update(self.url_parameters(privacy=privacy, *args, **kwargs))
# Determine Authentication
auth = ''
@@ -168,14 +164,15 @@ class NotifyXML(NotifyBase):
default_port = 443 if self.secure else 80
- return '{schema}://{auth}{hostname}{port}{fullpath}/?{args}'.format(
+ return '{schema}://{auth}{hostname}{port}{fullpath}/?{params}'.format(
schema=self.secure_protocol if self.secure else self.protocol,
auth=auth,
- hostname=NotifyXML.quote(self.host, safe=''),
+ # never encode hostname since we're expecting it to be a valid one
+ hostname=self.host,
port='' if self.port is None or self.port == default_port
else ':{}'.format(self.port),
fullpath=NotifyXML.quote(self.fullpath, safe='/'),
- args=NotifyXML.urlencode(args),
+ params=NotifyXML.urlencode(params),
)
def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs):
@@ -234,6 +231,7 @@ class NotifyXML(NotifyBase):
headers=headers,
auth=auth,
verify=self.verify_certificate,
+ timeout=self.request_timeout,
)
if r.status_code != requests.codes.ok:
# We had a problem
@@ -257,7 +255,7 @@ class NotifyXML(NotifyBase):
except requests.RequestException as e:
self.logger.warning(
- 'A Connection error occured sending XML '
+ 'A Connection error occurred sending XML '
'notification to %s.' % self.host)
self.logger.debug('Socket Exception: %s' % str(e))
@@ -270,11 +268,10 @@ class NotifyXML(NotifyBase):
def parse_url(url):
"""
Parses the URL and returns enough arguments that can allow
- us to substantiate this object.
+ us to re-instantiate this object.
"""
results = NotifyBase.parse_url(url)
-
if not results:
# We're done early as we couldn't load the results
return results
diff --git a/libs/apprise/plugins/NotifyXMPP/__init__.py b/libs/apprise/plugins/NotifyXMPP/__init__.py
index a1cd0073a..48dbc19b0 100644
--- a/libs/apprise/plugins/NotifyXMPP/__init__.py
+++ b/libs/apprise/plugins/NotifyXMPP/__init__.py
@@ -272,20 +272,16 @@ class NotifyXMPP(NotifyBase):
Returns the URL built dynamically based on specified arguments.
"""
- # Define any arguments set
- args = {
- 'format': self.notify_format,
- 'overflow': self.overflow_mode,
- 'verify': 'yes' if self.verify_certificate else 'no',
- }
+ # Our URL parameters
+ params = self.url_parameters(privacy=privacy, *args, **kwargs)
if self.jid:
- args['jid'] = self.jid
+ params['jid'] = self.jid
if self.xep:
# xep are integers, so we need to just iterate over a list and
# switch them to a string
- args['xep'] = ','.join([str(xep) for xep in self.xep])
+ params['xep'] = ','.join([str(xep) for xep in self.xep])
# Target JID(s) can clash with our existing paths, so we just use comma
# and/or space as a delimiters - %20 = space
@@ -307,25 +303,25 @@ class NotifyXMPP(NotifyBase):
self.password if self.password else self.user, privacy,
mode=PrivacyMode.Secret, safe='')
- return '{schema}://{auth}@{hostname}{port}/{jids}?{args}'.format(
+ return '{schema}://{auth}@{hostname}{port}/{jids}?{params}'.format(
auth=auth,
schema=default_schema,
- hostname=NotifyXMPP.quote(self.host, safe=''),
+ # never encode hostname since we're expecting it to be a valid one
+ hostname=self.host,
port='' if not self.port or self.port == default_port
else ':{}'.format(self.port),
jids=jids,
- args=NotifyXMPP.urlencode(args),
+ params=NotifyXMPP.urlencode(params),
)
@staticmethod
def parse_url(url):
"""
Parses the URL and returns enough arguments that can allow
- us to substantiate this object.
+ us to re-instantiate this object.
"""
results = NotifyBase.parse_url(url)
-
if not results:
# We're done early as we couldn't load the results
return results
diff --git a/libs/apprise/plugins/NotifyZulip.py b/libs/apprise/plugins/NotifyZulip.py
index 00024218f..2290efb0d 100644
--- a/libs/apprise/plugins/NotifyZulip.py
+++ b/libs/apprise/plugins/NotifyZulip.py
@@ -62,7 +62,7 @@ from .NotifyBase import NotifyBase
from ..common import NotifyType
from ..utils import parse_list
from ..utils import validate_regex
-from ..utils import GET_EMAIL_RE
+from ..utils import is_email
from ..AppriseLocale import gettext_lazy as _
# A Valid Bot Name
@@ -260,7 +260,8 @@ class NotifyZulip(NotifyBase):
targets = list(self.targets)
while len(targets):
target = targets.pop(0)
- if GET_EMAIL_RE.match(target):
+ result = is_email(target)
+ if result:
# Send a private message
payload['type'] = 'private'
else:
@@ -268,7 +269,7 @@ class NotifyZulip(NotifyBase):
payload['type'] = 'stream'
# Set our target
- payload['to'] = target
+ payload['to'] = target if not result else result['full_email']
self.logger.debug('Zulip POST URL: %s (cert_verify=%r)' % (
url, self.verify_certificate,
@@ -284,6 +285,7 @@ class NotifyZulip(NotifyBase):
headers=headers,
auth=auth,
verify=self.verify_certificate,
+ timeout=self.request_timeout,
)
if r.status_code != requests.codes.ok:
# We had a problem
@@ -312,7 +314,7 @@ class NotifyZulip(NotifyBase):
except requests.RequestException as e:
self.logger.warning(
- 'A Connection error occured sending Zulip '
+ 'A Connection error occurred sending Zulip '
'notification to {}.'.format(target))
self.logger.debug('Socket Exception: %s' % str(e))
@@ -327,12 +329,8 @@ class NotifyZulip(NotifyBase):
Returns the URL built dynamically based on specified arguments.
"""
- # Define any arguments set
- args = {
- 'format': self.notify_format,
- 'overflow': self.overflow_mode,
- 'verify': 'yes' if self.verify_certificate else 'no',
- }
+ # Our URL parameters
+ params = self.url_parameters(privacy=privacy, *args, **kwargs)
# simplify our organization in our URL if we can
organization = '{}{}'.format(
@@ -341,25 +339,24 @@ class NotifyZulip(NotifyBase):
if self.hostname != self.default_hostname else '')
return '{schema}://{botname}@{org}/{token}/' \
- '{targets}?{args}'.format(
+ '{targets}?{params}'.format(
schema=self.secure_protocol,
botname=NotifyZulip.quote(self.botname, safe=''),
org=NotifyZulip.quote(organization, safe=''),
token=self.pprint(self.token, privacy, safe=''),
targets='/'.join(
[NotifyZulip.quote(x, safe='') for x in self.targets]),
- args=NotifyZulip.urlencode(args),
+ params=NotifyZulip.urlencode(params),
)
@staticmethod
def parse_url(url):
"""
Parses the URL and returns enough arguments that can allow
- us to substantiate this object.
+ us to re-instantiate this object.
"""
- results = NotifyBase.parse_url(url)
-
+ results = NotifyBase.parse_url(url, verify_host=False)
if not results:
# We're done early as we couldn't load the results
return results
diff --git a/libs/apprise/plugins/__init__.py b/libs/apprise/plugins/__init__.py
index fd41cb7fd..22d938771 100644
--- a/libs/apprise/plugins/__init__.py
+++ b/libs/apprise/plugins/__init__.py
@@ -23,17 +23,16 @@
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
+import os
import six
import re
import copy
-from os import listdir
from os.path import dirname
from os.path import abspath
# Used for testing
from . import NotifyEmail as NotifyEmailBase
-from .NotifyGrowl import gntp
from .NotifyXMPP import SleekXmppAdapter
# NotifyBase object is passed in as a module not class
@@ -45,6 +44,7 @@ from ..common import NotifyType
from ..common import NOTIFY_TYPES
from ..utils import parse_list
from ..utils import GET_SCHEMA_RE
+from ..logger import logger
from ..AppriseLocale import gettext_lazy as _
from ..AppriseLocale import LazyTranslation
@@ -62,9 +62,6 @@ __all__ = [
# Tokenizer
'url_to_dict',
- # gntp (used for NotifyGrowl Testing)
- 'gntp',
-
# sleekxmpp access points (used for NotifyXMPP Testing)
'SleekXmppAdapter',
]
@@ -85,7 +82,7 @@ def __load_matrix(path=abspath(dirname(__file__)), name='apprise.plugins'):
# The .py extension is optional as we support loading directories too
module_re = re.compile(r'^(?P<name>Notify[a-z0-9]+)(\.py)?$', re.I)
- for f in listdir(path):
+ for f in os.listdir(path):
match = module_re.match(f)
if not match:
# keep going
@@ -131,29 +128,39 @@ def __load_matrix(path=abspath(dirname(__file__)), name='apprise.plugins'):
# Load our module into memory so it's accessible to all
globals()[plugin_name] = plugin
- # Load protocol(s) if defined
- proto = getattr(plugin, 'protocol', None)
- if isinstance(proto, six.string_types):
- if proto not in SCHEMA_MAP:
- SCHEMA_MAP[proto] = plugin
-
- elif isinstance(proto, (set, list, tuple)):
- # Support iterables list types
- for p in proto:
- if p not in SCHEMA_MAP:
- SCHEMA_MAP[p] = plugin
-
- # Load secure protocol(s) if defined
- protos = getattr(plugin, 'secure_protocol', None)
- if isinstance(protos, six.string_types):
- if protos not in SCHEMA_MAP:
- SCHEMA_MAP[protos] = plugin
-
- if isinstance(protos, (set, list, tuple)):
- # Support iterables list types
- for p in protos:
- if p not in SCHEMA_MAP:
- SCHEMA_MAP[p] = plugin
+ fn = getattr(plugin, 'schemas', None)
+ try:
+ schemas = set([]) if not callable(fn) else fn(plugin)
+
+ except TypeError:
+ # Python v2.x support where functions associated with classes
+ # were considered bound to them and could not be called prior
+ # to the classes initialization. This code can be dropped
+ # once Python v2.x support is dropped. The below code introduces
+ # replication as it already exists and is tested in
+ # URLBase.schemas()
+ schemas = set([])
+ for key in ('protocol', 'secure_protocol'):
+ schema = getattr(plugin, key, None)
+ if isinstance(schema, six.string_types):
+ schemas.add(schema)
+
+ elif isinstance(schema, (set, list, tuple)):
+ # Support iterables list types
+ for s in schema:
+ if isinstance(s, six.string_types):
+ schemas.add(s)
+
+ # map our schema to our plugin
+ for schema in schemas:
+ if schema in SCHEMA_MAP:
+ logger.error(
+ "Notification schema ({}) mismatch detected - {} to {}"
+ .format(schema, SCHEMA_MAP[schema], plugin))
+ continue
+
+ # Assign plugin
+ SCHEMA_MAP[schema] = plugin
return SCHEMA_MAP
@@ -452,6 +459,7 @@ def url_to_dict(url):
schema = GET_SCHEMA_RE.match(_url)
if schema is None:
# Not a valid URL; take an early exit
+ logger.error('Unsupported URL: {}'.format(url))
return None
# Ensure our schema is always in lower case
@@ -466,10 +474,28 @@ def url_to_dict(url):
for r in MODULE_MAP.values()
if r['plugin'].parse_native_url(_url) is not None),
None)
+
+ if not results:
+ logger.error('Unparseable URL {}'.format(url))
+ return None
+
+ logger.trace('URL {} unpacked as:{}{}'.format(
+ url, os.linesep, os.linesep.join(
+ ['{}="{}"'.format(k, v) for k, v in results.items()])))
+
else:
# Parse our url details of the server object as dictionary
# containing all of the information parsed from our URL
results = SCHEMA_MAP[schema].parse_url(_url)
+ if not results:
+ logger.error('Unparseable {} URL {}'.format(
+ SCHEMA_MAP[schema].service_name, url))
+ return None
+
+ logger.trace('{} URL {} unpacked as:{}{}'.format(
+ SCHEMA_MAP[schema].service_name, url,
+ os.linesep, os.linesep.join(
+ ['{}="{}"'.format(k, v) for k, v in results.items()])))
# Return our results
return results
diff --git a/libs/apprise/utils.py b/libs/apprise/utils.py
index b1758c1e5..8d0920071 100644
--- a/libs/apprise/utils.py
+++ b/libs/apprise/utils.py
@@ -104,40 +104,129 @@ GET_SCHEMA_RE = re.compile(r'\s*(?P<schema>[a-z0-9]{2,9})://.*$', re.I)
# Regular expression based and expanded from:
# http://www.regular-expressions.info/email.html
+# Extended to support colon (:) delimiter for parsing names from the URL
+# such as:
+# - 'Optional Name':[email protected]
+# - 'Optional Name' <[email protected]>
+#
+# The expression also parses the general email as well such as:
GET_EMAIL_RE = re.compile(
- r"(?P<fulluser>((?P<label>[^+]+)\+)?"
- r"(?P<userid>[a-z0-9$%=_~-]+"
- r"(?:\.[a-z0-9$%+=_~-]+)"
- r"*))@(?P<domain>(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+"
- r"[a-z0-9](?:[a-z0-9-]*"
- r"[a-z0-9]))?",
- re.IGNORECASE,
-)
+ r'((?P<name>[^:<]+)?[:<\s]+)?'
+ r'(?P<full_email>((?P<label>[^+]+)\+)?'
+ r'(?P<email>(?P<userid>[a-z0-9$%=_~-]+'
+ r'(?:\.[a-z0-9$%+=_~-]+)'
+ r'*)@(?P<domain>('
+ r'(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+'
+ r'[a-z0-9](?:[a-z0-9-]*[a-z0-9]))|'
+ r'[a-z0-9][a-z0-9-]{5,})))'
+ r'\s*>?', re.IGNORECASE)
# Regular expression used to extract a phone number
GET_PHONE_NO_RE = re.compile(r'^\+?(?P<phone>[0-9\s)(+-]+)\s*$')
# Regular expression used to destinguish between multiple URLs
URL_DETECTION_RE = re.compile(
- r'([a-z0-9]+?:\/\/.*?)[\s,]*(?=$|[a-z0-9]+?:\/\/)', re.I)
+ r'([a-z0-9]+?:\/\/.*?)(?=$|[\s,]+[a-z0-9]{2,9}?:\/\/)', re.I)
+
+EMAIL_DETECTION_RE = re.compile(
+ r'[\s,]*([^@]+@.*?)(?=$|[\s,]+'
+ + r'(?:[^:<]+?[:<\s]+?)?'
+ r'[^@\s,]+@[^\s,]+)',
+ re.IGNORECASE)
# validate_regex() utilizes this mapping to track and re-use pre-complied
# regular expressions
REGEX_VALIDATE_LOOKUP = {}
-def is_hostname(hostname):
+def is_ipaddr(addr, ipv4=True, ipv6=True):
+ """
+ Validates against IPV4 and IPV6 IP Addresses
+ """
+
+ if ipv4:
+ # Based on https://stackoverflow.com/questions/5284147/\
+ # validating-ipv4-addresses-with-regexp
+ re_ipv4 = re.compile(
+ r'^(?P<ip>((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}'
+ r'(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?))$'
+ )
+ match = re_ipv4.match(addr)
+ if match is not None:
+ # Return our matched IP
+ return match.group('ip')
+
+ if ipv6:
+ # Based on https://stackoverflow.com/questions/53497/\
+ # regular-expression-that-matches-valid-ipv6-addresses
+ #
+ # IPV6 URLs should be enclosed in square brackets when placed on a URL
+ # Source: https://tools.ietf.org/html/rfc2732
+ # - For this reason, they are additionally checked for existance
+ re_ipv6 = re.compile(
+ r'\[?(?P<ip>(([0-9a-f]{1,4}:){7,7}[0-9a-f]{1,4}|([0-9a-f]{1,4}:)'
+ r'{1,7}:|([0-9a-f]{1,4}:){1,6}:[0-9a-f]{1,4}|([0-9a-f]{1,4}:){1,5}'
+ r'(:[0-9a-f]{1,4}){1,2}|([0-9a-f]{1,4}:){1,4}'
+ r'(:[0-9a-f]{1,4}){1,3}|([0-9a-f]{1,4}:){1,3}'
+ r'(:[0-9a-f]{1,4}){1,4}|([0-9a-f]{1,4}:){1,2}'
+ r'(:[0-9a-f]{1,4}){1,5}|[0-9a-f]{1,4}:'
+ r'((:[0-9a-f]{1,4}){1,6})|:((:[0-9a-f]{1,4}){1,7}|:)|'
+ r'fe80:(:[0-9a-f]{0,4}){0,4}%[0-9a-z]{1,}|::'
+ r'(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]'
+ r'|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|'
+ r'1{0,1}[0-9]){0,1}[0-9])|([0-9a-f]{1,4}:){1,4}:((25[0-5]|'
+ r'(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|'
+ r'1{0,1}[0-9]){0,1}[0-9])))\]?', re.I,
+ )
+
+ match = re_ipv6.match(addr)
+ if match is not None:
+ # Return our matched IP between square brackets since that is
+ # required for URL formatting as per RFC 2732.
+ return '[{}]'.format(match.group('ip'))
+
+ # There was no match
+ return False
+
+
+def is_hostname(hostname, ipv4=True, ipv6=True):
"""
Validate hostname
"""
- if len(hostname) > 255 or len(hostname) == 0:
+ # The entire hostname, including the delimiting dots, has a maximum of 253
+ # ASCII characters.
+ if len(hostname) > 253 or len(hostname) == 0:
return False
+ # Strip trailling period on hostname (if one exists)
if hostname[-1] == ".":
hostname = hostname[:-1]
- allowed = re.compile(r'(?!-)[A-Z\d_-]{1,63}(?<!-)$', re.IGNORECASE)
- return all(allowed.match(x) for x in hostname.split("."))
+ # Split our hostname up
+ labels = hostname.split(".")
+
+ # ipv4 check
+ if len(labels) == 4 and re.match(r'[0-9.]+', hostname):
+ return is_ipaddr(hostname, ipv4=ipv4, ipv6=False)
+
+ # - RFC 1123 permits hostname labels to start with digits
+ # - digit must be followed by alpha/numeric so we don't end up
+ # processing IP addresses here
+ # - Hostnames can ony be comprised of alpha-numeric characters and the
+ # hyphen (-) character.
+ # - Hostnames can not start with the hyphen (-) character.
+ # - labels can not exceed 63 characters
+ allowed = re.compile(
+ r'(?!-)[a-z0-9][a-z0-9-]{1,62}(?<!-)$',
+ re.IGNORECASE,
+ )
+
+ if not all(allowed.match(x) for x in labels):
+ return is_ipaddr(hostname, ipv4=ipv4, ipv6=ipv6)
+
+ return hostname
def is_email(address):
@@ -152,11 +241,33 @@ def is_email(address):
"""
try:
- return GET_EMAIL_RE.match(address) is not None
+ match = GET_EMAIL_RE.match(address)
+
except TypeError:
- # invalid syntax
+ # not parseable content
return False
+ if match:
+ return {
+ # The name parsed from the URL (if one exists)
+ 'name': '' if match.group('name') is None
+ else match.group('name').strip(),
+ # The email address
+ 'email': match.group('email'),
+ # The full email address (includes label if specified)
+ 'full_email': match.group('full_email'),
+ # The label (if specified) e.g: [email protected]
+ 'label': '' if match.group('label') is None
+ else match.group('label').strip(),
+ # The user (which does not include the label) from the email
+ # parsed.
+ 'user': match.group('userid'),
+ # The domain associated with the email address
+ 'domain': match.group('domain'),
+ }
+
+ return False
+
def tidy_path(path):
"""take a filename and or directory and attempts to tidy it up by removing
@@ -384,30 +495,22 @@ def parse_url(url, default_schema='http', verify_host=True):
# and it's already assigned
pass
- try:
- (result['host'], result['port']) = \
- re.split(r'[:]+', result['host'])[:2]
-
- except ValueError:
- # no problem then, user only exists
- # and it's already assigned
- pass
-
- if result['port']:
- try:
- result['port'] = int(result['port'])
-
- except (ValueError, TypeError):
- # Invalid Port Specified
+ # Max port is 65535 so (1,5 digits)
+ match = re.search(
+ r'^(?P<host>.+):(?P<port>[1-9][0-9]{0,4})$', result['host'])
+ if match:
+ # Separate our port from our hostname (if port is detected)
+ result['host'] = match.group('host')
+ result['port'] = int(match.group('port'))
+
+ if verify_host:
+ # Verify and Validate our hostname
+ result['host'] = is_hostname(result['host'])
+ if not result['host']:
+ # Nothing more we can do without a hostname; give the user
+ # some indication as to what went wrong
return None
- if result['port'] == 0:
- result['port'] = None
-
- if verify_host and not is_hostname(result['host']):
- # Nothing more we can do without a hostname
- return None
-
# Re-assemble cleaned up version of the url
result['url'] = '%s://' % result['schema']
if isinstance(result['user'], six.string_types):
@@ -469,26 +572,76 @@ def parse_bool(arg, default=False):
return bool(arg)
-def split_urls(urls):
+def parse_emails(*args, **kwargs):
"""
Takes a string containing URLs separated by comma's and/or spaces and
returns a list.
"""
- try:
- results = URL_DETECTION_RE.findall(urls)
+ # for Python 2.7 support, store_unparsable is not in the url above
+ # as just parse_emails(*args, store_unparseable=True) since it is
+ # an invalid syntax. This is the workaround to be backards compatible:
+ store_unparseable = kwargs.get('store_unparseable', True)
- except TypeError:
- results = []
+ result = []
+ for arg in args:
+ if isinstance(arg, six.string_types) and arg:
+ _result = EMAIL_DETECTION_RE.findall(arg)
+ if _result:
+ result += _result
+
+ elif not _result and store_unparseable:
+ # we had content passed into us that was lost because it was
+ # so poorly formatted that it didn't even come close to
+ # meeting the regular expression we defined. We intentially
+ # keep it as part of our result set so that parsing done
+ # at a higher level can at least report this to the end user
+ # and hopefully give them some indication as to what they
+ # may have done wrong.
+ result += \
+ [x for x in filter(bool, re.split(STRING_DELIMITERS, arg))]
- if len(results) > 0 and results[len(results) - 1][-1] != urls[-1]:
- # we always want to save the end of url URL if we can; This handles
- # cases where there is actually a comma (,) at the end of a single URL
- # that would have otherwise got lost when our regex passed over it.
- results[len(results) - 1] += \
- re.match(r'.*?([\s,]+)?$', urls).group(1).rstrip()
+ elif isinstance(arg, (set, list, tuple)):
+ # Use recursion to handle the list of Emails
+ result += parse_emails(*arg, store_unparseable=store_unparseable)
+
+ return result
- return results
+
+def parse_urls(*args, **kwargs):
+ """
+ Takes a string containing URLs separated by comma's and/or spaces and
+ returns a list.
+ """
+
+ # for Python 2.7 support, store_unparsable is not in the url above
+ # as just parse_urls(*args, store_unparseable=True) since it is
+ # an invalid syntax. This is the workaround to be backards compatible:
+ store_unparseable = kwargs.get('store_unparseable', True)
+
+ result = []
+ for arg in args:
+ if isinstance(arg, six.string_types) and arg:
+ _result = URL_DETECTION_RE.findall(arg)
+ if _result:
+ result += _result
+
+ elif not _result and store_unparseable:
+ # we had content passed into us that was lost because it was
+ # so poorly formatted that it didn't even come close to
+ # meeting the regular expression we defined. We intentially
+ # keep it as part of our result set so that parsing done
+ # at a higher level can at least report this to the end user
+ # and hopefully give them some indication as to what they
+ # may have done wrong.
+ result += \
+ [x for x in filter(bool, re.split(STRING_DELIMITERS, arg))]
+
+ elif isinstance(arg, (set, list, tuple)):
+ # Use recursion to handle the list of URLs
+ result += parse_urls(*arg, store_unparseable=store_unparseable)
+
+ return result
def parse_list(*args):