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