From e6b8b1ad195d553bca2ecff7e52f67ba4f5871ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Louis=20V=C3=A9zina?= <5130500+morpheus65535@users.noreply.github.com> Date: Mon, 14 Sep 2020 08:24:26 -0400 Subject: [PATCH] Updated Apprise to 0.8.8 --- libs/apprise/Apprise.py | 54 +- libs/apprise/AppriseAsset.py | 6 + libs/apprise/AppriseConfig.py | 85 ++- libs/apprise/URLBase.py | 167 +++++- libs/apprise/__init__.py | 8 +- .../assets/themes/default/apprise-logo.png | Bin 37371 -> 160907 bytes libs/apprise/attachment/AttachFile.py | 12 +- libs/apprise/attachment/AttachHTTP.py | 32 +- libs/apprise/cli.py | 84 ++- libs/apprise/common.py | 29 +- libs/apprise/config/ConfigBase.py | 287 ++++++++-- libs/apprise/config/ConfigFile.py | 34 +- libs/apprise/config/ConfigHTTP.py | 30 +- libs/apprise/config/__init__.py | 60 +- libs/apprise/i18n/apprise.pot | 51 +- libs/apprise/plugins/NotifyBase.py | 73 ++- libs/apprise/plugins/NotifyBoxcar.py | 18 +- libs/apprise/plugins/NotifyClickSend.py | 19 +- libs/apprise/plugins/NotifyD7Networks.py | 24 +- libs/apprise/plugins/NotifyDBus.py | 44 +- libs/apprise/plugins/NotifyDiscord.py | 84 +-- libs/apprise/plugins/NotifyEmail.py | 207 +++++-- libs/apprise/plugins/NotifyEmby.py | 47 +- libs/apprise/plugins/NotifyEnigma2.py | 27 +- libs/apprise/plugins/NotifyFaast.py | 30 +- libs/apprise/plugins/NotifyFlock.py | 35 +- libs/apprise/plugins/NotifyGitter.py | 20 +- libs/apprise/plugins/NotifyGnome.py | 32 +- libs/apprise/plugins/NotifyGotify.py | 49 +- libs/apprise/plugins/NotifyGrowl/__init__.py | 374 ------------- .../plugins/NotifyGrowl/gntp/__init__.py | 0 libs/apprise/plugins/NotifyGrowl/gntp/cli.py | 141 ----- .../plugins/NotifyGrowl/gntp/config.py | 77 --- libs/apprise/plugins/NotifyGrowl/gntp/core.py | 511 ------------------ .../plugins/NotifyGrowl/gntp/errors.py | 25 - .../plugins/NotifyGrowl/gntp/notifier.py | 265 --------- libs/apprise/plugins/NotifyGrowl/gntp/shim.py | 45 -- .../plugins/NotifyGrowl/gntp/version.py | 4 - libs/apprise/plugins/NotifyIFTTT.py | 32 +- libs/apprise/plugins/NotifyJSON.py | 25 +- libs/apprise/plugins/NotifyJoin.py | 22 +- libs/apprise/plugins/NotifyKavenegar.py | 18 +- libs/apprise/plugins/NotifyKumulos.py | 20 +- libs/apprise/plugins/NotifyMSG91.py | 24 +- libs/apprise/plugins/NotifyMSTeams.py | 28 +- libs/apprise/plugins/NotifyMailgun.py | 24 +- libs/apprise/plugins/NotifyMatrix.py | 62 +-- libs/apprise/plugins/NotifyMatterMost.py | 26 +- libs/apprise/plugins/NotifyMessageBird.py | 22 +- libs/apprise/plugins/NotifyNexmo.py | 24 +- libs/apprise/plugins/NotifyNextcloud.py | 25 +- libs/apprise/plugins/NotifyNotica.py | 47 +- libs/apprise/plugins/NotifyNotifico.py | 29 +- libs/apprise/plugins/NotifyProwl.py | 22 +- libs/apprise/plugins/NotifyPushBullet.py | 47 +- libs/apprise/plugins/NotifyPushSafer.py | 27 +- libs/apprise/plugins/NotifyPushed.py | 20 +- libs/apprise/plugins/NotifyPushjet.py | 50 +- libs/apprise/plugins/NotifyPushover.py | 27 +- libs/apprise/plugins/NotifyRocketChat.py | 31 +- libs/apprise/plugins/NotifyRyver.py | 45 +- libs/apprise/plugins/NotifySNS.py | 20 +- libs/apprise/plugins/NotifySendGrid.py | 64 ++- libs/apprise/plugins/NotifySimplePush.py | 38 +- libs/apprise/plugins/NotifySinch.py | 19 +- libs/apprise/plugins/NotifySlack.py | 33 +- libs/apprise/plugins/NotifySyslog.py | 17 +- libs/apprise/plugins/NotifyTechulusPush.py | 18 +- libs/apprise/plugins/NotifyTelegram.py | 73 +-- libs/apprise/plugins/NotifyTwilio.py | 17 +- libs/apprise/plugins/NotifyTwist.py | 95 ++-- libs/apprise/plugins/NotifyTwitter.py | 37 +- libs/apprise/plugins/NotifyWebexTeams.py | 26 +- libs/apprise/plugins/NotifyWindows.py | 35 +- libs/apprise/plugins/NotifyXBMC.py | 30 +- libs/apprise/plugins/NotifyXML.py | 25 +- libs/apprise/plugins/NotifyXMPP/__init__.py | 22 +- libs/apprise/plugins/NotifyZulip.py | 27 +- libs/apprise/plugins/__init__.py | 84 ++- libs/apprise/utils.py | 251 +++++++-- libs/version.txt | 2 +- 81 files changed, 2030 insertions(+), 2690 deletions(-) delete mode 100644 libs/apprise/plugins/NotifyGrowl/__init__.py delete mode 100644 libs/apprise/plugins/NotifyGrowl/gntp/__init__.py delete mode 100644 libs/apprise/plugins/NotifyGrowl/gntp/cli.py delete mode 100644 libs/apprise/plugins/NotifyGrowl/gntp/config.py delete mode 100644 libs/apprise/plugins/NotifyGrowl/gntp/core.py delete mode 100644 libs/apprise/plugins/NotifyGrowl/gntp/errors.py delete mode 100644 libs/apprise/plugins/NotifyGrowl/gntp/notifier.py delete mode 100644 libs/apprise/plugins/NotifyGrowl/gntp/shim.py delete mode 100644 libs/apprise/plugins/NotifyGrowl/gntp/version.py 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 ' @@ -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 7617bb5b2874c9f13166c2b39e6091f1afa8caa4..aa6824bedb77227de0d7b3d714fc852fc0cdb25c 100644 GIT binary patch literal 160907 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 Id5f_B12U>RZ~y=R literal 37371 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 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 +# Copyright (C) 2020 Chris Caron # 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) + # 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 + """ - 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([;#]+(?P.*))|' r'(\s*(?P[^=]+)=|=)?\s*' - r'(?P[a-z0-9]{2,9}://.*))?$', re.I) + r'(?P[a-z0-9]{2,9}://.*)|' + r'include\s+(?P.+))?\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 @@ -536,15 +700,38 @@ class ConfigBase(URLBase): # Store any preset tags 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[^?]+)(\?.*)?', url, re.I) + match = re.match(r'[a-z0-9]+://(?P[^?]+)(\?.*)?', 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: lead2gold@gmail.com\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 \n" "Language-Team: LANGUAGE \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 tag. The below causes the title 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[0-9]+)/' r'(?P[A-Z0-9_-]+)/?' - r'(?P\?.+)?$', url, re.I) + r'(?P\?.+)?$', 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(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[a-z0-9-]{24})/?' - r'(?P\?.+)?$', url, re.I) + r'(?P\?.+)?$', 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 -# 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 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/ [:][ :.] -GNTP_INFO_LINE = re.compile( - r'GNTP/(?P\d+\.\d+) (?PREGISTER|NOTIFY|SUBSCRIBE|\-OK|\-ERROR)' + - r' (?P[A-Z0-9]+(:(?P[A-F0-9]+))?) ?' + - r'((?P[A-Z0-9]+):(?P[A-F0-9]+).(?P[A-F0-9]+))?\r\n', - re.IGNORECASE -) - -GNTP_INFO_LINE_SHORT = re.compile( - r'GNTP/(?P\d+\.\d+) (?PREGISTER|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 `_ - -""" -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[A-Z0-9_-]+)' r'/?(?P([A-Z0-9_-]+/?)+)?' - r'/?(?P\?.+)?$', url, re.I) + r'/?(?P\?.+)?$', 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[A-Z0-9]+)/' r'(?P[A-Z0-9-]+)/?' - r'(?P\?.+)?$', url, re.I) + r'(?P\?.+)?$', 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[A-Z0-9_-]+)/?' - r'(?P\?.+)?$', url, re.I) + r'(?P\?.+)?$', 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[^&]+)([&\s]*(?P.+))?$', url, re.I) + r'\??(?P[^&]+)([&\s]*(?P.+))?$', 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[0-9]+)/' r'(?P[A-Z0-9]+)/?' - r'(?P\?.+)?$', url, re.I) + r'(?P\?.+)?$', 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[A-Z0-9_-]+)\.ryver\.com/application/webhook/' r'(?P[A-Z0-9]+)/?' - r'(?P\?.+)?$', url, re.I) + r'(?P\?.+)?$', 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[A-Z0-9]+)/' r'(?P[A-Z0-9]+)/' r'(?P[A-Z0-9]+)/?' - r'(?P\?.+)?$', url, re.I) + r'(?P\?.+)?$', 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): # : 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[A-Z0-9_-]+)/?' - r'(?P\?.+)?$', url, re.I) + r'(?P\?.+)?$', 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'^(?PNotify[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[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':user@example.com +# - 'Optional Name' +# +# The expression also parses the general email as well such as: +# - user@example.com +# - label+user@example.com GET_EMAIL_RE = re.compile( - r"(?P((?P