diff --git a/apprise/URLBase.py b/apprise/URLBase.py index bc8ec51c..ff06d831 100644 --- a/apprise/URLBase.py +++ b/apprise/URLBase.py @@ -106,7 +106,7 @@ class URLBase(object): # Secure Mode self.secure = kwargs.get('secure', False) - self.host = kwargs.get('host', '') + self.host = URLBase.unquote(kwargs.get('host')) self.port = kwargs.get('port') if self.port: try: @@ -116,13 +116,20 @@ class URLBase(object): self.port = None self.user = kwargs.get('user') + if self.user: + # Always unquote user if it exists + self.user = URLBase.unquote(self.user) + self.password = kwargs.get('password') + if self.password: + # Always unquote the pssword if it exists + self.password = URLBase.unquote(self.password) if 'tag' in kwargs: # We want to associate some tags with our notification service. # the code below gets the 'tag' argument if defined, otherwise # it just falls back to whatever was already defined globally - self.tags = set(parse_list(kwargs.get('tag', self.tags))) + self.tags = set(parse_list(kwargs.get('tag'), self.tags)) # Tracks the time any i/o was made to the remote server. This value # is automatically set and controlled through the throttle() call. @@ -161,7 +168,7 @@ class URLBase(object): elapsed = (reference - self._last_io_datetime).total_seconds() if wait is not None: - self.logger.debug('Throttling for {}s...'.format(wait)) + self.logger.debug('Throttling forced for {}s...'.format(wait)) sleep(wait) elif elapsed < self.request_rate_per_sec: @@ -348,10 +355,42 @@ class URLBase(object): list: A list containing all of the elements in the path """ + try: + paths = PATHSPLIT_LIST_DELIM.split(path.lstrip('/')) + if unquote: + paths = \ + [URLBase.unquote(x) for x in filter(bool, paths)] + + except AttributeError: + # path is not useable, we still want to gracefully return an + # empty list + paths = [] + + return paths + + @staticmethod + def parse_list(content, unquote=True): + """A wrapper to utils.parse_list() with unquoting support + + Parses a specified set of data and breaks it into a list. + + Args: + content (str): The path to split up into a list. If a list is + provided, then it's individual entries are processed. + + unquote (:obj:`bool`, optional): call unquote on each element + added to the returned list. + + Returns: + list: A unique list containing all of the elements in the path + """ + + content = parse_list(content) if unquote: - return PATHSPLIT_LIST_DELIM.split( - URLBase.unquote(path).lstrip('/')) - return PATHSPLIT_LIST_DELIM.split(path.lstrip('/')) + content = \ + [URLBase.unquote(x) for x in filter(bool, content)] + + return content @property def app_id(self): diff --git a/apprise/plugins/NotifyBase.py b/apprise/plugins/NotifyBase.py index 30699888..8bd933bc 100644 --- a/apprise/plugins/NotifyBase.py +++ b/apprise/plugins/NotifyBase.py @@ -33,9 +33,6 @@ from ..common import NOTIFY_FORMATS from ..common import OverflowMode from ..common import OVERFLOW_MODES -# HTML New Line Delimiter -NOTIFY_NEWLINE = '\r\n' - class NotifyBase(URLBase): """ @@ -94,12 +91,10 @@ class NotifyBase(URLBase): # Store the specified format if specified notify_format = kwargs.get('format', '') if notify_format.lower() not in NOTIFY_FORMATS: - self.logger.error( - 'Invalid notification format %s' % notify_format, - ) - raise TypeError( - 'Invalid notification format %s' % notify_format, - ) + msg = 'Invalid notification format %s'.format(notify_format) + self.logger.error(msg) + raise TypeError(msg) + # Provide override self.notify_format = notify_format @@ -107,12 +102,10 @@ class NotifyBase(URLBase): # Store the specified format if specified overflow = kwargs.get('overflow', '') if overflow.lower() not in OVERFLOW_MODES: - self.logger.error( - 'Invalid overflow method %s' % overflow, - ) - raise TypeError( - 'Invalid overflow method %s' % overflow, - ) + msg = 'Invalid overflow method {}'.format(overflow) + self.logger.error(msg) + raise TypeError(msg) + # Provide override self.overflow_mode = overflow diff --git a/apprise/plugins/NotifyBoxcar.py b/apprise/plugins/NotifyBoxcar.py index 4632390c..29c23902 100644 --- a/apprise/plugins/NotifyBoxcar.py +++ b/apprise/plugins/NotifyBoxcar.py @@ -38,6 +38,7 @@ except ImportError: from urllib.parse import urlparse from .NotifyBase import NotifyBase +from ..utils import parse_bool from ..common import NotifyType from ..common import NotifyImageSize @@ -51,8 +52,8 @@ DEFAULT_TAG = '@all' IS_TAG = re.compile(r'^[@](?P[A-Z0-9]{1,63})$', re.I) # Device tokens are only referenced when developing. -# it's not likely you'll send a message directly to a device, but -# if you do; this plugin supports it. +# It's not likely you'll send a message directly to a device, but if you do; +# this plugin supports it. IS_DEVICETOKEN = re.compile(r'^[A-Z0-9]{64}$', re.I) # Both an access key and seret key are created and assigned to each project @@ -60,8 +61,8 @@ IS_DEVICETOKEN = re.compile(r'^[A-Z0-9]{64}$', re.I) VALIDATE_ACCESS = re.compile(r'[A-Z0-9_-]{64}', re.I) VALIDATE_SECRET = re.compile(r'[A-Z0-9_-]{64}', re.I) -# Used to break apart list of potential tags by their delimiter -# into a usable list. +# Used to break apart list of potential tags by their delimiter into a useable +# list. TAGS_LIST_DELIM = re.compile(r'[ \t\r\n,\\/]+') @@ -91,7 +92,8 @@ class NotifyBoxcar(NotifyBase): # The maximum allowable characters allowed in the body per message body_maxlen = 10000 - def __init__(self, access, secret, recipients=None, **kwargs): + def __init__(self, access, secret, targets=None, include_image=True, + **kwargs): """ Initialize Boxcar Object """ @@ -108,66 +110,62 @@ class NotifyBoxcar(NotifyBase): self.access = access.strip() except AttributeError: - self.logger.warning( - 'The specified access key specified is invalid.', - ) - raise TypeError( - 'The specified access key specified is invalid.', - ) + msg = 'The specified access key is invalid.' + self.logger.warning(msg) + raise TypeError(msg) try: # Secret Key (associated with project) self.secret = secret.strip() except AttributeError: - self.logger.warning( - 'The specified secret key specified is invalid.', - ) - raise TypeError( - 'The specified secret key specified is invalid.', - ) + msg = 'The specified secret key is invalid.' + self.logger.warning(msg) + raise TypeError(msg) if not VALIDATE_ACCESS.match(self.access): - self.logger.warning( - 'The access key specified (%s) is invalid.' % self.access, - ) - raise TypeError( - 'The access key specified (%s) is invalid.' % self.access, - ) + msg = 'The access key specified ({}) is invalid.'\ + .format(self.access) + self.logger.warning(msg) + raise TypeError(msg) if not VALIDATE_SECRET.match(self.secret): - self.logger.warning( - 'The secret key specified (%s) is invalid.' % self.secret, - ) - raise TypeError( - 'The secret key specified (%s) is invalid.' % self.secret, - ) + msg = 'The secret key specified ({}) is invalid.'\ + .format(self.secret) + self.logger.warning(msg) + raise TypeError(msg) - if not recipients: + if not targets: self.tags.append(DEFAULT_TAG) - recipients = [] + targets = [] - elif isinstance(recipients, six.string_types): - recipients = [x for x in filter(bool, TAGS_LIST_DELIM.split( - recipients, + elif isinstance(targets, six.string_types): + targets = [x for x in filter(bool, TAGS_LIST_DELIM.split( + targets, ))] - # Validate recipients and drop bad ones: - for recipient in recipients: - if IS_TAG.match(recipient): + # Validate targets and drop bad ones: + for target in targets: + if IS_TAG.match(target): # store valid tag/alias - self.tags.append(IS_TAG.match(recipient).group('name')) + self.tags.append(IS_TAG.match(target).group('name')) - elif IS_DEVICETOKEN.match(recipient): + elif IS_DEVICETOKEN.match(target): # store valid device - self.device_tokens.append(recipient) + self.device_tokens.append(target) else: self.logger.warning( 'Dropped invalid tag/alias/device_token ' - '(%s) specified.' % recipient, + '({}) specified.'.format(target), ) + # 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 Boxcar Notification @@ -200,7 +198,9 @@ class NotifyBoxcar(NotifyBase): payload['device_tokens'] = self.device_tokens # Source picture should be <= 450 DP wide, ~2:1 aspect. - image_url = self.image_url(notify_type) + image_url = None if not self.include_image \ + else self.image_url(notify_type) + if image_url: # Set our image payload['@img'] = image_url @@ -218,7 +218,7 @@ class NotifyBoxcar(NotifyBase): sha1, ) - params = self.urlencode({ + params = NotifyBoxcar.urlencode({ "publishkey": self.access, "signature": h.hexdigest(), }) @@ -244,7 +244,7 @@ class NotifyBoxcar(NotifyBase): if r.status_code != requests.codes.created: # We had a problem status_str = \ - NotifyBase.http_response_code_lookup(r.status_code) + NotifyBoxcar.http_response_code_lookup(r.status_code) self.logger.warning( 'Failed to send Boxcar notification: ' @@ -282,16 +282,17 @@ class NotifyBoxcar(NotifyBase): args = { 'format': self.notify_format, 'overflow': self.overflow_mode, + 'image': 'yes' if self.include_image else 'no', } - return '{schema}://{access}/{secret}/{recipients}/?{args}'.format( + return '{schema}://{access}/{secret}/{targets}/?{args}'.format( schema=self.secure_protocol, - access=self.quote(self.access), - secret=self.quote(self.secret), - recipients='/'.join([ - self.quote(x) for x in chain( + access=NotifyBoxcar.quote(self.access, safe=''), + secret=NotifyBoxcar.quote(self.secret, safe=''), + targets='/'.join([ + NotifyBoxcar.quote(x, safe='') for x in chain( self.tags, self.device_tokens) if x != DEFAULT_TAG]), - args=self.urlencode(args), + args=NotifyBoxcar.urlencode(args), ) @staticmethod @@ -307,23 +308,30 @@ class NotifyBoxcar(NotifyBase): return None # The first token is stored in the hostname - access = results['host'] + results['access'] = NotifyBoxcar.unquote(results['host']) - # Now fetch the remaining tokens - secret = NotifyBase.split_path(results['fullpath'])[0] + # Get our entries; split_path() looks after unquoting content for us + # by default + entries = NotifyBoxcar.split_path(results['fullpath']) - # Our recipients - recipients = ','.join( - NotifyBase.split_path(results['fullpath'])[1:]) + try: + # Now fetch the remaining tokens + results['secret'] = entries.pop(0) - if not (access and secret): - # If we did not recive an access and/or secret code - # then we're done - return None + except IndexError: + # secret wasn't specified + results['secret'] = None - # Store our required content - results['recipients'] = recipients if recipients else None - results['access'] = access - results['secret'] = secret + # Our recipients make up the remaining entries of our array + results['targets'] = entries + + # The 'to' makes it easier to use yaml configuration + if 'to' in results['qsd'] and len(results['qsd']['to']): + results['targets'] += \ + NotifyBoxcar.parse_list(results['qsd'].get('to')) + + # Include images with our message + results['include_image'] = \ + parse_bool(results['qsd'].get('image', True)) return results diff --git a/apprise/plugins/NotifyDBus.py b/apprise/plugins/NotifyDBus.py index af3ede5f..b72a84fc 100644 --- a/apprise/plugins/NotifyDBus.py +++ b/apprise/plugins/NotifyDBus.py @@ -30,6 +30,7 @@ from .NotifyBase import NotifyBase from ..common import NotifyImageSize from ..common import NotifyType from ..utils import GET_SCHEMA_RE +from ..utils import parse_bool # Default our global support flag NOTIFY_DBUS_SUPPORT_ENABLED = False @@ -170,7 +171,8 @@ class NotifyDBus(NotifyBase): # let me know! :) _enabled = NOTIFY_DBUS_SUPPORT_ENABLED - def __init__(self, urgency=None, x_axis=None, y_axis=None, **kwargs): + def __init__(self, urgency=None, x_axis=None, y_axis=None, + include_image=True, **kwargs): """ Initialize DBus Object """ @@ -184,13 +186,10 @@ class NotifyDBus(NotifyBase): self.schema = kwargs.get('schema', 'dbus') if self.schema not in MAINLOOP_MAP: - # Unsupported Schema - self.logger.warning( - 'The schema specified ({}) is not supported.' - .format(self.schema)) - raise TypeError( - 'The schema specified ({}) is not supported.' - .format(self.schema)) + msg = 'The schema specified ({}) is not supported.' \ + .format(self.schema) + self.logger.warning(msg) + raise TypeError(msg) # The urgency of the message if urgency not in DBUS_URGENCIES: @@ -200,8 +199,12 @@ class NotifyDBus(NotifyBase): self.urgency = urgency # Our x/y axis settings - self.x_axis = x_axis - self.y_axis = y_axis + self.x_axis = x_axis if isinstance(x_axis, int) else None + self.y_axis = y_axis if isinstance(y_axis, int) else None + + # Track whether or not we want to send an image with our notification + # or not. + self.include_image = include_image def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs): """ @@ -229,7 +232,8 @@ class NotifyDBus(NotifyBase): ) # image path - icon_path = self.image_path(notify_type, extension='.ico') + icon_path = None if not self.include_image \ + else self.image_path(notify_type, extension='.ico') # Our meta payload meta_payload = { @@ -241,7 +245,7 @@ class NotifyDBus(NotifyBase): meta_payload['x'] = self.x_axis meta_payload['y'] = self.y_axis - if NOTIFY_DBUS_IMAGE_SUPPORT is True: + if NOTIFY_DBUS_IMAGE_SUPPORT and icon_path: try: # Use Pixbuf to create the proper image type image = GdkPixbuf.Pixbuf.new_from_file(icon_path) @@ -299,7 +303,33 @@ class NotifyDBus(NotifyBase): Returns the URL built dynamically based on specified arguments. """ - return '{schema}://'.format(schema=self.schema) + _map = { + DBusUrgency.LOW: 'low', + DBusUrgency.NORMAL: 'normal', + DBusUrgency.HIGH: 'high', + } + + # Define any arguments set + args = { + 'format': self.notify_format, + 'overflow': self.overflow_mode, + 'image': 'yes' if self.include_image else 'no', + 'urgency': 'normal' if self.urgency not in _map + else _map[self.urgency] + } + + # x in (x,y) screen coordinates + if self.x_axis: + args['x'] = str(self.x_axis) + + # y in (x,y) screen coordinates + if self.y_axis: + args['y'] = str(self.y_axis) + + return '{schema}://_/?{args}'.format( + schema=self.protocol, + args=NotifyDBus.urlencode(args), + ) @staticmethod def parse_url(url): @@ -314,23 +344,58 @@ class NotifyDBus(NotifyBase): # Content is simply not parseable return None - # return a very basic set of requirements - return { - 'schema': schema.group('schema').lower(), - 'user': None, - 'password': None, - 'port': None, - 'host': 'localhost', - 'fullpath': None, - 'path': None, - 'url': url, - 'qsd': {}, - # screen lat/lon (in pixels) where x=0 and y=0 if you want to put - # the notification in the top left hand side. Accept defaults if - # set to None - 'x_axis': None, - 'y_axis': None, - # Set the urgency to None so that we fall back to the default - # value. - 'urgency': 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': {}, + } + + # Include images with our message + results['include_image'] = \ + parse_bool(results['qsd'].get('image', True)) + + # DBus supports urgency, but we we also support the keyword priority + # so that it is consistent with some of the other plugins + urgency = results['qsd'].get('urgency', results['qsd'].get('priority')) + if urgency and len(urgency): + _map = { + '0': DBusUrgency.LOW, + 'l': DBusUrgency.LOW, + 'n': DBusUrgency.NORMAL, + '1': DBusUrgency.NORMAL, + 'h': DBusUrgency.HIGH, + '2': DBusUrgency.HIGH, + } + + try: + # Attempt to index/retrieve our urgency + results['urgency'] = _map[urgency[0].lower()] + + except KeyError: + # No priority was set + pass + + # handle x,y coordinates + try: + results['x_axis'] = int(results['qsd'].get('x')) + + except (TypeError, ValueError): + # No x was set + pass + + try: + results['y_axis'] = int(results['qsd'].get('y')) + + except (TypeError, ValueError): + # No y was set + pass + + return results diff --git a/apprise/plugins/NotifyDiscord.py b/apprise/plugins/NotifyDiscord.py index e6b3523a..57c096fe 100644 --- a/apprise/plugins/NotifyDiscord.py +++ b/apprise/plugins/NotifyDiscord.py @@ -78,7 +78,7 @@ class NotifyDiscord(NotifyBase): body_maxlen = 2000 def __init__(self, webhook_id, webhook_token, tts=False, avatar=True, - footer=False, thumbnail=True, **kwargs): + footer=False, footer_logo=True, include_image=True, **kwargs): """ Initialize Discord Object @@ -86,14 +86,14 @@ class NotifyDiscord(NotifyBase): super(NotifyDiscord, self).__init__(**kwargs) if not webhook_id: - raise TypeError( - 'An invalid Client ID was specified.' - ) + msg = 'An invalid Client ID was specified.' + self.logger.warning(msg) + raise TypeError(msg) if not webhook_token: - raise TypeError( - 'An invalid Webhook Token was specified.' - ) + msg = 'An invalid Webhook Token was specified.' + self.logger.warning(msg) + raise TypeError(msg) # Store our data self.webhook_id = webhook_id @@ -105,11 +105,14 @@ class NotifyDiscord(NotifyBase): # Over-ride Avatar Icon self.avatar = avatar - # Place a footer icon + # Place a footer self.footer = footer + # include a footer_logo in footer + self.footer_logo = footer_logo + # Place a thumbnail image inline with the message body - self.thumbnail = thumbnail + self.include_image = include_image return @@ -163,15 +166,18 @@ class NotifyDiscord(NotifyBase): payload['embeds'][0]['fields'] = fields[1:] if self.footer: + # Acquire logo URL logo_url = self.image_url(notify_type, logo=True) + + # Set Footer text to our app description payload['embeds'][0]['footer'] = { 'text': self.app_desc, } - if logo_url: + if self.footer_logo and logo_url: payload['embeds'][0]['footer']['icon_url'] = logo_url - if self.thumbnail and image_url: + if self.include_image and image_url: payload['embeds'][0]['thumbnail'] = { 'url': image_url, 'height': 256, @@ -256,14 +262,15 @@ class NotifyDiscord(NotifyBase): 'tts': 'yes' if self.tts else 'no', 'avatar': 'yes' if self.avatar else 'no', 'footer': 'yes' if self.footer else 'no', - 'thumbnail': 'yes' if self.thumbnail else 'no', + 'footer_logo': 'yes' if self.footer_logo else 'no', + 'image': 'yes' if self.include_image else 'no', } return '{schema}://{webhook_id}/{webhook_token}/?{args}'.format( schema=self.secure_protocol, - webhook_id=self.quote(self.webhook_id), - webhook_token=self.quote(self.webhook_token), - args=self.urlencode(args), + webhook_id=NotifyDiscord.quote(self.webhook_id, safe=''), + webhook_token=NotifyDiscord.quote(self.webhook_token, safe=''), + args=NotifyDiscord.urlencode(args), ) @staticmethod @@ -283,14 +290,14 @@ class NotifyDiscord(NotifyBase): return results # Store our webhook ID - webhook_id = results['host'] + webhook_id = NotifyDiscord.unquote(results['host']) # Now fetch our tokens try: - webhook_token = [x for x in filter(bool, NotifyBase.split_path( - results['fullpath']))][0] + webhook_token = \ + NotifyDiscord.split_path(results['fullpath'])[0] - except (ValueError, AttributeError, IndexError): + except IndexError: # Force some bad values that will get caught # in parsing later webhook_token = None @@ -304,12 +311,27 @@ class NotifyDiscord(NotifyBase): # Use Footer results['footer'] = parse_bool(results['qsd'].get('footer', False)) + # Use Footer Logo + results['footer_logo'] = \ + parse_bool(results['qsd'].get('footer_logo', True)) + # Update Avatar Icon results['avatar'] = parse_bool(results['qsd'].get('avatar', True)) # Use Thumbnail - results['thumbnail'] = \ - parse_bool(results['qsd'].get('thumbnail', False)) + if 'thumbnail' in results['qsd']: + # Deprication Notice issued for v0.7.5 + NotifyDiscord.logger.warning( + 'DEPRICATION NOTICE - The Discord URL contains the parameter ' + '"thumbnail=" which will be depricated 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))) return results diff --git a/apprise/plugins/NotifyEmail.py b/apprise/plugins/NotifyEmail.py index eaa956a6..183ecb21 100644 --- a/apprise/plugins/NotifyEmail.py +++ b/apprise/plugins/NotifyEmail.py @@ -450,13 +450,13 @@ class NotifyEmail(NotifyBase): auth = '' if self.user and self.password: auth = '{user}:{password}@'.format( - user=self.quote(user, safe=''), - password=self.quote(self.password, safe=''), + user=NotifyEmail.quote(user, safe=''), + password=NotifyEmail.quote(self.password, safe=''), ) else: # user url auth = '{user}@'.format( - user=self.quote(user, safe=''), + user=NotifyEmail.quote(user, safe=''), ) # Default Port setup @@ -466,10 +466,10 @@ class NotifyEmail(NotifyBase): return '{schema}://{auth}{hostname}{port}/?{args}'.format( schema=self.secure_protocol if self.secure else self.protocol, auth=auth, - hostname=self.host, + hostname=NotifyEmail.quote(self.host, safe=''), port='' if self.port is None or self.port == default_port else ':{}'.format(self.port), - args=self.urlencode(args), + args=NotifyEmail.urlencode(args), ) @staticmethod @@ -485,21 +485,28 @@ class NotifyEmail(NotifyBase): # We're done early as we couldn't load the results return results - # Apply our settings now - + # The To: address is pre-determined if to= is not otherwise + # specified. to_addr = '' + + # The From address is a must; either through the use of templates + # from= entry and/or merging the user and hostname together, this + # must be calculated or parse_url will fail. The to_addr will + # become the from_addr if it can't be calculated from_addr = '' + + # The server we connect to to send our mail to smtp_host = '' # Attempt to detect 'from' email address if 'from' in results['qsd'] and len(results['qsd']['from']): - from_addr = NotifyBase.unquote(results['qsd']['from']) + from_addr = NotifyEmail.unquote(results['qsd']['from']) else: # get 'To' email address from_addr = '%s@%s' % ( re.split( - r'[\s@]+', NotifyBase.unquote(results['user']))[0], + r'[\s@]+', NotifyEmail.unquote(results['user']))[0], results.get('host', '') ) # Lets be clever and attempt to make the from @@ -511,7 +518,7 @@ class NotifyEmail(NotifyBase): # Attempt to detect 'to' email address if 'to' in results['qsd'] and len(results['qsd']['to']): - to_addr = NotifyBase.unquote(results['qsd']['to']).strip() + to_addr = NotifyEmail.unquote(results['qsd']['to']).strip() if not to_addr: # Send to ourselves if not otherwise specified to do so @@ -519,7 +526,7 @@ class NotifyEmail(NotifyBase): if 'name' in results['qsd'] and len(results['qsd']['name']): # Extract from name to associate with from address - results['name'] = NotifyBase.unquote(results['qsd']['name']) + results['name'] = NotifyEmail.unquote(results['qsd']['name']) if 'timeout' in results['qsd'] and len(results['qsd']['timeout']): # Extract the timeout to associate with smtp server @@ -528,7 +535,7 @@ class NotifyEmail(NotifyBase): # Store SMTP Host if specified if 'smtp' in results['qsd'] and len(results['qsd']['smtp']): # Extract the smtp server - smtp_host = NotifyBase.unquote(results['qsd']['smtp']) + smtp_host = NotifyEmail.unquote(results['qsd']['smtp']) if 'mode' in results['qsd'] and len(results['qsd']['mode']): # Extract the secure mode to over-ride the default diff --git a/apprise/plugins/NotifyEmby.py b/apprise/plugins/NotifyEmby.py index 70a8b826..87611c5f 100644 --- a/apprise/plugins/NotifyEmby.py +++ b/apprise/plugins/NotifyEmby.py @@ -96,9 +96,10 @@ class NotifyEmby(NotifyBase): self.modal = modal if not self.user: - # Token was None - self.logger.warning('No Username was specified.') - raise TypeError('No Username was specified.') + # User was not specified + msg = 'No Username was specified.' + self.logger.warning(msg) + raise TypeError(msg) return @@ -169,7 +170,7 @@ class NotifyEmby(NotifyBase): if r.status_code != requests.codes.ok: # We had a problem status_str = \ - NotifyBase.http_response_code_lookup(r.status_code) + NotifyEmby.http_response_code_lookup(r.status_code) self.logger.warning( 'Failed to authenticate Emby user {} details: ' @@ -329,7 +330,7 @@ class NotifyEmby(NotifyBase): if r.status_code != requests.codes.ok: # We had a problem status_str = \ - NotifyBase.http_response_code_lookup(r.status_code) + NotifyEmby.http_response_code_lookup(r.status_code) self.logger.warning( 'Failed to acquire Emby session for user {}: ' @@ -412,7 +413,7 @@ class NotifyEmby(NotifyBase): # We had a problem status_str = \ - NotifyBase.http_response_code_lookup(r.status_code) + NotifyEmby.http_response_code_lookup(r.status_code) self.logger.warning( 'Failed to logoff Emby user {}: ' @@ -508,7 +509,7 @@ class NotifyEmby(NotifyBase): requests.codes.no_content): # We had a problem status_str = \ - NotifyBase.http_response_code_lookup(r.status_code) + NotifyEmby.http_response_code_lookup(r.status_code) self.logger.warning( 'Failed to send Emby notification: ' @@ -555,21 +556,21 @@ class NotifyEmby(NotifyBase): auth = '' if self.user and self.password: auth = '{user}:{password}@'.format( - user=self.quote(self.user, safe=''), - password=self.quote(self.password, safe=''), + user=NotifyEmby.quote(self.user, safe=''), + password=NotifyEmby.quote(self.password, safe=''), ) else: # self.user is set auth = '{user}@'.format( - user=self.quote(self.user, safe=''), + user=NotifyEmby.quote(self.user, safe=''), ) return '{schema}://{auth}{hostname}{port}/?{args}'.format( schema=self.secure_protocol if self.secure else self.protocol, auth=auth, - hostname=self.host, + hostname=NotifyEmby.quote(self.host, safe=''), port='' if self.port is None or self.port == self.emby_default_port else ':{}'.format(self.port), - args=self.urlencode(args), + args=NotifyEmby.urlencode(args), ) @property diff --git a/apprise/plugins/NotifyFaast.py b/apprise/plugins/NotifyFaast.py index 5e45ad93..dd5f2de9 100644 --- a/apprise/plugins/NotifyFaast.py +++ b/apprise/plugins/NotifyFaast.py @@ -27,6 +27,7 @@ import requests from .NotifyBase import NotifyBase from ..common import NotifyImageSize from ..common import NotifyType +from ..utils import parse_bool class NotifyFaast(NotifyBase): @@ -52,14 +53,18 @@ class NotifyFaast(NotifyBase): # Allows the user to specify the NotifyImageSize object image_size = NotifyImageSize.XY_72 - def __init__(self, authtoken, **kwargs): + def __init__(self, authtoken, include_image=True, **kwargs): """ Initialize Faast Object """ super(NotifyFaast, self).__init__(**kwargs) + # Store the Authentication Token self.authtoken = authtoken + # Associate an image with our post + self.include_image = include_image + def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs): """ Perform Faast Notification @@ -77,7 +82,10 @@ class NotifyFaast(NotifyBase): 'message': body, } - image_url = self.image_url(notify_type) + # Acquire our image if we're configured to do so + image_url = None if not self.include_image \ + else self.image_url(notify_type) + if image_url: payload['icon_url'] = image_url @@ -99,7 +107,7 @@ class NotifyFaast(NotifyBase): if r.status_code != requests.codes.ok: # We had a problem status_str = \ - NotifyBase.http_response_code_lookup(r.status_code) + NotifyFaast.http_response_code_lookup(r.status_code) self.logger.warning( 'Failed to send Faast notification:' @@ -136,12 +144,13 @@ class NotifyFaast(NotifyBase): args = { 'format': self.notify_format, 'overflow': self.overflow_mode, + 'image': 'yes' if self.include_image else 'no', } return '{schema}://{authtoken}/?{args}'.format( schema=self.protocol, - authtoken=self.quote(self.authtoken, safe=''), - args=self.urlencode(args), + authtoken=NotifyFaast.quote(self.authtoken, safe=''), + args=NotifyFaast.urlencode(args), ) @staticmethod @@ -157,9 +166,11 @@ class NotifyFaast(NotifyBase): # We're done early as we couldn't load the results return results - # Apply our settings now - # Store our authtoken using the host - results['authtoken'] = results['host'] + results['authtoken'] = NotifyFaast.unquote(results['host']) + + # Include image with our post + results['include_image'] = \ + parse_bool(results['qsd'].get('image', True)) return results diff --git a/apprise/plugins/NotifyFlock.py b/apprise/plugins/NotifyFlock.py index 41b38a5c..98145e1f 100644 --- a/apprise/plugins/NotifyFlock.py +++ b/apprise/plugins/NotifyFlock.py @@ -25,15 +25,17 @@ # To use this plugin, you need to first access https://dev.flock.com/webhooks # Specifically https://dev.flock.com/webhooks/incoming -# to create a new incoming webhook for your account. You'll need to +# +# To create a new incoming webhook for your account. You'll need to # follow the wizard to pre-determine the channel(s) you want your -# message to broadcast to, and when you're complete, you will +# message to broadcast to. When you've completed this, you will # recieve a URL that looks something like this: # https://api.flock.com/hooks/sendMessage/134b8gh0-eba0-4fa9-ab9c-257ced0e8221 # ^ # | # This is important <----------------------------------------^ # +# It becomes your 'token' that you will pass into this class # import re import requests @@ -44,6 +46,7 @@ from ..common import NotifyType from ..common import NotifyFormat from ..common import NotifyImageSize from ..utils import parse_list +from ..utils import parse_bool # Extend HTTP Error Messages @@ -89,7 +92,7 @@ class NotifyFlock(NotifyBase): # Allows the user to specify the NotifyImageSize object image_size = NotifyImageSize.XY_72 - def __init__(self, token, targets=None, **kwargs): + def __init__(self, token, targets=None, include_image=True, **kwargs): """ Initialize Flock Object """ @@ -134,6 +137,10 @@ class NotifyFlock(NotifyBase): 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 + def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs): """ Perform Flock Notification @@ -151,8 +158,8 @@ class NotifyFlock(NotifyBase): body = '{}'.format(body) else: - title = NotifyBase.escape_html(title, whitespace=False) - body = NotifyBase.escape_html(body, whitespace=False) + title = NotifyFlock.escape_html(title, whitespace=False) + body = NotifyFlock.escape_html(body, whitespace=False) body = '{}{}'.format( '' if not title else '{}
'.format(title), body) @@ -162,7 +169,10 @@ class NotifyFlock(NotifyBase): 'flockml': body, 'sendAs': { 'name': FLOCK_DEFAULT_USER if not self.user else self.user, - 'profileImage': self.image_url(notify_type), + # A Profile Image is only configured if we're configured to + # allow it + 'profileImage': None if not self.include_image + else self.image_url(notify_type), } } @@ -213,7 +223,7 @@ class NotifyFlock(NotifyBase): if r.status_code != requests.codes.ok: # We had a problem status_str = \ - NotifyBase.http_response_code_lookup( + NotifyFlock.http_response_code_lookup( r.status_code, FLOCK_HTTP_ERROR_MAP) self.logger.warning( @@ -251,15 +261,17 @@ class NotifyFlock(NotifyBase): args = { 'format': self.notify_format, 'overflow': self.overflow_mode, + 'image': 'yes' if self.include_image else 'no', } return '{schema}://{token}/{targets}?{args}'\ .format( schema=self.secure_protocol, - token=self.quote(self.token, safe=''), + token=NotifyFlock.quote(self.token, safe=''), targets='/'.join( - [self.quote(target, safe='') for target in self.targets]), - args=self.urlencode(args), + [NotifyFlock.quote(target, safe='') + for target in self.targets]), + args=NotifyFlock.urlencode(args), ) @staticmethod @@ -274,12 +286,19 @@ class NotifyFlock(NotifyBase): # We're done early as we couldn't load the results return results - # Apply our settings now + # Get our entries; split_path() looks after unquoting content for us + # by default + results['targets'] = NotifyFlock.split_path(results['fullpath']) - results['targets'] = [x for x in filter( - bool, NotifyBase.split_path(results['fullpath']))] + # The 'to' makes it easier to use yaml configuration + if 'to' in results['qsd'] and len(results['qsd']['to']): + results['targets'] += NotifyFlock.parse_list(results['qsd']['to']) # The first token is stored in the hostname - results['token'] = results['host'] + results['token'] = NotifyFlock.unquote(results['host']) + + # Include images with our message + results['include_image'] = \ + parse_bool(results['qsd'].get('image', True)) return results diff --git a/apprise/plugins/NotifyGitter.py b/apprise/plugins/NotifyGitter.py index 2e572792..005be8dd 100644 --- a/apprise/plugins/NotifyGitter.py +++ b/apprise/plugins/NotifyGitter.py @@ -55,7 +55,7 @@ from ..utils import parse_bool # API Gitter URL GITTER_API_URL = 'https://api.gitter.im/v1' -# Used to validate API Key +# Used to validate your personal access token VALIDATE_TOKEN = re.compile(r'^[a-z0-9]{40}$', re.I) # Used to break path apart into list of targets @@ -95,9 +95,11 @@ class NotifyGitter(NotifyBase): # For Tracking Purposes ratelimit_reset = datetime.utcnow() + # Default to 1 ratelimit_remaining = 1 + # Default Notification Format notify_format = NotifyFormat.MARKDOWN def __init__(self, token, targets, include_image=True, **kwargs): @@ -107,7 +109,7 @@ class NotifyGitter(NotifyBase): super(NotifyGitter, self).__init__(**kwargs) try: - # The token associated with the account + # The personal access token associated with the account self.token = token.strip() except AttributeError: @@ -117,7 +119,8 @@ class NotifyGitter(NotifyBase): raise TypeError(msg) if not VALIDATE_TOKEN.match(self.token): - msg = 'The API Token specified ({}) is invalid.'.format(token) + msg = 'The Personal Access Token specified ({}) is invalid.' \ + .format(token) self.logger.warning(msg) raise TypeError(msg) @@ -140,10 +143,11 @@ class NotifyGitter(NotifyBase): # error tracking (used for function return) has_error = False - # Build mapping of room names to their channel id's + # Set up our image for display if configured to do so + image_url = None if not self.include_image \ + else self.image_url(notify_type) - image_url = self.image_url(notify_type) - if self.include_image and image_url: + if image_url: body = '![alt]({})\n{}'.format(image_url, body) # Create a copy of the targets list @@ -288,7 +292,7 @@ class NotifyGitter(NotifyBase): if r.status_code != requests.codes.ok: # We had a problem status_str = \ - NotifyBase.http_response_code_lookup(r.status_code) + NotifyGitter.http_response_code_lookup(r.status_code) self.logger.warning( 'Failed to send Gitter POST to {}: ' @@ -342,14 +346,15 @@ class NotifyGitter(NotifyBase): args = { 'format': self.notify_format, 'overflow': self.overflow_mode, - 'image': self.include_image, + 'image': 'yes' if self.include_image else 'no', } return '{schema}://{token}/{targets}/?{args}'.format( schema=self.secure_protocol, - token=self.quote(self.token, safe=''), - targets='/'.join(self.targets), - args=self.urlencode(args)) + token=NotifyGitter.quote(self.token, safe=''), + targets='/'.join( + [NotifyGitter.quote(x, safe='') for x in self.targets]), + args=NotifyGitter.urlencode(args)) @staticmethod def parse_url(url): @@ -364,15 +369,16 @@ class NotifyGitter(NotifyBase): # We're done early as we couldn't load the results return results - results['token'] = results['host'] - results['targets'] = \ - [NotifyBase.unquote(x) for x in filter(bool, NotifyBase.split_path( - results['fullpath']))] + results['token'] = NotifyGitter.unquote(results['host']) + + # Get our entries; split_path() looks after unquoting content for us + # by default + results['targets'] = NotifyGitter.split_path(results['fullpath']) # Support the 'to' variable so that we can support targets this way too # The 'to' makes it easier to use yaml configuration if 'to' in results['qsd'] and len(results['qsd']['to']): - results['targets'] += parse_list(results['qsd']['to']) + results['targets'] += NotifyGitter.parse_list(results['qsd']['to']) # Include images with our message results['include_image'] = \ diff --git a/apprise/plugins/NotifyGnome.py b/apprise/plugins/NotifyGnome.py index cfb40d61..4e74d0a3 100644 --- a/apprise/plugins/NotifyGnome.py +++ b/apprise/plugins/NotifyGnome.py @@ -29,6 +29,7 @@ from __future__ import print_function from .NotifyBase import NotifyBase from ..common import NotifyImageSize from ..common import NotifyType +from ..utils import parse_bool # Default our global support flag NOTIFY_GNOME_SUPPORT_ENABLED = False @@ -109,7 +110,7 @@ class NotifyGnome(NotifyBase): # let me know! :) _enabled = NOTIFY_GNOME_SUPPORT_ENABLED - def __init__(self, urgency=None, **kwargs): + def __init__(self, urgency=None, include_image=True, **kwargs): """ Initialize Gnome Object """ @@ -123,6 +124,10 @@ class NotifyGnome(NotifyBase): else: self.urgency = urgency + # Track whether or not we want to send an image with our notification + # or not. + self.include_image = include_image + def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs): """ Perform Gnome Notification @@ -138,7 +143,8 @@ class NotifyGnome(NotifyBase): Notify.init(self.app_id) # image path - icon_path = self.image_path(notify_type, extension='.ico') + icon_path = None if not self.include_image \ + else self.image_path(notify_type, extension='.ico') # Build message body notification = Notify.Notification.new(body) @@ -149,18 +155,19 @@ class NotifyGnome(NotifyBase): # Always call throttle before any remote server i/o is made self.throttle() - try: - # Use Pixbuf to create the proper image type - image = GdkPixbuf.Pixbuf.new_from_file(icon_path) + if icon_path: + try: + # Use Pixbuf to create the proper image type + image = GdkPixbuf.Pixbuf.new_from_file(icon_path) - # Associate our image to our notification - notification.set_icon_from_pixbuf(image) - notification.set_image_from_pixbuf(image) + # Associate our image to our notification + notification.set_icon_from_pixbuf(image) + notification.set_image_from_pixbuf(image) - except Exception as e: - self.logger.warning( - "Could not load Gnome notification icon ({}): {}" - .format(icon_path, e)) + except Exception as e: + self.logger.warning( + "Could not load Gnome notification icon ({}): {}" + .format(icon_path, e)) notification.show() self.logger.info('Sent Gnome notification.') @@ -177,7 +184,25 @@ class NotifyGnome(NotifyBase): Returns the URL built dynamically based on specified arguments. """ - return '{schema}://'.format(schema=self.protocol) + _map = { + GnomeUrgency.LOW: 'low', + GnomeUrgency.NORMAL: 'normal', + GnomeUrgency.HIGH: 'high', + } + + # Define any arguments set + args = { + 'format': self.notify_format, + 'overflow': self.overflow_mode, + 'image': 'yes' if self.include_image else 'no', + 'urgency': 'normal' if self.urgency not in _map + else _map[self.urgency] + } + + return '{schema}://_/?{args}'.format( + schema=self.protocol, + args=NotifyGnome.urlencode(args), + ) @staticmethod def parse_url(url): @@ -188,18 +213,43 @@ class NotifyGnome(NotifyBase): """ - # return a very basic set of requirements - return { - 'schema': NotifyGnome.protocol, - 'user': None, - 'password': None, - 'port': None, - 'host': 'localhost', - 'fullpath': None, - 'path': None, - 'url': url, - 'qsd': {}, - # Set the urgency to None so that we fall back to the default - # value. - 'urgency': None, - } + 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': {}, + } + + # Include images with our message + results['include_image'] = \ + parse_bool(results['qsd'].get('image', True)) + + # Gnome supports urgency, but we we also support the keyword priority + # so that it is consistent with some of the other plugins + urgency = results['qsd'].get('urgency', results['qsd'].get('priority')) + if urgency and len(urgency): + _map = { + '0': GnomeUrgency.LOW, + 'l': GnomeUrgency.LOW, + 'n': GnomeUrgency.NORMAL, + '1': GnomeUrgency.NORMAL, + 'h': GnomeUrgency.HIGH, + '2': GnomeUrgency.HIGH, + } + + try: + # Attempt to index/retrieve our urgency + results['urgency'] = _map[urgency[0].lower()] + + except KeyError: + # No priority was set + pass + + return results diff --git a/apprise/plugins/NotifyGotify.py b/apprise/plugins/NotifyGotify.py index 56b85e77..ed3a21f9 100644 --- a/apprise/plugins/NotifyGotify.py +++ b/apprise/plugins/NotifyGotify.py @@ -103,7 +103,7 @@ class NotifyGotify(NotifyBase): # Our access token does not get created until we first # authenticate with our Gotify server. The same goes for the # user id below. - self.access_token = token + self.token = token return @@ -121,12 +121,12 @@ class NotifyGotify(NotifyBase): # Define our parameteers params = { - 'token': self.access_token, + 'token': self.token, } # Prepare Gotify Object payload = { - 'priority': 2, + 'priority': self.priority, 'title': title, 'message': body, } @@ -156,7 +156,7 @@ class NotifyGotify(NotifyBase): if r.status_code != requests.codes.ok: # We had a problem status_str = \ - NotifyBase.http_response_code_lookup(r.status_code) + NotifyGotify.http_response_code_lookup(r.status_code) self.logger.warning( 'Failed to send Gotify notification: ' @@ -201,11 +201,11 @@ class NotifyGotify(NotifyBase): return '{schema}://{hostname}{port}/{token}/?{args}'.format( schema=self.secure_protocol if self.secure else self.protocol, - hostname=self.host, + hostname=NotifyGotify.quote(self.host, safe=''), port='' if self.port is None or self.port == default_port else ':{}'.format(self.port), - token=self.access_token, - args=self.urlencode(args), + token=NotifyGotify.quote(self.token, safe=''), + args=NotifyGotify.urlencode(args), ) @staticmethod @@ -220,13 +220,17 @@ class NotifyGotify(NotifyBase): # We're done early return results + # Retrieve our escaped entries found on the fullpath + entries = NotifyBase.split_path(results['fullpath']) + # optionally find the provider key try: - token = [x for x in filter( - bool, NotifyBase.split_path(results['fullpath']))][0] + # The first entry is our token + results['token'] = entries.pop(0) - except (AttributeError, IndexError): - token = None + except IndexError: + # No token was set + results['token'] = None if 'priority' in results['qsd'] and len(results['qsd']['priority']): _map = { @@ -244,7 +248,4 @@ class NotifyGotify(NotifyBase): # No priority was set pass - # Set our token - results['token'] = token - return results diff --git a/apprise/plugins/NotifyGrowl/__init__.py b/apprise/plugins/NotifyGrowl/__init__.py index 86adee1b..1069c444 100644 --- a/apprise/plugins/NotifyGrowl/__init__.py +++ b/apprise/plugins/NotifyGrowl/__init__.py @@ -28,6 +28,7 @@ from .gntp import errors from ..NotifyBase import NotifyBase from ...common import NotifyImageSize from ...common import NotifyType +from ...utils import parse_bool # Priorities @@ -86,7 +87,7 @@ class NotifyGrowl(NotifyBase): # Default Growl Port default_port = 23053 - def __init__(self, priority=None, version=2, **kwargs): + def __init__(self, priority=None, version=2, include_image=True, **kwargs): """ Initialize Growl Object """ @@ -129,28 +130,26 @@ class NotifyGrowl(NotifyBase): ) except errors.NetworkError: - self.logger.warning( - 'A network error occured sending Growl ' - 'notification to %s.' % self.host) - raise TypeError( - 'A network error occured sending Growl ' - 'notification to %s.' % self.host) + msg = 'A network error occured sending Growl ' \ + 'notification to {}.'.format(self.host) + self.logger.warning(msg) + raise TypeError(msg) except errors.AuthError: - self.logger.warning( - 'An authentication error occured sending Growl ' - 'notification to %s.' % self.host) - raise TypeError( - 'An authentication error occured sending Growl ' - 'notification to %s.' % self.host) + msg = 'An authentication error occured sending Growl ' \ + 'notification to {}.'.format(self.host) + self.logger.warning(msg) + raise TypeError(msg) except errors.UnsupportedError: - self.logger.warning( - 'An unsupported error occured sending Growl ' - 'notification to %s.' % self.host) - raise TypeError( - 'An unsupported error occured sending Growl ' - 'notification to %s.' % self.host) + 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 @@ -162,11 +161,13 @@ class NotifyGrowl(NotifyBase): icon = None if self.version >= 2: # URL Based - icon = self.image_url(notify_type) + icon = None if not self.include_image \ + else self.image_url(notify_type) else: # Raw - icon = self.image_raw(notify_type) + icon = None if not self.include_image \ + else self.image_raw(notify_type) payload = { 'noteType': GROWL_NOTIFICATION_TYPE, @@ -232,6 +233,7 @@ class NotifyGrowl(NotifyBase): 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], @@ -239,18 +241,19 @@ class NotifyGrowl(NotifyBase): } auth = '' - if self.password: + if self.user: + # The growl password is stored in the user field auth = '{password}@'.format( - password=self.quote(self.user, safe=''), + password=NotifyGrowl.quote(self.user, safe=''), ) return '{schema}://{auth}{hostname}{port}/?{args}'.format( schema=self.secure_protocol if self.secure else self.protocol, auth=auth, - hostname=self.host, + hostname=NotifyGrowl.quote(self.host, safe=''), port='' if self.port is None or self.port == self.default_port else ':{}'.format(self.port), - args=self.urlencode(args), + args=NotifyGrowl.urlencode(args), ) @staticmethod @@ -272,11 +275,11 @@ class NotifyGrowl(NotifyBase): # Allow the user to specify the version of the protocol to use. try: version = int( - NotifyBase.unquote( + NotifyGrowl.unquote( results['qsd']['version']).strip().split('.')[0]) except (AttributeError, IndexError, TypeError, ValueError): - NotifyBase.logger.warning( + NotifyGrowl.logger.warning( 'An invalid Growl version of "%s" was specified and will ' 'be ignored.' % results['qsd']['version'] ) @@ -306,6 +309,11 @@ class NotifyGrowl(NotifyBase): 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 diff --git a/apprise/plugins/NotifyIFTTT.py b/apprise/plugins/NotifyIFTTT.py index f0a2027d..4a43eb32 100644 --- a/apprise/plugins/NotifyIFTTT.py +++ b/apprise/plugins/NotifyIFTTT.py @@ -108,14 +108,17 @@ class NotifyIFTTT(NotifyBase): super(NotifyIFTTT, self).__init__(**kwargs) if not webhook_id: - raise TypeError('You must specify the Webhooks webhook_id.') + msg = 'You must specify the Webhooks webhook_id.' + self.logger.warning(msg) + raise TypeError(msg) # Store our Events we wish to trigger self.events = parse_list(events) if not self.events: - raise TypeError( - 'You must specify at least one event you wish to trigger on.') + msg = 'You must specify at least one event you wish to trigger on.' + self.logger.warning(msg) + raise TypeError(msg) # Store our APIKey self.webhook_id = webhook_id @@ -132,9 +135,10 @@ class NotifyIFTTT(NotifyBase): self.del_tokens = del_tokens else: - raise TypeError( - 'del_token must be a list; {} was provided'.format( - str(type(del_tokens)))) + msg = 'del_token must be a list; {} was provided'.format( + str(type(del_tokens))) + self.logger.warning(msg) + raise TypeError(msg) def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs): """ @@ -202,7 +206,7 @@ class NotifyIFTTT(NotifyBase): if r.status_code != requests.codes.ok: # We had a problem status_str = \ - NotifyBase.http_response_code_lookup(r.status_code) + NotifyIFTTT.http_response_code_lookup(r.status_code) self.logger.warning( 'Failed to send IFTTT notification to {}: ' @@ -253,9 +257,10 @@ class NotifyIFTTT(NotifyBase): return '{schema}://{webhook_id}@{events}/?{args}'.format( schema=self.secure_protocol, - webhook_id=self.webhook_id, - events='/'.join([self.quote(x, safe='') for x in self.events]), - args=self.urlencode(args), + webhook_id=NotifyIFTTT.quote(self.webhook_id, safe=''), + events='/'.join([NotifyIFTTT.quote(x, safe='') + for x in self.events]), + args=NotifyIFTTT.urlencode(args), ) @staticmethod @@ -271,15 +276,26 @@ class NotifyIFTTT(NotifyBase): # We're done early as we couldn't load the results return results + # Our API Key is the hostname if no user is specified + results['webhook_id'] = \ + results['user'] if results['user'] else results['host'] + + # Unquote our API Key + results['webhook_id'] = NotifyIFTTT.unquote(results['webhook_id']) + # Our Event results['events'] = list() - results['events'].append(results['host']) - - # Our API Key - results['webhook_id'] = results['user'] + if results['user']: + # If a user was defined, then the hostname is actually a event + # too + results['events'].append(NotifyIFTTT.unquote(results['host'])) # Now fetch the remaining tokens - results['events'].extend([x for x in filter( - bool, NotifyBase.split_path(results['fullpath']))][0:]) + results['events'].extend(NotifyIFTTT.split_path(results['fullpath'])) + + # The 'to' makes it easier to use yaml configuration + if 'to' in results['qsd'] and len(results['qsd']['to']): + results['events'] += \ + NotifyIFTTT.parse_list(results['qsd']['to']) return results diff --git a/apprise/plugins/NotifyJSON.py b/apprise/plugins/NotifyJSON.py index 3415c1d7..1adade27 100644 --- a/apprise/plugins/NotifyJSON.py +++ b/apprise/plugins/NotifyJSON.py @@ -56,7 +56,7 @@ class NotifyJSON(NotifyBase): # local anyway request_rate_per_sec = 0 - def __init__(self, headers, **kwargs): + def __init__(self, headers=None, **kwargs): """ Initialize JSON Object @@ -66,12 +66,6 @@ class NotifyJSON(NotifyBase): """ super(NotifyJSON, self).__init__(**kwargs) - if self.secure: - self.schema = 'https' - - else: - self.schema = 'http' - self.fullpath = kwargs.get('fullpath') if not isinstance(self.fullpath, six.string_types): self.fullpath = '/' @@ -101,12 +95,12 @@ class NotifyJSON(NotifyBase): auth = '' if self.user and self.password: auth = '{user}:{password}@'.format( - user=self.quote(self.user, safe=''), - password=self.quote(self.password, safe=''), + user=NotifyJSON.quote(self.user, safe=''), + password=NotifyJSON.quote(self.password, safe=''), ) elif self.user: auth = '{user}@'.format( - user=self.quote(self.user, safe=''), + user=NotifyJSON.quote(self.user, safe=''), ) default_port = 443 if self.secure else 80 @@ -114,10 +108,10 @@ class NotifyJSON(NotifyBase): return '{schema}://{auth}{hostname}{port}/?{args}'.format( schema=self.secure_protocol if self.secure else self.protocol, auth=auth, - hostname=self.host, + hostname=NotifyJSON.quote(self.host, safe=''), port='' if self.port is None or self.port == default_port else ':{}'.format(self.port), - args=self.urlencode(args), + args=NotifyJSON.urlencode(args), ) def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs): @@ -148,7 +142,10 @@ class NotifyJSON(NotifyBase): if self.user: auth = (self.user, self.password) - url = '%s://%s' % (self.schema, self.host) + # Set our schema + schema = 'https' if self.secure else 'http' + + url = '%s://%s' % (schema, self.host) if isinstance(self.port, int): url += ':%d' % self.port @@ -173,7 +170,7 @@ class NotifyJSON(NotifyBase): if r.status_code != requests.codes.ok: # We had a problem status_str = \ - NotifyBase.http_response_code_lookup(r.status_code) + NotifyJSON.http_response_code_lookup(r.status_code) self.logger.warning( 'Failed to send JSON notification: ' @@ -219,4 +216,8 @@ class NotifyJSON(NotifyBase): results['headers'] = results['qsd-'] results['headers'].update(results['qsd+']) + # Tidy our header entries by unquoting them + results['headers'] = {NotifyJSON.unquote(x): NotifyJSON.unquote(y) + for x, y in results['headers'].items()} + return results diff --git a/apprise/plugins/NotifyJoin.py b/apprise/plugins/NotifyJoin.py index c8e7cb4f..4c86a442 100644 --- a/apprise/plugins/NotifyJoin.py +++ b/apprise/plugins/NotifyJoin.py @@ -34,12 +34,13 @@ # https://play.google.com/store/apps/details?id=com.joaomgcd.join import re -import six import requests from .NotifyBase import NotifyBase from ..common import NotifyImageSize from ..common import NotifyType +from ..utils import parse_list +from ..utils import parse_bool # Token required as part of the API request VALIDATE_APIKEY = re.compile(r'[A-Za-z0-9]{32}') @@ -49,9 +50,6 @@ JOIN_HTTP_ERROR_MAP = { 401: 'Unauthorized - Invalid Token.', } -# Used to break path apart into list of devices -DEVICE_LIST_DELIM = re.compile(r'[ \t\r\n,\\/]+') - # Used to detect a device IS_DEVICE_RE = re.compile(r'([A-Za-z0-9]{32})') @@ -99,39 +97,32 @@ class NotifyJoin(NotifyBase): # The default group to use if none is specified default_join_group = 'group.all' - def __init__(self, apikey, devices, **kwargs): + def __init__(self, apikey, targets, include_image=True, **kwargs): """ Initialize Join Object """ super(NotifyJoin, self).__init__(**kwargs) if not VALIDATE_APIKEY.match(apikey.strip()): - self.logger.warning( - 'The first API Token specified (%s) is invalid.' % apikey, - ) - - raise TypeError( - 'The first API Token specified (%s) is invalid.' % apikey, - ) + msg = 'The JOIN API Token specified ({}) is invalid.'\ + .format(apikey) + self.logger.warning(msg) + raise TypeError(msg) # The token associated with the account self.apikey = apikey.strip() - if isinstance(devices, six.string_types): - self.devices = [x for x in filter(bool, DEVICE_LIST_DELIM.split( - devices, - ))] - - elif isinstance(devices, (set, tuple, list)): - self.devices = devices - - else: - self.devices = list() + # Parse devices specified + self.devices = parse_list(targets) if len(self.devices) == 0: # Default to everyone self.devices.append(self.default_join_group) + # Track whether or not we want to send an image with our notification + # or not. + self.include_image = include_image + def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs): """ Perform Join Notification @@ -151,13 +142,12 @@ class NotifyJoin(NotifyBase): device = devices.pop(0) group_re = IS_GROUP_RE.match(device) if group_re: - device = 'group.%s' % group_re.group('name').lower() + device = 'group.{}'.format(group_re.group('name').lower()) elif not IS_DEVICE_RE.match(device): self.logger.warning( - "The specified device/group '%s' is invalid; skipping." % ( - device, - ) + 'Skipping specified invalid device/group "{}"' + .format(device) ) # Mark our failure has_error = True @@ -170,7 +160,10 @@ class NotifyJoin(NotifyBase): 'text': body, } - image_url = self.image_url(notify_type) + # prepare our image for display if configured to do so + image_url = None if not self.include_image \ + else self.image_url(notify_type) + if image_url: url_args['icon'] = image_url @@ -178,7 +171,7 @@ class NotifyJoin(NotifyBase): payload = {} # Prepare the URL - url = '%s?%s' % (self.notify_url, NotifyBase.urlencode(url_args)) + url = '%s?%s' % (self.notify_url, NotifyJoin.urlencode(url_args)) self.logger.debug('Join POST URL: %s (cert_verify=%r)' % ( url, self.verify_certificate, @@ -199,7 +192,7 @@ class NotifyJoin(NotifyBase): if r.status_code != requests.codes.ok: # We had a problem status_str = \ - NotifyBase.http_response_code_lookup( + NotifyJoin.http_response_code_lookup( r.status_code, JOIN_HTTP_ERROR_MAP) self.logger.warning( @@ -242,13 +235,15 @@ class NotifyJoin(NotifyBase): args = { 'format': self.notify_format, 'overflow': self.overflow_mode, + 'image': 'yes' if self.include_image else 'no', } return '{schema}://{apikey}/{devices}/?{args}'.format( schema=self.secure_protocol, - apikey=self.quote(self.apikey, safe=''), - devices='/'.join([self.quote(x) for x in self.devices]), - args=self.urlencode(args)) + apikey=NotifyJoin.quote(self.apikey, safe=''), + devices='/'.join([NotifyJoin.quote(x, safe='') + for x in self.devices]), + args=NotifyJoin.urlencode(args)) @staticmethod def parse_url(url): @@ -263,11 +258,30 @@ class NotifyJoin(NotifyBase): # We're done early as we couldn't load the results return results - # Apply our settings now - devices = ' '.join( - filter(bool, NotifyBase.split_path(results['fullpath']))) + # Our API Key is the hostname if no user is specified + results['apikey'] = \ + results['user'] if results['user'] else results['host'] - results['apikey'] = results['host'] - results['devices'] = devices + # Unquote our API Key + results['apikey'] = NotifyJoin.unquote(results['apikey']) + + # Our Devices + results['targets'] = list() + if results['user']: + # If a user was defined, then the hostname is actually a target + # too + results['targets'].append(NotifyJoin.unquote(results['host'])) + + # Now fetch the remaining tokens + results['targets'].extend( + NotifyJoin.split_path(results['fullpath'])) + + # The 'to' makes it easier to use yaml configuration + if 'to' in results['qsd'] and len(results['qsd']['to']): + results['targets'] += NotifyJoin.parse_list(results['qsd']['to']) + + # Include images with our message + results['include_image'] = \ + parse_bool(results['qsd'].get('image', True)) return results diff --git a/apprise/plugins/NotifyMatrix.py b/apprise/plugins/NotifyMatrix.py index 9c1017f5..786207aa 100644 --- a/apprise/plugins/NotifyMatrix.py +++ b/apprise/plugins/NotifyMatrix.py @@ -39,6 +39,7 @@ from ..common import NotifyType from ..common import NotifyImageSize from ..common import NotifyFormat from ..utils import parse_bool +from ..utils import parse_list # Define default path MATRIX_V2_API_PATH = '/_matrix/client/r0' @@ -50,10 +51,6 @@ MATRIX_HTTP_ERROR_MAP = { 429: 'Rate limit imposed; wait 2s and try again', } -# Used to break apart list of potential tags by their delimiter -# into a usable list. -LIST_DELIM = re.compile(r'[ \t\r\n,\\/]+') - # Matrix Room Syntax IS_ROOM_ALIAS = re.compile( r'^\s*(#|%23)?(?P[a-z0-9-]+)((:|%3A)' @@ -120,30 +117,15 @@ class NotifyMatrix(NotifyBase): # the server doesn't remind us how long we shoul wait for default_wait_ms = 1000 - def __init__(self, rooms=None, webhook=None, thumbnail=True, **kwargs): + def __init__(self, targets=None, mode=None, include_image=True, + **kwargs): """ Initialize Matrix Object """ super(NotifyMatrix, self).__init__(**kwargs) # Prepare a list of rooms to connect and notify - if isinstance(rooms, six.string_types): - self.rooms = [x for x in filter(bool, LIST_DELIM.split( - rooms, - ))] - - elif isinstance(rooms, (set, tuple, list)): - self.rooms = rooms - - else: - self.rooms = [] - - self.webhook = None \ - if not isinstance(webhook, six.string_types) else webhook.lower() - if self.webhook and self.webhook not in MATRIX_WEBHOOK_MODES: - msg = 'The webhook specified ({}) is invalid.'.format(webhook) - self.logger.warning(msg) - raise TypeError(msg) + self.rooms = parse_list(targets) # our home server gets populated after a login/registration self.home_server = None @@ -154,23 +136,31 @@ class NotifyMatrix(NotifyBase): # This gets initialized after a login/registration self.access_token = None - # Place a thumbnail image inline with the message body - self.thumbnail = thumbnail + # Place an image inline with the message body + self.include_image = include_image # maintain a lookup of room alias's we already paired with their id # to speed up future requests self._room_cache = {} + # Setup our mode + self.mode = None \ + if not isinstance(mode, six.string_types) else mode.lower() + if self.mode and self.mode not in MATRIX_WEBHOOK_MODES: + msg = 'The mode specified ({}) is invalid.'.format(mode) + self.logger.warning(msg) + raise TypeError(msg) + def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs): """ Perform Matrix Notification """ # Call the _send_ function applicable to whatever mode we're in - # - calls _send_webhook_notification if the webhook variable is set - # - calls _send_server_notification if the webhook variable is not set + # - calls _send_webhook_notification if the mode variable is set + # - calls _send_server_notification if the mode variable is not set return getattr(self, '_send_{}_notification'.format( - 'webhook' if self.webhook else 'server'))( + 'webhook' if self.mode else 'server'))( body=body, title=title, notify_type=notify_type, **kwargs) def _send_webhook_notification(self, body, title='', @@ -200,7 +190,7 @@ class NotifyMatrix(NotifyBase): ) # Retrieve our payload - payload = getattr(self, '_{}_webhook_payload'.format(self.webhook))( + payload = getattr(self, '_{}_webhook_payload'.format(self.mode))( body=body, title=title, notify_type=notify_type, **kwargs) self.logger.debug('Matrix POST URL: %s (cert_verify=%r)' % ( @@ -221,7 +211,7 @@ class NotifyMatrix(NotifyBase): if r.status_code != requests.codes.ok: # We had a problem status_str = \ - NotifyBase.http_response_code_lookup( + NotifyMatrix.http_response_code_lookup( r.status_code, MATRIX_HTTP_ERROR_MAP) self.logger.warning( @@ -318,8 +308,8 @@ class NotifyMatrix(NotifyBase): else: # TEXT or MARKDOWN # Ensure our content is escaped - title = NotifyBase.escape_html(title) - body = NotifyBase.escape_html(body) + title = NotifyMatrix.escape_html(title) + body = NotifyMatrix.escape_html(body) payload['text'] = '{}{}'.format( '' if not title else '

{}

'.format(title), body) @@ -375,8 +365,11 @@ class NotifyMatrix(NotifyBase): title='' if not title else '{}\r\n'.format(title), body=body) - image_url = self.image_url(notify_type) - if self.thumbnail and image_url: + # Acquire our image url if we're configured to do so + image_url = None if not self.include_image else \ + self.image_url(notify_type) + + if image_url: # Define our payload image_payload = { 'msgtype': 'm.image', @@ -385,7 +378,7 @@ class NotifyMatrix(NotifyBase): } # Build our path path = '/rooms/{}/send/m.room.message'.format( - NotifyBase.quote(room_id)) + NotifyMatrix.quote(room_id)) # Post our content postokay, response = self._fetch(path, payload=image_payload) @@ -402,7 +395,7 @@ class NotifyMatrix(NotifyBase): # Build our path path = '/rooms/{}/send/m.room.message'.format( - NotifyBase.quote(room_id)) + NotifyMatrix.quote(room_id)) # Post our content postokay, response = self._fetch(path, payload=payload) @@ -446,7 +439,7 @@ class NotifyMatrix(NotifyBase): # Register postokay, response = \ self._fetch('/register', payload=payload, params=params) - if not postokay: + if not (postokay and isinstance(response, dict)): # Failed to register return False @@ -489,7 +482,7 @@ class NotifyMatrix(NotifyBase): # Build our URL postokay, response = self._fetch('/login', payload=payload) - if not postokay: + if not (postokay and isinstance(response, dict)): # Failed to login return False @@ -581,7 +574,7 @@ class NotifyMatrix(NotifyBase): ) # Build our URL - path = '/join/{}'.format(NotifyBase.quote(room_id)) + path = '/join/{}'.format(NotifyMatrix.quote(room_id)) # Make our query postokay, _ = self._fetch(path, payload=payload) @@ -612,7 +605,7 @@ class NotifyMatrix(NotifyBase): # If we reach here, we need to join the channel # Build our URL - path = '/join/{}'.format(NotifyBase.quote(room)) + path = '/join/{}'.format(NotifyMatrix.quote(room)) # Attempt to join the channel postokay, response = self._fetch(path, payload=payload) @@ -695,7 +688,7 @@ class NotifyMatrix(NotifyBase): return list() postokay, response = self._fetch( - '/joined_rooms', payload=None, fn=requests.get) + '/joined_rooms', payload=None, method='GET') if not postokay: # Failed to retrieve listings return list() @@ -736,14 +729,14 @@ class NotifyMatrix(NotifyBase): # Make our request postokay, response = self._fetch( "/directory/room/{}".format( - self.quote(room)), payload=None, fn=requests.get) + NotifyMatrix.quote(room)), payload=None, method='GET') if postokay: return response.get("room_id") return None - def _fetch(self, path, payload=None, params=None, fn=requests.post): + def _fetch(self, path, payload=None, params=None, method='POST'): """ Wrapper to request.post() to manage it's response better and make the send() function cleaner and easier to maintain. @@ -775,6 +768,9 @@ class NotifyMatrix(NotifyBase): # Our response object response = {} + # fetch function + fn = requests.post if method == 'POST' else requests.get + # Define how many attempts we'll make if we get caught in a throttle # event retries = self.default_retries if self.default_retries > 0 else 1 @@ -789,7 +785,7 @@ class NotifyMatrix(NotifyBase): self.logger.debug('Matrix Payload: %s' % str(payload)) try: - r = requests.post( + r = fn( url, data=dumps(payload), params=params, @@ -826,7 +822,7 @@ class NotifyMatrix(NotifyBase): elif r.status_code != requests.codes.ok: # We had a problem status_str = \ - NotifyBase.http_response_code_lookup( + NotifyMatrix.http_response_code_lookup( r.status_code, MATRIX_HTTP_ERROR_MAP) self.logger.warning( @@ -877,22 +873,23 @@ class NotifyMatrix(NotifyBase): args = { 'format': self.notify_format, 'overflow': self.overflow_mode, + 'image': 'yes' if self.include_image else 'no', } - if self.webhook: - args['webhook'] = self.webhook + if self.mode: + args['mode'] = self.mode - # Determine Authentication method + # Determine Authentication auth = '' if self.user and self.password: auth = '{user}:{password}@'.format( - user=self.quote(self.user, safe=''), - password=self.quote(self.password, safe=''), + user=NotifyMatrix.quote(self.user, safe=''), + password=NotifyMatrix.quote(self.password, safe=''), ) elif self.user: auth = '{user}@'.format( - user=self.quote(self.user, safe=''), + user=NotifyMatrix.quote(self.user, safe=''), ) default_port = 443 if self.secure else 80 @@ -900,11 +897,11 @@ class NotifyMatrix(NotifyBase): return '{schema}://{auth}{hostname}{port}/{rooms}?{args}'.format( schema=self.secure_protocol if self.secure else self.protocol, auth=auth, - hostname=self.host, + hostname=NotifyMatrix.quote(self.host, safe=''), port='' if self.port is None or self.port == default_port else ':{}'.format(self.port), - rooms=self.quote('/'.join(self.rooms)), - args=self.urlencode(args), + rooms=NotifyMatrix.quote('/'.join(self.rooms)), + args=NotifyMatrix.urlencode(args), ) @staticmethod @@ -921,15 +918,40 @@ class NotifyMatrix(NotifyBase): return results # Get our rooms - results['rooms'] = [ - NotifyBase.unquote(x) for x in filter(bool, NotifyBase.split_path( - results['fullpath']))][0:] + results['targets'] = NotifyMatrix.split_path(results['fullpath']) - # Use Thumbnail - results['thumbnail'] = \ - parse_bool(results['qsd'].get('thumbnail', False)) + # Support the 'to' variable so that we can support rooms this way too + # The 'to' makes it easier to use yaml configuration + if 'to' in results['qsd'] and len(results['qsd']['to']): + results['targets'] += NotifyMatrix.parse_list(results['qsd']['to']) - # Webhook - results['webhook'] = results['qsd'].get('webhook') + # Thumbnail (old way) + if 'thumbnail' in results['qsd']: + # Deprication Notice issued for v0.7.5 + NotifyMatrix.logger.warning( + 'DEPRICATION NOTICE - The Matrix URL contains the parameter ' + '"thumbnail=" which will be depricated 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.warning( + 'DEPRICATION NOTICE - The Matrix URL contains the parameter ' + '"webhook=" which will be depricated 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')) return results diff --git a/apprise/plugins/NotifyMatterMost.py b/apprise/plugins/NotifyMatterMost.py index 677dffd0..4d51edc0 100644 --- a/apprise/plugins/NotifyMatterMost.py +++ b/apprise/plugins/NotifyMatterMost.py @@ -30,6 +30,8 @@ from json import dumps from .NotifyBase import NotifyBase from ..common import NotifyImageSize from ..common import NotifyType +from ..utils import parse_bool +from ..utils import parse_list # Some Reference Locations: # - https://docs.mattermost.com/developer/webhooks-incoming.html @@ -71,7 +73,8 @@ class NotifyMatterMost(NotifyBase): # Mattermost does not have a title title_maxlen = 0 - def __init__(self, authtoken, channel=None, **kwargs): + def __init__(self, authtoken, channels=None, include_image=True, + **kwargs): """ Initialize MatterMost Object """ @@ -88,27 +91,24 @@ class NotifyMatterMost(NotifyBase): # Validate authtoken if not authtoken: - self.logger.warning( - 'Missing MatterMost Authorization Token.' - ) - raise TypeError( - 'Missing MatterMost Authorization Token.' - ) + msg = 'Missing MatterMost Authorization Token.' + self.logger.warning(msg) + raise TypeError(msg) if not VALIDATE_AUTHTOKEN.match(authtoken): - self.logger.warning( - 'Invalid MatterMost Authorization Token Specified.' - ) - raise TypeError( - 'Invalid MatterMost Authorization Token Specified.' - ) + msg = 'Invalid MatterMost Authorization Token Specified.' + self.logger.warning(msg) + raise TypeError(msg) - # A Channel (optional) - self.channel = channel + # Optional Channels + self.channels = parse_list(channels) if not self.port: self.port = self.default_port + # Place a thumbnail image inline with the message body + self.include_image = include_image + return def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs): @@ -116,6 +116,9 @@ class NotifyMatterMost(NotifyBase): Perform MatterMost Notification """ + # Create a copy of our channels, otherwise place a dummy entry + channels = list(self.channels) if self.channels else [None, ] + headers = { 'User-Agent': self.app_id, 'Content-Type': 'application/json' @@ -124,67 +127,91 @@ class NotifyMatterMost(NotifyBase): # prepare JSON Object payload = { 'text': body, - 'icon_url': self.image_url(notify_type), + 'icon_url': None, } - if self.user: - payload['username'] = self.user + # Acquire our image url if configured to do so + image_url = None if not self.include_image \ + else self.image_url(notify_type) - else: - payload['username'] = self.app_id + if image_url: + # Set our image configuration if told to do so + payload['icon_url'] = image_url - if self.channel: - payload['channel'] = self.channel + # Set our user + payload['username'] = self.user if self.user else self.app_id - url = '%s://%s:%d' % (self.schema, self.host, self.port) - url += '/hooks/%s' % self.authtoken + # For error tracking + has_error = False - self.logger.debug('MatterMost POST URL: %s (cert_verify=%r)' % ( - url, self.verify_certificate, - )) - self.logger.debug('MatterMost Payload: %s' % str(payload)) + while len(channels): + # Pop a channel off of the list + channel = channels.pop(0) - # Always call throttle before any remote server i/o is made - self.throttle() + if channel: + payload['channel'] = channel - try: - r = requests.post( - url, - data=dumps(payload), - headers=headers, - verify=self.verify_certificate, - ) - if r.status_code != requests.codes.ok: - # We had a problem - status_str = \ - NotifyBase.http_response_code_lookup(r.status_code) + url = '%s://%s:%d' % (self.schema, self.host, self.port) + url += '/hooks/%s' % self.authtoken + self.logger.debug('MatterMost POST URL: %s (cert_verify=%r)' % ( + url, self.verify_certificate, + )) + self.logger.debug('MatterMost Payload: %s' % str(payload)) + + # Always call throttle before any remote server i/o is made + self.throttle() + + try: + r = requests.post( + url, + data=dumps(payload), + headers=headers, + verify=self.verify_certificate, + ) + + if r.status_code != requests.codes.ok: + # We had a problem + status_str = \ + NotifyMatterMost.http_response_code_lookup( + r.status_code) + + self.logger.warning( + 'Failed to send MatterMost notification{}: ' + '{}{}error={}.'.format( + '' if not channel + else ' to channel {}'.format(channel), + status_str, + ', ' if status_str else '', + r.status_code)) + + self.logger.debug( + 'Response Details:\r\n{}'.format(r.content)) + + # Flag our error + has_error = True + continue + + else: + self.logger.info( + 'Sent MatterMost notification{}.'.format( + '' if not channel + else ' to channel {}'.format(channel))) + + except requests.RequestException as e: self.logger.warning( - 'Failed to send MatterMost notification: ' - '{}{}error={}.'.format( - status_str, - ', ' if status_str else '', - r.status_code)) + 'A Connection error occured sending MatterMost ' + 'notification{}.'.format( + '' if not channel + else ' to channel {}'.format(channel))) + self.logger.debug('Socket Exception: %s' % str(e)) - self.logger.debug('Response Details:\r\n{}'.format(r.content)) + # Flag our error + has_error = True + continue - # Return; we're done - return False - - else: - self.logger.info('Sent MatterMost notification.') - - except requests.RequestException as e: - self.logger.warning( - 'A Connection error occured sending MatterMost ' - 'notification.' - ) - self.logger.debug('Socket Exception: %s' % str(e)) - - # Return; we're done - return False - - return True + # Return our overall status + return not has_error def url(self): """ @@ -195,18 +222,25 @@ class NotifyMatterMost(NotifyBase): args = { 'format': self.notify_format, 'overflow': self.overflow_mode, + 'image': 'yes' if self.include_image else 'no', } + 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) + default_port = 443 if self.secure else self.default_port default_schema = self.secure_protocol if self.secure else self.protocol return '{schema}://{hostname}{port}/{authtoken}/?{args}'.format( schema=default_schema, - hostname=self.host, + hostname=NotifyMatterMost.quote(self.host, safe=''), port='' if not self.port or self.port == default_port else ':{}'.format(self.port), - authtoken=self.quote(self.authtoken, safe=''), - args=self.urlencode(args), + authtoken=NotifyMatterMost.quote(self.authtoken, safe=''), + args=NotifyMatterMost.urlencode(args), ) @staticmethod @@ -222,15 +256,31 @@ class NotifyMatterMost(NotifyBase): # We're done early as we couldn't load the results return results - # Apply our settings now - authtoken = NotifyBase.split_path(results['fullpath'])[0] + try: + # Apply our settings now + results['authtoken'] = \ + NotifyMatterMost.split_path(results['fullpath'])[0] + + except IndexError: + # There was no Authorization Token specified + results['authtoken'] = None + + # Define our optional list of channels to notify + results['channels'] = list() + + # Support both 'to' (for yaml configuration) and channel= + if 'to' in results['qsd'] and len(results['qsd']['to']): + # Allow the user to specify the channel to post to + results['channels'].append( + NotifyMatterMost.parse_list(results['qsd']['to'])) - channel = None if 'channel' in results['qsd'] and len(results['qsd']['channel']): # Allow the user to specify the channel to post to - channel = NotifyBase.unquote(results['qsd']['channel']).strip() + results['channels'].append( + NotifyMatterMost.parse_list(results['qsd']['channel'])) - results['authtoken'] = authtoken - results['channel'] = channel + # Image manipulation + results['include_image'] = \ + parse_bool(results['qsd'].get('image', False)) return results diff --git a/apprise/plugins/NotifyProwl.py b/apprise/plugins/NotifyProwl.py index 7202b6af..cf0f25b6 100644 --- a/apprise/plugins/NotifyProwl.py +++ b/apprise/plugins/NotifyProwl.py @@ -103,12 +103,9 @@ class NotifyProwl(NotifyBase): self.priority = priority if not VALIDATE_APIKEY.match(apikey): - self.logger.warning( - 'The API key specified (%s) is invalid.' % apikey, - ) - raise TypeError( - 'The API key specified (%s) is invalid.' % apikey, - ) + msg = 'The API key specified ({}) is invalid.'.format(apikey) + self.logger.warning(msg) + raise TypeError(msg) # Store the API key self.apikey = apikey @@ -116,13 +113,12 @@ class NotifyProwl(NotifyBase): # Store the provider key (if specified) if providerkey: if not VALIDATE_PROVIDERKEY.match(providerkey): - self.logger.warning( - 'The Provider key specified (%s) ' - 'is invalid.' % providerkey) + msg = \ + 'The Provider key specified ({}) is invalid.' \ + .format(providerkey) - raise TypeError( - 'The Provider key specified (%s) ' - 'is invalid.' % providerkey) + self.logger.warning(msg) + raise TypeError(msg) # Store the Provider Key self.providerkey = providerkey @@ -218,10 +214,10 @@ class NotifyProwl(NotifyBase): return '{schema}://{apikey}/{providerkey}/?{args}'.format( schema=self.secure_protocol, - apikey=self.quote(self.apikey, safe=''), + apikey=NotifyProwl.quote(self.apikey, safe=''), providerkey='' if not self.providerkey - else self.quote(self.providerkey, safe=''), - args=self.urlencode(args), + else NotifyProwl.quote(self.providerkey, safe=''), + args=NotifyProwl.urlencode(args), ) @staticmethod @@ -237,15 +233,16 @@ class NotifyProwl(NotifyBase): # We're done early as we couldn't load the results return results - # Apply our settings now + # Set the API Key + results['apikey'] = NotifyProwl.unquote(results['host']) - # optionally find the provider key + # Optionally try to find the provider key try: - providerkey = [x for x in filter( - bool, NotifyBase.split_path(results['fullpath']))][0] + results['providerkey'] = \ + NotifyProwl.split_path(results['fullpath'])[0] - except (AttributeError, IndexError): - providerkey = None + except IndexError: + pass if 'priority' in results['qsd'] and len(results['qsd']['priority']): _map = { @@ -263,7 +260,4 @@ class NotifyProwl(NotifyBase): # No priority was set pass - results['apikey'] = results['host'] - results['providerkey'] = providerkey - return results diff --git a/apprise/plugins/NotifyPushBullet.py b/apprise/plugins/NotifyPushBullet.py index 52cd3f41..62837974 100644 --- a/apprise/plugins/NotifyPushBullet.py +++ b/apprise/plugins/NotifyPushBullet.py @@ -23,22 +23,17 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. -import re -import six import requests from json import dumps from .NotifyBase import NotifyBase from ..utils import GET_EMAIL_RE from ..common import NotifyType +from ..utils import parse_list # Flag used as a placeholder to sending to all devices PUSHBULLET_SEND_TO_ALL = 'ALL_DEVICES' -# Used to break apart list of potential recipients by their delimiter -# into a usable list. -RECIPIENTS_LIST_DELIM = re.compile(r'[ \t\r\n,\\/]+') - # Provide some known codes Pushbullet uses and what they translate to: PUSHBULLET_HTTP_ERROR_MAP = { 401: 'Unauthorized - Invalid Token.', @@ -65,25 +60,17 @@ class NotifyPushBullet(NotifyBase): # PushBullet uses the http protocol with JSON requests notify_url = 'https://api.pushbullet.com/v2/pushes' - def __init__(self, accesstoken, recipients=None, **kwargs): + def __init__(self, accesstoken, targets=None, **kwargs): """ Initialize PushBullet Object """ super(NotifyPushBullet, self).__init__(**kwargs) self.accesstoken = accesstoken - if isinstance(recipients, six.string_types): - self.recipients = [x for x in filter( - bool, RECIPIENTS_LIST_DELIM.split(recipients))] - elif isinstance(recipients, (set, tuple, list)): - self.recipients = recipients - - else: - self.recipients = list() - - if len(self.recipients) == 0: - self.recipients = (PUSHBULLET_SEND_TO_ALL, ) + self.targets = parse_list(targets) + if len(self.targets) == 0: + self.targets = (PUSHBULLET_SEND_TO_ALL, ) def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs): """ @@ -99,10 +86,10 @@ class NotifyPushBullet(NotifyBase): # error tracking (used for function return) has_error = False - # Create a copy of the recipients list - recipients = list(self.recipients) - while len(recipients): - recipient = recipients.pop(0) + # Create a copy of the targets list + targets = list(self.targets) + while len(targets): + recipient = targets.pop(0) # prepare JSON Object payload = { @@ -149,7 +136,7 @@ class NotifyPushBullet(NotifyBase): if r.status_code != requests.codes.ok: # We had a problem status_str = \ - NotifyBase.http_response_code_lookup( + NotifyPushBullet.http_response_code_lookup( r.status_code, PUSHBULLET_HTTP_ERROR_MAP) self.logger.warning( @@ -195,17 +182,17 @@ class NotifyPushBullet(NotifyBase): 'overflow': self.overflow_mode, } - recipients = '/'.join([self.quote(x) for x in self.recipients]) - if recipients == PUSHBULLET_SEND_TO_ALL: + targets = '/'.join([NotifyPushBullet.quote(x) for x in self.targets]) + if targets == PUSHBULLET_SEND_TO_ALL: # keyword is reserved for internal usage only; it's safe to remove # it from the recipients list - recipients = '' + targets = '' - return '{schema}://{accesstoken}/{recipients}/?{args}'.format( + return '{schema}://{accesstoken}/{targets}/?{args}'.format( schema=self.secure_protocol, - accesstoken=self.quote(self.accesstoken, safe=''), - recipients=recipients, - args=self.urlencode(args)) + accesstoken=NotifyPushBullet.quote(self.accesstoken, safe=''), + targets=targets, + args=NotifyPushBullet.urlencode(args)) @staticmethod def parse_url(url): @@ -220,10 +207,17 @@ class NotifyPushBullet(NotifyBase): # We're done early as we couldn't load the results return results - # Apply our settings now - recipients = NotifyBase.unquote(results['fullpath']) + # Fetch our targets + results['targets'] = \ + NotifyPushBullet.split_path(results['fullpath']) - results['accesstoken'] = results['host'] - results['recipients'] = recipients + # The 'to' makes it easier to use yaml configuration + if 'to' in results['qsd'] and len(results['qsd']['to']): + results['targets'] += \ + NotifyPushBullet.parse_list(results['qsd']['to']) + + # Setup the token; we store it in Access Token for global + # plugin consistency with naming conventions + results['accesstoken'] = NotifyPushBullet.unquote(results['host']) return results diff --git a/apprise/plugins/NotifyPushed.py b/apprise/plugins/NotifyPushed.py index 05782a72..fe90905c 100644 --- a/apprise/plugins/NotifyPushed.py +++ b/apprise/plugins/NotifyPushed.py @@ -24,13 +24,13 @@ # THE SOFTWARE. import re -import six import requests from json import dumps from itertools import chain from .NotifyBase import NotifyBase from ..common import NotifyType +from ..utils import parse_list # Used to detect and parse channels IS_CHANNEL = re.compile(r'^#(?P[A-Za-z0-9]+)$') @@ -38,10 +38,6 @@ IS_CHANNEL = re.compile(r'^#(?P[A-Za-z0-9]+)$') # Used to detect and parse a users push id IS_USER_PUSHED_ID = re.compile(r'^@(?P[A-Za-z0-9]+)$') -# Used to break apart list of potential tags by their delimiter -# into a usable list. -LIST_DELIM = re.compile(r'[ \t\r\n,\\/]+') - class NotifyPushed(NotifyBase): """ @@ -71,7 +67,7 @@ class NotifyPushed(NotifyBase): # The maximum allowable characters allowed in the body per message body_maxlen = 140 - def __init__(self, app_key, app_secret, recipients=None, **kwargs): + def __init__(self, app_key, app_secret, targets=None, **kwargs): """ Initialize Pushed Object @@ -79,14 +75,14 @@ class NotifyPushed(NotifyBase): super(NotifyPushed, self).__init__(**kwargs) if not app_key: - raise TypeError( - 'An invalid Application Key was specified.' - ) + msg = 'An invalid Application Key was specified.' + self.logger.warning(msg) + raise TypeError(msg) if not app_secret: - raise TypeError( - 'An invalid Application Secret was specified.' - ) + msg = 'An invalid Application Secret was specified.' + self.logger.warning(msg) + raise TypeError(msg) # Initialize channel list self.channels = list() @@ -94,28 +90,15 @@ class NotifyPushed(NotifyBase): # Initialize user list self.users = list() - if recipients is None: - recipients = [] - - elif isinstance(recipients, six.string_types): - recipients = [x for x in filter(bool, LIST_DELIM.split( - recipients, - ))] - - elif not isinstance(recipients, (set, tuple, list)): - raise TypeError( - 'An invalid receipient list was specified.' - ) - # Validate recipients and drop bad ones: - for recipient in recipients: - result = IS_CHANNEL.match(recipient) + for target in parse_list(targets): + result = IS_CHANNEL.match(target) if result: # store valid device self.channels.append(result.group('name')) continue - result = IS_USER_PUSHED_ID.match(recipient) + result = IS_USER_PUSHED_ID.match(target) if result: # store valid room self.users.append(result.group('name')) @@ -123,7 +106,7 @@ class NotifyPushed(NotifyBase): self.logger.warning( 'Dropped invalid channel/userid ' - '(%s) specified.' % recipient, + '(%s) specified.' % target, ) # Store our data @@ -229,7 +212,7 @@ class NotifyPushed(NotifyBase): if r.status_code != requests.codes.ok: # We had a problem status_str = \ - NotifyBase.http_response_code_lookup(r.status_code) + NotifyPushed.http_response_code_lookup(r.status_code) self.logger.warning( 'Failed to send Pushed notification:' @@ -269,16 +252,16 @@ class NotifyPushed(NotifyBase): return '{schema}://{app_key}/{app_secret}/{targets}/?{args}'.format( schema=self.secure_protocol, - app_key=self.quote(self.app_key, safe=''), - app_secret=self.quote(self.app_secret, safe=''), + app_key=NotifyPushed.quote(self.app_key, safe=''), + app_secret=NotifyPushed.quote(self.app_secret, safe=''), targets='/'.join( - [self.quote(x) for x in chain( + [NotifyPushed.quote(x) for x in chain( # Channels are prefixed with a pound/hashtag symbol ['#{}'.format(x) for x in self.channels], # Users are prefixed with an @ symbol ['@{}'.format(x) for x in self.users], )]), - args=self.urlencode(args)) + args=NotifyPushed.urlencode(args)) @staticmethod def parse_url(url): @@ -296,30 +279,28 @@ class NotifyPushed(NotifyBase): # Apply our settings now # The first token is stored in the hostname - app_key = results['host'] - - # Initialize our recipients - recipients = None + app_key = NotifyPushed.unquote(results['host']) + entries = NotifyPushed.split_path(results['fullpath']) # Now fetch the remaining tokens try: - app_secret = \ - [x for x in filter(bool, NotifyBase.split_path( - results['fullpath']))][0] + app_secret = entries.pop(0) - except (ValueError, AttributeError, IndexError): + except IndexError: # Force some bad values that will get caught # in parsing later app_secret = None app_key = None - # Get our recipients - recipients = \ - [x for x in filter(bool, NotifyBase.split_path( - results['fullpath']))][1:] + # Get our recipients (based on remaining entries) + results['targets'] = entries + + # The 'to' makes it easier to use yaml configuration + if 'to' in results['qsd'] and len(results['qsd']['to']): + results['targets'] += \ + NotifyPushed.parse_list(results['qsd']['to']) results['app_key'] = app_key results['app_secret'] = app_secret - results['recipients'] = recipients return results diff --git a/apprise/plugins/NotifyPushjet/__init__.py b/apprise/plugins/NotifyPushjet/__init__.py index e158164b..e02aad1b 100644 --- a/apprise/plugins/NotifyPushjet/__init__.py +++ b/apprise/plugins/NotifyPushjet/__init__.py @@ -62,6 +62,12 @@ class NotifyPushjet(NotifyBase): """ super(NotifyPushjet, self).__init__(**kwargs) + if not secret_key: + # You must provide a Pushjet key to work with + msg = 'You must specify a Pushjet Secret Key.' + self.logger.warning(msg) + raise TypeError(msg) + # store our key self.secret_key = secret_key @@ -107,11 +113,11 @@ class NotifyPushjet(NotifyBase): return '{schema}://{secret_key}@{hostname}{port}/?{args}'.format( schema=self.secure_protocol if self.secure else self.protocol, - secret_key=self.quote(self.secret_key, safe=''), - hostname=self.host, + secret_key=NotifyPushjet.quote(self.secret_key, safe=''), + hostname=NotifyPushjet.quote(self.host, safe=''), port='' if self.port is None or self.port == default_port else ':{}'.format(self.port), - args=self.urlencode(args), + args=NotifyPushjet.urlencode(args), ) @staticmethod @@ -133,11 +139,8 @@ class NotifyPushjet(NotifyBase): # We're done early as we couldn't load the results return results - if not results.get('user'): - # a username is required - return None - # Store it as it's value - results['secret_key'] = results.get('user') + results['secret_key'] = \ + NotifyPushjet.unquote(results.get('user')) return results diff --git a/apprise/plugins/NotifyPushover.py b/apprise/plugins/NotifyPushover.py index 8739381c..49d74350 100644 --- a/apprise/plugins/NotifyPushover.py +++ b/apprise/plugins/NotifyPushover.py @@ -24,11 +24,11 @@ # THE SOFTWARE. import re -import six import requests from .NotifyBase import NotifyBase from ..common import NotifyType +from ..utils import parse_list # Flag used as a placeholder to sending to all devices PUSHOVER_SEND_TO_ALL = 'ALL_DEVICES' @@ -60,9 +60,6 @@ PUSHOVER_PRIORITIES = ( PushoverPriority.EMERGENCY, ) -# Used to break path apart into list of devices -DEVICE_LIST_DELIM = re.compile(r'[ \t\r\n,\\/]+') - # Extend HTTP Error Messages PUSHOVER_HTTP_ERROR_MAP = { 401: 'Unauthorized - Invalid Token.', @@ -92,7 +89,7 @@ class NotifyPushover(NotifyBase): # The maximum allowable characters allowed in the body per message body_maxlen = 512 - def __init__(self, token, devices=None, priority=None, **kwargs): + def __init__(self, token, targets=None, priority=None, **kwargs): """ Initialize Pushover Object """ @@ -104,30 +101,18 @@ class NotifyPushover(NotifyBase): except AttributeError: # Token was None - self.logger.warning('No API Token was specified.') - raise TypeError('No API Token was specified.') + msg = 'No API Token was specified.' + self.logger.warning(msg) + raise TypeError(msg) if not VALIDATE_TOKEN.match(self.token): - self.logger.warning( - 'The API Token specified (%s) is invalid.' % token, - ) - raise TypeError( - 'The API Token specified (%s) is invalid.' % token, - ) + msg = 'The API Token specified (%s) is invalid.'.format(token) + self.logger.warning(msg) + raise TypeError(msg) - if isinstance(devices, six.string_types): - self.devices = [x for x in filter(bool, DEVICE_LIST_DELIM.split( - devices, - ))] - - elif isinstance(devices, (set, tuple, list)): - self.devices = devices - - else: - self.devices = list() - - if len(self.devices) == 0: - self.devices = (PUSHOVER_SEND_TO_ALL, ) + self.targets = parse_list(targets) + if len(self.targets) == 0: + self.targets = (PUSHOVER_SEND_TO_ALL, ) # The Priority of the message if priority not in PUSHOVER_PRIORITIES: @@ -137,16 +122,14 @@ class NotifyPushover(NotifyBase): self.priority = priority if not self.user: - self.logger.warning('No user was specified.') - raise TypeError('No user was specified.') + msg = 'No user was specified.' + self.logger.warning(msg) + raise TypeError(msg) if not VALIDATE_USERGROUP.match(self.user): - self.logger.warning( - 'The user/group specified (%s) is invalid.' % self.user, - ) - raise TypeError( - 'The user/group specified (%s) is invalid.' % self.user, - ) + msg = 'The user/group specified (%s) is invalid.' % self.user + self.logger.warning(msg) + raise TypeError(msg) def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs): """ @@ -163,7 +146,7 @@ class NotifyPushover(NotifyBase): has_error = False # Create a copy of the devices list - devices = list(self.devices) + devices = list(self.targets) while len(devices): device = devices.pop(0) @@ -205,7 +188,7 @@ class NotifyPushover(NotifyBase): if r.status_code != requests.codes.ok: # We had a problem status_str = \ - NotifyBase.http_response_code_lookup( + NotifyPushover.http_response_code_lookup( r.status_code, PUSHOVER_HTTP_ERROR_MAP) self.logger.warning( @@ -262,7 +245,10 @@ class NotifyPushover(NotifyBase): else _map[self.priority], } - devices = '/'.join([self.quote(x) for x in self.devices]) + # Escape our devices + devices = '/'.join([NotifyPushover.quote(x, safe='') + for x in self.targets]) + if devices == PUSHOVER_SEND_TO_ALL: # keyword is reserved for internal usage only; it's safe to remove # it from the devices list @@ -271,10 +257,11 @@ class NotifyPushover(NotifyBase): return '{schema}://{auth}{token}/{devices}/?{args}'.format( schema=self.secure_protocol, auth='' if not self.user - else '{user}@'.format(user=self.quote(self.user, safe='')), - token=self.quote(self.token, safe=''), + else '{user}@'.format( + user=NotifyPushover.quote(self.user, safe='')), + token=NotifyPushover.quote(self.token, safe=''), devices=devices, - args=self.urlencode(args)) + args=NotifyPushover.urlencode(args)) @staticmethod def parse_url(url): @@ -289,21 +276,14 @@ class NotifyPushover(NotifyBase): # We're done early as we couldn't load the results return results - # Apply our settings now - devices = NotifyBase.unquote(results['fullpath']) - + # Set our priority if 'priority' in results['qsd'] and len(results['qsd']['priority']): _map = { 'l': PushoverPriority.LOW, - '-2': PushoverPriority.LOW, 'm': PushoverPriority.MODERATE, - '-1': PushoverPriority.MODERATE, 'n': PushoverPriority.NORMAL, - '0': PushoverPriority.NORMAL, 'h': PushoverPriority.HIGH, - '1': PushoverPriority.HIGH, 'e': PushoverPriority.EMERGENCY, - '2': PushoverPriority.EMERGENCY, } try: results['priority'] = \ @@ -313,7 +293,15 @@ class NotifyPushover(NotifyBase): # No priority was set pass - results['token'] = results['host'] - results['devices'] = devices + # Retrieve all of our targets + results['targets'] = NotifyPushover.split_path(results['fullpath']) + + # The 'to' makes it easier to use yaml configuration + if 'to' in results['qsd'] and len(results['qsd']['to']): + results['targets'] += \ + NotifyPushover.parse_list(results['qsd']['to']) + + # Token + results['token'] = NotifyPushover.unquote(results['host']) return results diff --git a/apprise/plugins/NotifyRocketChat.py b/apprise/plugins/NotifyRocketChat.py index 73115e6d..59783f08 100644 --- a/apprise/plugins/NotifyRocketChat.py +++ b/apprise/plugins/NotifyRocketChat.py @@ -24,13 +24,13 @@ # THE SOFTWARE. import re -import six import requests from json import loads from itertools import chain from .NotifyBase import NotifyBase from ..common import NotifyType +from ..utils import parse_list IS_CHANNEL = re.compile(r'^#(?P[A-Za-z0-9]+)$') IS_ROOM_ID = re.compile(r'^(?P[A-Za-z0-9]+)$') @@ -72,17 +72,14 @@ class NotifyRocketChat(NotifyBase): # The maximum size of the message body_maxlen = 200 - def __init__(self, recipients=None, **kwargs): + def __init__(self, targets=None, **kwargs): """ Initialize Notify Rocket.Chat Object """ super(NotifyRocketChat, self).__init__(**kwargs) - if self.secure: - self.schema = 'https' - - else: - self.schema = 'http' + # Set our schema + self.schema = 'https' if self.secure else 'http' # Prepare our URL self.api_url = '%s://%s' % (self.schema, self.host) @@ -98,17 +95,6 @@ class NotifyRocketChat(NotifyBase): # Initialize room list self.rooms = list() - if recipients is None: - recipients = [] - - elif isinstance(recipients, six.string_types): - recipients = [x for x in filter(bool, LIST_DELIM.split( - recipients, - ))] - - elif not isinstance(recipients, (set, tuple, list)): - recipients = [] - if not (self.user and self.password): # Username & Password is required for Rocket Chat to work raise TypeError( @@ -116,7 +102,7 @@ class NotifyRocketChat(NotifyBase): ) # Validate recipients and drop bad ones: - for recipient in recipients: + for recipient in parse_list(targets): result = IS_CHANNEL.match(recipient) if result: # store valid device @@ -135,9 +121,9 @@ class NotifyRocketChat(NotifyBase): ) if len(self.rooms) == 0 and len(self.channels) == 0: - raise TypeError( - 'No Rocket.Chat room and/or channels specified to notify.' - ) + msg = 'No Rocket.Chat room and/or channels specified to notify.' + self.logger.warning(msg) + raise TypeError(msg) # Used to track token headers upon authentication (if successful) self.headers = {} @@ -155,8 +141,8 @@ class NotifyRocketChat(NotifyBase): # Determine Authentication auth = '{user}:{password}@'.format( - user=self.quote(self.user, safe=''), - password=self.quote(self.password, safe=''), + user=NotifyRocketChat.quote(self.user, safe=''), + password=NotifyRocketChat.quote(self.password, safe=''), ) default_port = 443 if self.secure else 80 @@ -164,17 +150,17 @@ class NotifyRocketChat(NotifyBase): return '{schema}://{auth}{hostname}{port}/{targets}/?{args}'.format( schema=self.secure_protocol if self.secure else self.protocol, auth=auth, - hostname=self.host, + hostname=NotifyRocketChat.quote(self.host, safe=''), port='' if self.port is None or self.port == default_port else ':{}'.format(self.port), targets='/'.join( - [self.quote(x) for x in chain( + [NotifyRocketChat.quote(x, safe='') for x in chain( # Channels are prefixed with a pound/hashtag symbol ['#{}'.format(x) for x in self.channels], # Rooms are as is self.rooms, )]), - args=self.urlencode(args), + args=NotifyRocketChat.urlencode(args), ) def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs): @@ -252,7 +238,7 @@ class NotifyRocketChat(NotifyBase): if r.status_code != requests.codes.ok: # We had a problem status_str = \ - NotifyBase.http_response_code_lookup( + NotifyRocketChat.http_response_code_lookup( r.status_code, RC_HTTP_ERROR_MAP) self.logger.warning( @@ -300,7 +286,7 @@ class NotifyRocketChat(NotifyBase): if r.status_code != requests.codes.ok: # We had a problem status_str = \ - NotifyBase.http_response_code_lookup( + NotifyRocketChat.http_response_code_lookup( r.status_code, RC_HTTP_ERROR_MAP) self.logger.warning( @@ -353,7 +339,7 @@ class NotifyRocketChat(NotifyBase): if r.status_code != requests.codes.ok: # We had a problem status_str = \ - NotifyBase.http_response_code_lookup( + NotifyRocketChat.http_response_code_lookup( r.status_code, RC_HTTP_ERROR_MAP) self.logger.warning( @@ -396,7 +382,12 @@ class NotifyRocketChat(NotifyBase): # We're done early as we couldn't load the results return results - # Apply our settings now - results['recipients'] = NotifyBase.unquote(results['fullpath']) + # Apply our targets + results['targets'] = NotifyRocketChat.split_path(results['fullpath']) + + # The 'to' makes it easier to use yaml configuration + if 'to' in results['qsd'] and len(results['qsd']['to']): + results['targets'] += \ + NotifyRocketChat.parse_list(results['qsd']['to']) return results diff --git a/apprise/plugins/NotifyRyver.py b/apprise/plugins/NotifyRyver.py index a7cfcd8e..124c2de5 100644 --- a/apprise/plugins/NotifyRyver.py +++ b/apprise/plugins/NotifyRyver.py @@ -32,12 +32,14 @@ # These are important <---^----------------------------------------^ # import re +import six import requests from json import dumps from .NotifyBase import NotifyBase from ..common import NotifyImageSize from ..common import NotifyType +from ..utils import parse_bool # Token required as part of the API request VALIDATE_TOKEN = re.compile(r'[A-Za-z0-9]{15}') @@ -46,18 +48,18 @@ VALIDATE_TOKEN = re.compile(r'[A-Za-z0-9]{15}') VALIDATE_ORG = re.compile(r'[A-Za-z0-9-]{3,32}') -class RyverWebhookType(object): +class RyverWebhookMode(object): """ - Ryver supports to webhook types + Ryver supports to webhook modes """ SLACK = 'slack' RYVER = 'ryver' # Define the types in a list for validation purposes -RYVER_WEBHOOK_TYPES = ( - RyverWebhookType.SLACK, - RyverWebhookType.RYVER, +RYVER_WEBHOOK_MODES = ( + RyverWebhookMode.SLACK, + RyverWebhookMode.RYVER, ) @@ -84,39 +86,44 @@ class NotifyRyver(NotifyBase): # The maximum allowable characters allowed in the body per message body_maxlen = 1000 - def __init__(self, organization, token, webhook=RyverWebhookType.RYVER, - **kwargs): + def __init__(self, organization, token, mode=RyverWebhookMode.RYVER, + include_image=True, **kwargs): """ Initialize Ryver Object """ super(NotifyRyver, self).__init__(**kwargs) + if not token: + msg = 'No Ryver token was specified.' + self.logger.warning(msg) + raise TypeError(msg) + + if not organization: + msg = 'No Ryver organization was specified.' + self.logger.warning(msg) + raise TypeError(msg) + if not VALIDATE_TOKEN.match(token.strip()): - self.logger.warning( - 'The token specified (%s) is invalid.' % token, - ) - raise TypeError( - 'The token specified (%s) is invalid.' % token, - ) + msg = 'The Ryver token specified ({}) is invalid.'\ + .format(token) + self.logger.warning(msg) + raise TypeError(msg) if not VALIDATE_ORG.match(organization.strip()): - self.logger.warning( - 'The organization specified (%s) is invalid.' % organization, - ) - raise TypeError( - 'The organization specified (%s) is invalid.' % organization, - ) + msg = 'The Ryver organization specified ({}) is invalid.'\ + .format(organization) + self.logger.warning(msg) + raise TypeError(msg) - # Store our webhook type - self.webhook = webhook + # Store our webhook mode + self.mode = None \ + if not isinstance(mode, six.string_types) else mode.lower() - if self.webhook not in RYVER_WEBHOOK_TYPES: - self.logger.warning( - 'The webhook specified (%s) is invalid.' % webhook, - ) - raise TypeError( - 'The webhook specified (%s) is invalid.' % webhook, - ) + if self.mode not in RYVER_WEBHOOK_MODES: + msg = 'The Ryver webhook mode specified ({}) is invalid.' \ + .format(mode) + self.logger.warning(msg) + raise TypeError(msg) # The organization associated with the account self.organization = organization.strip() @@ -124,6 +131,9 @@ class NotifyRyver(NotifyBase): # The token associated with the account self.token = token.strip() + # Place an image inline with the message body + self.include_image = include_image + # Slack formatting requirements are defined here which Ryver supports: # https://api.slack.com/docs/message-formatting self._re_formatting_map = { @@ -151,7 +161,7 @@ class NotifyRyver(NotifyBase): 'Content-Type': 'application/json', } - if self.webhook == RyverWebhookType.SLACK: + if self.mode == RyverWebhookMode.SLACK: # Perform Slack formatting title = self._re_formatting_rules.sub( # pragma: no branch lambda x: self._re_formatting_map[x.group()], title, @@ -160,20 +170,27 @@ class NotifyRyver(NotifyBase): lambda x: self._re_formatting_map[x.group()], body, ) - url = 'https://%s.ryver.com/application/webhook/%s' % ( + url = 'https://{}.ryver.com/application/webhook/{}'.format( self.organization, self.token, ) # prepare JSON Object payload = { - "body": body if not title else '**{}**\r\n{}'.format(title, body), + 'body': body if not title else '**{}**\r\n{}'.format(title, body), 'createSource': { - "displayName": self.user, - "avatar": self.image_url(notify_type), + 'displayName': self.user, + 'avatar': None, }, } + # Acquire our image url if configured to do so + image_url = None if not self.include_image else \ + self.image_url(notify_type) + + if image_url: + payload['createSource']['avatar'] = image_url + self.logger.debug('Ryver POST URL: %s (cert_verify=%r)' % ( url, self.verify_certificate, )) @@ -229,22 +246,23 @@ class NotifyRyver(NotifyBase): args = { 'format': self.notify_format, 'overflow': self.overflow_mode, - 'webhook': self.webhook, + 'image': 'yes' if self.include_image else 'no', + 'mode': self.mode, } # Determine if there is a botname present botname = '' if self.user: botname = '{botname}@'.format( - botname=self.quote(self.user, safe=''), + botname=NotifyRyver.quote(self.user, safe=''), ) return '{schema}://{botname}{organization}/{token}/?{args}'.format( schema=self.secure_protocol, botname=botname, - organization=self.quote(self.organization, safe=''), - token=self.quote(self.token, safe=''), - args=self.urlencode(args), + organization=NotifyRyver.quote(self.organization, safe=''), + token=NotifyRyver.quote(self.token, safe=''), + args=NotifyRyver.urlencode(args), ) @staticmethod @@ -254,31 +272,41 @@ class NotifyRyver(NotifyBase): 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 - # Apply our settings now - # The first token is stored in the hostname - organization = results['host'] + results['organization'] = NotifyRyver.unquote(results['host']) # Now fetch the remaining tokens try: - token = [x for x in filter( - bool, NotifyBase.split_path(results['fullpath']))][0] + results['token'] = \ + NotifyRyver.split_path(results['fullpath'])[0] - except (ValueError, AttributeError, IndexError): - # We're done - return None + except IndexError: + # no token + results['token'] = None - if 'webhook' in results['qsd'] and len(results['qsd']['webhook']): - results['webhook'] = results['qsd']\ - .get('webhook', RyverWebhookType.RYVER).lower() + if 'webhook' in results['qsd']: + # Deprication Notice issued for v0.7.5 + NotifyRyver.logger.warning( + 'DEPRICATION NOTICE - The Ryver URL contains the parameter ' + '"webhook=" which will be depricated in an upcoming ' + 'release. Please use "mode=" instead.' + ) - results['organization'] = organization - results['token'] = token + # 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)) + + # use image= for consistency with the other plugins + results['include_image'] = \ + parse_bool(results['qsd'].get('image', True)) return results diff --git a/apprise/plugins/NotifySNS.py b/apprise/plugins/NotifySNS.py index b0b792fb..2d2420b6 100644 --- a/apprise/plugins/NotifySNS.py +++ b/apprise/plugins/NotifySNS.py @@ -24,7 +24,6 @@ # THE SOFTWARE. import re -import six import hmac import requests from hashlib import sha256 @@ -35,6 +34,7 @@ from itertools import chain from .NotifyBase import NotifyBase from ..common import NotifyType +from ..utils import parse_list # Some Phone Number Detection IS_PHONE_NO = re.compile(r'^\+?(?P[0-9\s)(+-]+)\s*$') @@ -50,10 +50,6 @@ IS_PHONE_NO = re.compile(r'^\+?(?P[0-9\s)(+-]+)\s*$') # ambiguity between a topic that is comprised of all digits and a phone number IS_TOPIC = re.compile(r'^#?(?P[A-Za-z0-9_-]+)\s*$') -# Used to break apart list of potential tags by their delimiter -# into a usable list. -LIST_DELIM = re.compile(r'[ \t\r\n,\\/]+') - # Because our AWS Access Key Secret contains slashes, we actually use the # region as a delimiter. This is a bit hacky; but it's much easier than having # users of this product search though this Access Key Secret and escape all @@ -97,26 +93,26 @@ class NotifySNS(NotifyBase): title_maxlen = 0 def __init__(self, access_key_id, secret_access_key, region_name, - recipients=None, **kwargs): + targets=None, **kwargs): """ Initialize Notify AWS SNS Object """ super(NotifySNS, self).__init__(**kwargs) if not access_key_id: - raise TypeError( - 'An invalid AWS Access Key ID was specified.' - ) + msg = 'An invalid AWS Access Key ID was specified.' + self.logger.warning(msg) + raise TypeError(msg) if not secret_access_key: - raise TypeError( - 'An invalid AWS Secret Access Key was specified.' - ) + msg = 'An invalid AWS Secret Access Key was specified.' + self.logger.warning(msg) + raise TypeError(msg) if not (region_name and IS_REGION.match(region_name)): - raise TypeError( - 'An invalid AWS Region was specified.' - ) + msg = 'An invalid AWS Region was specified.' + self.logger.warning(msg) + raise TypeError(msg) # Initialize topic list self.topics = list() @@ -147,20 +143,12 @@ class NotifySNS(NotifyBase): self.aws_auth_algorithm = 'AWS4-HMAC-SHA256' self.aws_auth_request = 'aws4_request' - if recipients is None: - recipients = [] + # Get our targets + targets = parse_list(targets) - elif isinstance(recipients, six.string_types): - recipients = [x for x in filter(bool, LIST_DELIM.split( - recipients, - ))] - - elif not isinstance(recipients, (set, tuple, list)): - recipients = [] - - # Validate recipients and drop bad ones: - for recipient in recipients: - result = IS_PHONE_NO.match(recipient) + # Validate targets and drop bad ones: + for target in targets: + result = IS_PHONE_NO.match(target) if result: # Further check our phone # for it's digit count # if it's less than 10, then we can assume it's @@ -169,7 +157,7 @@ class NotifySNS(NotifyBase): if len(result) < 11 or len(result) > 14: self.logger.warning( 'Dropped invalid phone # ' - '(%s) specified.' % recipient, + '(%s) specified.' % target, ) continue @@ -177,7 +165,7 @@ class NotifySNS(NotifyBase): self.phone.append('+{}'.format(result)) continue - result = IS_TOPIC.match(recipient) + result = IS_TOPIC.match(target) if result: # store valid topic self.topics.append(result.group('name')) @@ -185,12 +173,12 @@ class NotifySNS(NotifyBase): self.logger.warning( 'Dropped invalid phone/topic ' - '(%s) specified.' % recipient, + '(%s) specified.' % target, ) if len(self.phone) == 0 and len(self.topics) == 0: self.logger.warning( - 'There are no valid recipient identified to notify.') + 'There are no valid target identified to notify.') def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs): """ @@ -278,7 +266,7 @@ class NotifySNS(NotifyBase): self.throttle() # Convert our payload from a dict() into a urlencoded string - payload = self.urlencode(payload) + payload = NotifySNS.urlencode(payload) # Prepare our Notification URL # Prepare our AWS Headers based on our payload @@ -300,7 +288,7 @@ class NotifySNS(NotifyBase): if r.status_code != requests.codes.ok: # We had a problem status_str = \ - NotifyBase.http_response_code_lookup( + NotifySNS.http_response_code_lookup( r.status_code, AWS_HTTP_ERROR_MAP) self.logger.warning( @@ -541,17 +529,18 @@ class NotifySNS(NotifyBase): return '{schema}://{key_id}/{key_secret}/{region}/{targets}/'\ '?{args}'.format( schema=self.secure_protocol, - key_id=self.quote(self.aws_access_key_id, safe=''), - key_secret=self.quote(self.aws_secret_access_key, safe=''), - region=self.quote(self.aws_region_name, safe=''), + key_id=NotifySNS.quote(self.aws_access_key_id, safe=''), + key_secret=NotifySNS.quote( + self.aws_secret_access_key, safe=''), + region=NotifySNS.quote(self.aws_region_name, safe=''), targets='/'.join( - [self.quote(x) for x in chain( + [NotifySNS.quote(x) for x in chain( # Phone # are prefixed with a plus symbol ['+{}'.format(x) for x in self.phone], # Topics are prefixed with a pound/hashtag symbol ['#{}'.format(x) for x in self.topics], )]), - args=self.urlencode(args), + args=NotifySNS.urlencode(args), ) @staticmethod @@ -567,12 +556,8 @@ class NotifySNS(NotifyBase): # We're done early as we couldn't load the results return results - # - # Apply our settings now - # - # The AWS Access Key ID is stored in the hostname - access_key_id = results['host'] + access_key_id = NotifySNS.unquote(results['host']) # Our AWS Access Key Secret contains slashes in it which unfortunately # means it is of variable length after the hostname. Since we require @@ -586,9 +571,12 @@ class NotifySNS(NotifyBase): # accumulated data. secret_access_key_parts = list() + # Start with a list of entries to work with + entries = NotifySNS.split_path(results['fullpath']) + # Section 1: Get Region and Access Secret index = 0 - for i, entry in enumerate(NotifyBase.split_path(results['fullpath'])): + for i, entry in enumerate(entries): # Are we at the region yet? result = IS_REGION.match(entry) @@ -615,9 +603,13 @@ class NotifySNS(NotifyBase): secret_access_key_parts.append(entry) # Section 2: Get our Recipients (basically all remaining entries) - results['recipients'] = [ - NotifyBase.unquote(x) for x in filter(bool, NotifyBase.split_path( - results['fullpath']))][index:] + results['targets'] = entries[index:] + + # Support the 'to' variable so that we can support rooms this way too + # The 'to' makes it easier to use yaml configuration + if 'to' in results['qsd'] and len(results['qsd']['to']): + results['targets'] += \ + NotifySNS.parse_list(results['qsd']['to']) # Store our other detected data (if at all) results['region_name'] = region_name diff --git a/apprise/plugins/NotifySlack.py b/apprise/plugins/NotifySlack.py index 6a321cc4..a7bd7f17 100644 --- a/apprise/plugins/NotifySlack.py +++ b/apprise/plugins/NotifySlack.py @@ -45,6 +45,7 @@ from .NotifyBase import NotifyBase from ..common import NotifyImageSize from ..common import NotifyType from ..common import NotifyFormat +from ..utils import parse_bool # Token required as part of the API request # /AAAAAAAAA/........./........................ @@ -101,41 +102,51 @@ class NotifySlack(NotifyBase): notify_format = NotifyFormat.MARKDOWN - def __init__(self, token_a, token_b, token_c, channels, **kwargs): + def __init__(self, token_a, token_b, token_c, targets, + include_image=True, **kwargs): """ Initialize Slack Object """ super(NotifySlack, self).__init__(**kwargs) + if not token_a: + msg = 'The first API token is not specified.' + self.logger.warning(msg) + raise TypeError(msg) + + if not token_b: + msg = 'The second API token is not specified.' + self.logger.warning(msg) + raise TypeError(msg) + + if not token_c: + msg = 'The third API token is not specified.' + self.logger.warning(msg) + raise TypeError(msg) + if not VALIDATE_TOKEN_A.match(token_a.strip()): - self.logger.warning( - 'The first API Token specified (%s) is invalid.' % token_a, - ) - raise TypeError( - 'The first API Token specified (%s) is invalid.' % token_a, - ) + msg = 'The first API token specified ({}) is invalid.'\ + .format(token_a) + self.logger.warning(msg) + raise TypeError(msg) # The token associated with the account self.token_a = token_a.strip() if not VALIDATE_TOKEN_B.match(token_b.strip()): - self.logger.warning( - 'The second API Token specified (%s) is invalid.' % token_b, - ) - raise TypeError( - 'The second API Token specified (%s) is invalid.' % token_b, - ) + msg = 'The second API token specified ({}) is invalid.'\ + .format(token_b) + self.logger.warning(msg) + raise TypeError(msg) # The token associated with the account self.token_b = token_b.strip() if not VALIDATE_TOKEN_C.match(token_c.strip()): - self.logger.warning( - 'The third API Token specified (%s) is invalid.' % token_c, - ) - raise TypeError( - 'The third API Token specified (%s) is invalid.' % token_c, - ) + msg = 'The third API token specified ({}) is invalid.'\ + .format(token_c) + self.logger.warning(msg) + raise TypeError(msg) # The token associated with the account self.token_c = token_c.strip() @@ -144,20 +155,21 @@ class NotifySlack(NotifyBase): self.logger.warning( 'No user was specified; using %s.' % SLACK_DEFAULT_USER) - if isinstance(channels, six.string_types): + if isinstance(targets, six.string_types): self.channels = [x for x in filter(bool, CHANNEL_LIST_DELIM.split( - channels, + targets, ))] - elif isinstance(channels, (set, tuple, list)): - self.channels = channels + elif isinstance(targets, (set, tuple, list)): + self.channels = targets else: self.channels = list() if len(self.channels) == 0: - self.logger.warning('No channel(s) were specified.') - raise TypeError('No channel(s) were specified.') + msg = 'No channel(s) were specified.' + self.logger.warning(msg) + raise TypeError(msg) # Formatting requirements are defined here: # https://api.slack.com/docs/message-formatting @@ -176,6 +188,9 @@ class NotifySlack(NotifyBase): re.IGNORECASE, ) + # Place a thumbnail image inline with the message body + self.include_image = include_image + def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs): """ Perform Slack Notification @@ -203,8 +218,6 @@ class NotifySlack(NotifyBase): self.token_c, ) - image_url = self.image_url(notify_type) - # Create a copy of the channel list channels = list(self.channels) while len(channels): @@ -247,6 +260,10 @@ class NotifySlack(NotifyBase): }], } + # Acquire our to-be footer icon if configured to do so + image_url = None if not self.include_image \ + else self.image_url(notify_type) + if image_url: payload['attachments'][0]['footer_icon'] = image_url @@ -267,7 +284,7 @@ class NotifySlack(NotifyBase): if r.status_code != requests.codes.ok: # We had a problem status_str = \ - NotifyBase.http_response_code_lookup( + NotifySlack.http_response_code_lookup( r.status_code, SLACK_HTTP_ERROR_MAP) self.logger.warning( @@ -311,25 +328,26 @@ class NotifySlack(NotifyBase): args = { 'format': self.notify_format, 'overflow': self.overflow_mode, + 'image': 'yes' if self.include_image else 'no', } # Determine if there is a botname present botname = '' if self.user: botname = '{botname}@'.format( - botname=self.quote(self.user, safe=''), + botname=NotifySlack.quote(self.user, safe=''), ) return '{schema}://{botname}{token_a}/{token_b}/{token_c}/{targets}/'\ '?{args}'.format( schema=self.secure_protocol, botname=botname, - token_a=self.quote(self.token_a, safe=''), - token_b=self.quote(self.token_b, safe=''), - token_c=self.quote(self.token_c, safe=''), + token_a=NotifySlack.quote(self.token_a, safe=''), + token_b=NotifySlack.quote(self.token_b, safe=''), + token_c=NotifySlack.quote(self.token_c, safe=''), targets='/'.join( - [self.quote(x, safe='') for x in self.channels]), - args=self.urlencode(args), + [NotifySlack.quote(x, safe='') for x in self.channels]), + args=NotifySlack.urlencode(args), ) @staticmethod @@ -345,26 +363,39 @@ class NotifySlack(NotifyBase): # We're done early as we couldn't load the results return results - # Apply our settings now + # Get unquoted entries + entries = NotifySlack.split_path(results['fullpath']) # The first token is stored in the hostname - token_a = results['host'] + results['token_a'] = NotifySlack.unquote(results['host']) # Now fetch the remaining tokens try: - token_b, token_c = [x for x in filter( - bool, NotifyBase.split_path(results['fullpath']))][0:2] + results['token_b'] = entries.pop(0) - except (ValueError, AttributeError, IndexError): + except IndexError: # We're done - return None + results['token_b'] = None - channels = [x for x in filter( - bool, NotifyBase.split_path(results['fullpath']))][2:] + try: + results['token_c'] = entries.pop(0) - results['token_a'] = token_a - results['token_b'] = token_b - results['token_c'] = token_c - results['channels'] = channels + except IndexError: + # We're done + results['token_c'] = None + + # assign remaining entries to the channels we wish to notify + results['targets'] = entries + + # Support the 'to' variable so that we can support rooms this way too + # The 'to' makes it easier to use yaml configuration + if 'to' in results['qsd'] and len(results['qsd']['to']): + results['targets'] += [x for x in filter( + bool, CHANNEL_LIST_DELIM.split( + NotifySlack.unquote(results['qsd']['to'])))] + + # Get Image + results['include_image'] = \ + parse_bool(results['qsd'].get('image', True)) return results diff --git a/apprise/plugins/NotifyTelegram.py b/apprise/plugins/NotifyTelegram.py index cb80d69b..da1b7908 100644 --- a/apprise/plugins/NotifyTelegram.py +++ b/apprise/plugins/NotifyTelegram.py @@ -107,7 +107,7 @@ class NotifyTelegram(NotifyBase): # The maximum allowable characters allowed in the body per message body_maxlen = 4096 - def __init__(self, bot_token, chat_ids, detect_bot_owner=True, + def __init__(self, bot_token, targets, detect_bot_owner=True, include_image=True, **kwargs): """ Initialize Telegram Object @@ -133,19 +133,19 @@ class NotifyTelegram(NotifyBase): self.bot_token = result.group('key') # Parse our list - self.chat_ids = parse_list(chat_ids) + self.targets = parse_list(targets) if self.user: # Treat this as a channel too - self.chat_ids.append(self.user) + self.targets.append(self.user) - if len(self.chat_ids) == 0 and detect_bot_owner: + if len(self.targets) == 0 and detect_bot_owner: _id = self.detect_bot_owner() if _id: # Store our id - self.chat_ids.append(str(_id)) + self.targets.append(str(_id)) - if len(self.chat_ids) == 0: + if len(self.targets) == 0: err = 'No chat_id(s) were specified.' self.logger.warning(err) raise TypeError(err) @@ -168,14 +168,25 @@ class NotifyTelegram(NotifyBase): 'sendPhoto' ) + # Acquire our image path if configured to do so; we don't bother + # checking to see if selfinclude_image is set here because the + # send_image() function itself (this function) checks this flag + # already path = self.image_path(notify_type) + if not path: # No image to send self.logger.debug( 'Telegram Image does not exist for %s' % (notify_type)) - return None - files = {'photo': (basename(path), open(path), 'rb')} + # No need to fail; we may have been configured this way through + # the apprise.AssetObject() + return True + + # Configure file payload (for upload) + files = { + 'photo': (basename(path), open(path), 'rb'), + } payload = { 'chat_id': chat_id, @@ -196,7 +207,7 @@ class NotifyTelegram(NotifyBase): if r.status_code != requests.codes.ok: # We had a problem status_str = \ - NotifyBase.http_response_code_lookup(r.status_code) + NotifyTelegram.http_response_code_lookup(r.status_code) self.logger.warning( 'Failed to send Telegram Image: ' @@ -248,7 +259,7 @@ class NotifyTelegram(NotifyBase): if r.status_code != requests.codes.ok: # We had a problem status_str = \ - NotifyBase.http_response_code_lookup(r.status_code) + NotifyTelegram.http_response_code_lookup(r.status_code) try: # Try to get the error message if we can: @@ -368,10 +379,10 @@ class NotifyTelegram(NotifyBase): title = re.sub(' ?', ' ', title, re.I) # HTML - title = NotifyBase.escape_html(title, whitespace=False) + title = NotifyTelegram.escape_html(title, whitespace=False) # HTML - body = NotifyBase.escape_html(body, whitespace=False) + body = NotifyTelegram.escape_html(body, whitespace=False) if title and self.notify_format == NotifyFormat.TEXT: # Text HTML Formatting @@ -393,9 +404,9 @@ class NotifyTelegram(NotifyBase): payload['text'] = body # Create a copy of the chat_ids list - chat_ids = list(self.chat_ids) - while len(chat_ids): - chat_id = chat_ids.pop(0) + targets = list(self.targets) + while len(targets): + chat_id = targets.pop(0) chat_id = IS_CHAT_ID_RE.match(chat_id) if not chat_id: self.logger.warning( @@ -441,7 +452,7 @@ class NotifyTelegram(NotifyBase): if r.status_code != requests.codes.ok: # We had a problem status_str = \ - NotifyBase.http_response_code_lookup(r.status_code) + NotifyTelegram.http_response_code_lookup(r.status_code) try: # Try to get the error message if we can: @@ -489,16 +500,17 @@ class NotifyTelegram(NotifyBase): args = { 'format': self.notify_format, 'overflow': self.overflow_mode, + 'image': self.include_image, } # 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( schema=self.secure_protocol, - bot_token=self.quote(self.bot_token, safe=''), + bot_token=NotifyTelegram.quote(self.bot_token, safe=''), targets='/'.join( - [self.quote('@{}'.format(x)) for x in self.chat_ids]), - args=self.urlencode(args)) + [NotifyTelegram.quote('@{}'.format(x)) for x in self.targets]), + args=NotifyTelegram.urlencode(args)) @staticmethod def parse_url(url): @@ -507,9 +519,9 @@ class NotifyTelegram(NotifyBase): us to substantiate this object. """ - # This is a dirty hack; but it's the only work around to - # tgram:// messages since the bot_token has a colon in it. - # It invalidates an normal URL. + # This is a dirty hack; but it's the only work around to tgram:// + # messages since the bot_token has a colon in it. It invalidates a + # normal URL. # This hack searches for this bogus URL and corrects it so we can # properly load it further down. The other alternative is to ask users @@ -550,23 +562,28 @@ class NotifyTelegram(NotifyBase): ) # The first token is stored in the hostname - bot_token_a = results['host'] + bot_token_a = NotifyTelegram.unquote(results['host']) + + # Get a nice unquoted list of path entries + entries = NotifyTelegram.split_path(results['fullpath']) # Now fetch the remaining tokens - bot_token_b = [x for x in filter( - bool, NotifyBase.split_path(results['fullpath']))][0] + bot_token_b = entries.pop(0) bot_token = '%s:%s' % (bot_token_a, bot_token_b) - chat_ids = [x for x in filter( - bool, NotifyBase.split_path(results['fullpath']))][1:] + # Store our chat ids (as these are the remaining entries) + results['targets'] = entries + + # Support the 'to' variable so that we can support rooms this way too + # The 'to' makes it easier to use yaml configuration + if 'to' in results['qsd'] and len(results['qsd']['to']): + results['targets'] += \ + NotifyTelegram.parse_list(results['qsd']['to']) # Store our bot token results['bot_token'] = bot_token - # Store our chat ids - results['chat_ids'] = chat_ids - # Include images with our message results['include_image'] = \ parse_bool(results['qsd'].get('image', False)) diff --git a/apprise/plugins/NotifyTwitter/__init__.py b/apprise/plugins/NotifyTwitter/__init__.py index 50275d4e..d2ee30b6 100644 --- a/apprise/plugins/NotifyTwitter/__init__.py +++ b/apprise/plugins/NotifyTwitter/__init__.py @@ -26,6 +26,7 @@ from . import tweepy from ..NotifyBase import NotifyBase from ...common import NotifyType +from ...utils import parse_list class NotifyTwitter(NotifyBase): @@ -54,7 +55,7 @@ class NotifyTwitter(NotifyBase): # Twitter does have titles when creating a message title_maxlen = 0 - def __init__(self, ckey, csecret, akey, asecret, **kwargs): + def __init__(self, ckey, csecret, akey, asecret, targets=None, **kwargs): """ Initialize Twitter Object @@ -62,29 +63,32 @@ class NotifyTwitter(NotifyBase): super(NotifyTwitter, self).__init__(**kwargs) if not ckey: - raise TypeError( - 'An invalid Consumer API Key was specified.' - ) + msg = 'An invalid Consumer API Key was specified.' + self.logger.warning(msg) + raise TypeError(msg) if not csecret: - raise TypeError( - 'An invalid Consumer Secret API Key was specified.' - ) + msg = 'An invalid Consumer Secret API Key was specified.' + self.logger.warning(msg) + raise TypeError(msg) if not akey: - raise TypeError( - 'An invalid Acess Token API Key was specified.' - ) + msg = 'An invalid Access Token API Key was specified.' + self.logger.warning(msg) + raise TypeError(msg) if not asecret: - raise TypeError( - 'An invalid Acess Token Secret API Key was specified.' - ) + msg = 'An invalid Access Token Secret API Key was specified.' + self.logger.warning(msg) + raise TypeError(msg) - if not self.user: - raise TypeError( - 'No user was specified.' - ) + # Identify our targets + self.targets = parse_list(targets) + + if len(self.targets) == 0 and not self.user: + msg = 'No user(s) were specified.' + self.logger.warning(msg) + raise TypeError(msg) # Store our data self.ckey = ckey @@ -113,28 +117,68 @@ class NotifyTwitter(NotifyBase): ) return False - # Always call throttle before any remote server i/o is made to avoid - # thrashing the remote server and risk being blocked. - self.throttle() + # Get ourselves a list of targets + users = list(self.targets) + if not users: + # notify ourselves + users.append(self.user) - try: - # Get our API - api = tweepy.API(self.auth) + # Error Tracking + has_error = False - # Send our Direct Message - api.send_direct_message(self.user, text=body) - self.logger.info('Sent Twitter DM notification.') + while len(users) > 0: + # Get our user + user = users.pop(0) - except Exception as e: - self.logger.warning( - 'A Connection error occured sending Twitter ' - 'direct message to %s.' % self.user) - self.logger.debug('Twitter Exception: %s' % str(e)) + # Always call throttle before any remote server i/o is made to + # avoid thrashing the remote server and risk being blocked. + self.throttle() - # Return; we're done - return False + try: + # Get our API + api = tweepy.API(self.auth) - return True + # Send our Direct Message + api.send_direct_message(user, text=body) + self.logger.info( + 'Sent Twitter DM notification to {}.'.format(user)) + + except Exception as e: + self.logger.warning( + 'A Connection error occured sending Twitter ' + 'direct message to %s.' % user) + self.logger.debug('Twitter Exception: %s' % str(e)) + + # Track our error + has_error = True + + return not has_error + + def url(self): + """ + Returns the URL built dynamically based on specified arguments. + """ + + # Define any arguments set + args = { + 'format': self.notify_format, + 'overflow': self.overflow_mode, + } + + if len(self.targets) > 0: + args['to'] = ','.join([NotifyTwitter.quote(x, safe='') + for x in self.targets]) + + return '{schema}://{auth}{ckey}/{csecret}/{akey}/{asecret}' \ + '/?{args}'.format( + auth='' if not self.user else '{user}@'.format( + user=NotifyTwitter.quote(self.user, safe='')), + schema=self.secure_protocol, + ckey=NotifyTwitter.quote(self.ckey, safe=''), + asecret=NotifyTwitter.quote(self.csecret, safe=''), + akey=NotifyTwitter.quote(self.akey, safe=''), + csecret=NotifyTwitter.quote(self.asecret, safe=''), + args=NotifyTwitter.urlencode(args)) @staticmethod def parse_url(url): @@ -152,13 +196,12 @@ class NotifyTwitter(NotifyBase): # Apply our settings now # The first token is stored in the hostname - consumer_key = results['host'] + consumer_key = NotifyTwitter.unquote(results['host']) # Now fetch the remaining tokens try: consumer_secret, access_token_key, access_token_secret = \ - [x for x in filter(bool, NotifyBase.split_path( - results['fullpath']))][0:3] + NotifyTwitter.split_path(results['fullpath'])[0:3] except (ValueError, AttributeError, IndexError): # Force some bad values that will get caught @@ -172,4 +215,8 @@ class NotifyTwitter(NotifyBase): results['akey'] = access_token_key results['asecret'] = access_token_secret + # Support the to= allowing one to identify more then one user to tweet + # too + results['targets'] = NotifyTwitter.parse_list(results['qsd'].get('to')) + return results diff --git a/apprise/plugins/NotifyWindows.py b/apprise/plugins/NotifyWindows.py index f39a5285..65207a69 100644 --- a/apprise/plugins/NotifyWindows.py +++ b/apprise/plugins/NotifyWindows.py @@ -31,6 +31,7 @@ from time import sleep from .NotifyBase import NotifyBase from ..common import NotifyImageSize from ..common import NotifyType +from ..utils import parse_bool # Default our global support flag NOTIFY_WINDOWS_SUPPORT_ENABLED = False @@ -75,6 +76,9 @@ class NotifyWindows(NotifyBase): # content to display body_max_line_count = 2 + # The number of seconds to display the popup for + default_popup_duration_sec = 12 + # This entry is a bit hacky, but it allows us to unit-test this library # in an environment that simply doesn't have the windows packages # available to us. It also allows us to handle situations where the @@ -84,18 +88,23 @@ class NotifyWindows(NotifyBase): # let me know! :) _enabled = NOTIFY_WINDOWS_SUPPORT_ENABLED - def __init__(self, **kwargs): + def __init__(self, include_image=True, duration=None, **kwargs): """ Initialize Windows Object """ + super(NotifyWindows, self).__init__(**kwargs) + # Number of seconds to display notification for - self.duration = 12 + self.duration = self.default_popup_duration_sec \ + if not (isinstance(duration, int) and duration > 0) else duration # Define our handler self.hwnd = None - super(NotifyWindows, self).__init__(**kwargs) + # Track whether or not we want to send an image with our notification + # or not. + self.include_image = include_image def _on_destroy(self, hwnd, msg, wparam, lparam): """ @@ -140,20 +149,26 @@ class NotifyWindows(NotifyBase): self.hinst, None) win32gui.UpdateWindow(self.hwnd) - # image path - icon_path = self.image_path(notify_type, extension='.ico') - icon_flags = win32con.LR_LOADFROMFILE | win32con.LR_DEFAULTSIZE + # image path (if configured to acquire) + icon_path = None if not self.include_image \ + else self.image_path(notify_type, extension='.ico') - try: - hicon = win32gui.LoadImage( - self.hinst, icon_path, win32con.IMAGE_ICON, 0, 0, - icon_flags) + if icon_path: + icon_flags = win32con.LR_LOADFROMFILE | win32con.LR_DEFAULTSIZE - except Exception as e: - self.logger.warning( - "Could not load windows notification icon ({}): {}" - .format(icon_path, e)) + try: + hicon = win32gui.LoadImage( + self.hinst, icon_path, win32con.IMAGE_ICON, 0, 0, + icon_flags) + except Exception as e: + self.logger.warning( + "Could not load windows notification icon ({}): {}" + .format(icon_path, e)) + + # disable icon + hicon = win32gui.LoadIcon(0, win32con.IDI_APPLICATION) + else: # disable icon hicon = win32gui.LoadIcon(0, win32con.IDI_APPLICATION) @@ -185,7 +200,18 @@ class NotifyWindows(NotifyBase): Returns the URL built dynamically based on specified arguments. """ - return '{schema}://'.format(schema=self.protocol) + # Define any arguments set + args = { + 'format': self.notify_format, + 'overflow': self.overflow_mode, + 'image': 'yes' if self.include_image else 'no', + 'duration': str(self.duration), + } + + return '{schema}://_/?{args}'.format( + schema=self.protocol, + args=NotifyWindows.urlencode(args), + ) @staticmethod def parse_url(url): @@ -196,15 +222,31 @@ class NotifyWindows(NotifyBase): """ - # return a very basic set of requirements - return { - 'schema': NotifyWindows.protocol, - 'user': None, - 'password': None, - 'port': None, - 'host': 'localhost', - 'fullpath': None, - 'path': None, - 'url': url, - 'qsd': {}, - } + 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': {}, + } + + # Include images with our message + results['include_image'] = \ + parse_bool(results['qsd'].get('image', True)) + + # Set duration + try: + results['duration'] = int(results['qsd'].get('duration')) + + except (TypeError, ValueError): + # Not a valid integer; ignore entry + pass + + # return results + return results diff --git a/apprise/plugins/NotifyXBMC.py b/apprise/plugins/NotifyXBMC.py index beb2853a..ccbb9c68 100644 --- a/apprise/plugins/NotifyXBMC.py +++ b/apprise/plugins/NotifyXBMC.py @@ -29,6 +29,7 @@ from json import dumps from .NotifyBase import NotifyBase from ..common import NotifyType from ..common import NotifyImageSize +from ..utils import parse_bool class NotifyXBMC(NotifyBase): @@ -70,26 +71,27 @@ 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 # KODI default protocol version (v6) kodi_remote_protocol = 6 - def __init__(self, **kwargs): + def __init__(self, include_image=True, duration=None, **kwargs): """ Initialize XBMC/KODI Object """ super(NotifyXBMC, self).__init__(**kwargs) - # Number of micro-seconds to display notification for - self.duration = 12000 + # Number of seconds to display notification for + self.duration = self.default_popup_duration_sec \ + if not (isinstance(duration, int) and duration > 0) else duration - if self.secure: - self.schema = 'https' - - else: - self.schema = 'http' + # Build our schema + self.schema = 'https' if self.secure else 'http' # Prepare the default header self.headers = { @@ -100,6 +102,10 @@ class NotifyXBMC(NotifyBase): # Default protocol self.protocol = kwargs.get('protocol', self.xbmc_remote_protocol) + # Track whether or not we want to send an image with our notification + # or not. + self.include_image = include_image + def _payload_60(self, title, body, notify_type, **kwargs): """ Builds payload for KODI API v6.0 @@ -114,13 +120,17 @@ class NotifyXBMC(NotifyBase): 'params': { 'title': title, 'message': body, - # displaytime is defined in microseconds - 'displaytime': self.duration, + # displaytime is defined in microseconds so we need to just + # do some simple math + 'displaytime': int(self.duration * 1000), }, 'id': 1, } - image_url = self.image_url(notify_type) + # Acquire our image url if configured to do so + image_url = None if not self.include_image else \ + self.image_url(notify_type) + if image_url: payload['params']['image'] = image_url if notify_type is NotifyType.FAILURE: @@ -148,13 +158,17 @@ class NotifyXBMC(NotifyBase): 'params': { 'title': title, 'message': body, - # displaytime is defined in microseconds - 'displaytime': self.duration, + # displaytime is defined in microseconds so we need to just + # do some simple math + 'displaytime': int(self.duration * 1000), }, 'id': 1, } - image_url = self.image_url(notify_type) + # Include our logo if configured to do so + image_url = None if not self.include_image \ + else self.image_url(notify_type) + if image_url: payload['params']['image'] = image_url @@ -204,7 +218,7 @@ class NotifyXBMC(NotifyBase): if r.status_code != requests.codes.ok: # We had a problem status_str = \ - NotifyBase.http_response_code_lookup(r.status_code) + NotifyXBMC.http_response_code_lookup(r.status_code) self.logger.warning( 'Failed to send XBMC/KODI notification: ' @@ -242,18 +256,20 @@ class NotifyXBMC(NotifyBase): args = { 'format': self.notify_format, 'overflow': self.overflow_mode, + 'image': 'yes' if self.include_image else 'no', + 'duration': str(self.duration), } # Determine Authentication auth = '' if self.user and self.password: auth = '{user}:{password}@'.format( - user=self.quote(self.user, safe=''), - password=self.quote(self.password, safe=''), + user=NotifyXBMC.quote(self.user, safe=''), + password=NotifyXBMC.quote(self.password, safe=''), ) elif self.user: auth = '{user}@'.format( - user=self.quote(self.user, safe=''), + user=NotifyXBMC.quote(self.user, safe=''), ) default_schema = self.xbmc_protocol if ( @@ -266,10 +282,10 @@ class NotifyXBMC(NotifyBase): return '{schema}://{auth}{hostname}{port}/?{args}'.format( schema=default_schema, auth=auth, - hostname=self.host, + hostname=NotifyXBMC.quote(self.host, safe=''), port='' if not self.port or self.port == default_port else ':{}'.format(self.port), - args=self.urlencode(args), + args=NotifyXBMC.urlencode(args), ) @staticmethod @@ -298,4 +314,16 @@ class NotifyXBMC(NotifyBase): # KODI Support results['protocol'] = NotifyXBMC.kodi_remote_protocol + # Include images with our message + results['include_image'] = \ + parse_bool(results['qsd'].get('image', True)) + + # Set duration + try: + results['duration'] = abs(int(results['qsd'].get('duration'))) + + except (TypeError, ValueError): + # Not a valid integer; ignore entry + pass + return results diff --git a/apprise/plugins/NotifyXML.py b/apprise/plugins/NotifyXML.py index 41c88e2c..e5f41d07 100644 --- a/apprise/plugins/NotifyXML.py +++ b/apprise/plugins/NotifyXML.py @@ -81,12 +81,6 @@ class NotifyXML(NotifyBase): """ - if self.secure: - self.schema = 'https' - - else: - self.schema = 'http' - self.fullpath = kwargs.get('fullpath') if not isinstance(self.fullpath, six.string_types): self.fullpath = '/' @@ -116,12 +110,12 @@ class NotifyXML(NotifyBase): auth = '' if self.user and self.password: auth = '{user}:{password}@'.format( - user=self.quote(self.user, safe=''), - password=self.quote(self.password, safe=''), + user=NotifyXML.quote(self.user, safe=''), + password=NotifyXML.quote(self.password, safe=''), ) elif self.user: auth = '{user}@'.format( - user=self.quote(self.user, safe=''), + user=NotifyXML.quote(self.user, safe=''), ) default_port = 443 if self.secure else 80 @@ -129,10 +123,10 @@ class NotifyXML(NotifyBase): return '{schema}://{auth}{hostname}{port}/?{args}'.format( schema=self.secure_protocol if self.secure else self.protocol, auth=auth, - hostname=self.host, + hostname=NotifyXML.quote(self.host, safe=''), port='' if self.port is None or self.port == default_port else ':{}'.format(self.port), - args=self.urlencode(args), + args=NotifyXML.urlencode(args), ) def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs): @@ -150,9 +144,10 @@ class NotifyXML(NotifyBase): headers.update(self.headers) re_map = { - '{MESSAGE_TYPE}': NotifyBase.quote(notify_type), - '{SUBJECT}': NotifyBase.quote(title), - '{MESSAGE}': NotifyBase.quote(body), + '{MESSAGE_TYPE}': NotifyXML.escape_html( + notify_type, whitespace=False), + '{SUBJECT}': NotifyXML.escape_html(title, whitespace=False), + '{MESSAGE}': NotifyXML.escape_html(body, whitespace=False), } # Iterate over above list and store content accordingly @@ -165,7 +160,10 @@ class NotifyXML(NotifyBase): if self.user: auth = (self.user, self.password) - url = '%s://%s' % (self.schema, self.host) + # Set our schema + schema = 'https' if self.secure else 'http' + + url = '%s://%s' % (schema, self.host) if isinstance(self.port, int): url += ':%d' % self.port @@ -191,7 +189,7 @@ class NotifyXML(NotifyBase): if r.status_code != requests.codes.ok: # We had a problem status_str = \ - NotifyBase.http_response_code_lookup(r.status_code) + NotifyXML.http_response_code_lookup(r.status_code) self.logger.warning( 'Failed to send XML notification: ' @@ -237,4 +235,8 @@ class NotifyXML(NotifyBase): results['headers'] = results['qsd-'] results['headers'].update(results['qsd+']) + # Tidy our header entries by unquoting them + results['headers'] = {NotifyXML.unquote(x): NotifyXML.unquote(y) + for x, y in results['headers'].items()} + return results diff --git a/apprise/plugins/NotifyXMPP.py b/apprise/plugins/NotifyXMPP.py index de3ef224..3aa97c79 100644 --- a/apprise/plugins/NotifyXMPP.py +++ b/apprise/plugins/NotifyXMPP.py @@ -24,7 +24,6 @@ # THE SOFTWARE. import re -import six import ssl from os.path import isfile @@ -99,7 +98,7 @@ class NotifyXMPP(NotifyBase): # let me know! :) _enabled = NOTIFY_XMPP_SUPPORT_ENABLED - def __init__(self, targets=None, jid=None, xep=None, to=None, **kwargs): + def __init__(self, targets=None, jid=None, xep=None, **kwargs): """ Initialize XMPP Object """ @@ -177,17 +176,6 @@ class NotifyXMPP(NotifyBase): else: self.targets = list() - if isinstance(to, six.string_types): - # supporting to= makes yaml configuration easier since the user - # just has to identify each user one after another. This is just - # an optional extension to also make the url easier to read if - # some wish to use it. - - # the to is presumed to be the targets JID - self.targets.append(to) - - return - def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs): """ Perform XMPP Notification @@ -302,15 +290,16 @@ class NotifyXMPP(NotifyBase): } if self.jid: - args['jid'] = self.quote(self.jid, safe='') + args['jid'] = self.jid if self.xep: - args['xep'] = self.quote( - ','.join([str(xep) for xep in self.xep]), safe='') + # 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]) # Target JID(s) can clash with our existing paths, so we just use comma - # and/or space as a delimiters - jids = self.quote(' '.join(self.targets), safe='') + # and/or space as a delimiters - %20 = space + jids = '%20'.join([NotifyXMPP.quote(x, safe='') for x in self.targets]) default_port = self.default_secure_port \ if self.secure else self.default_unsecure_port @@ -318,19 +307,21 @@ class NotifyXMPP(NotifyBase): default_schema = self.secure_protocol if self.secure else self.protocol if self.user and self.password: - auth = '{}:{}'.format(self.user, self.password) + auth = '{}:{}'.format( + NotifyXMPP.quote(self.user, safe=''), + NotifyXMPP.quote(self.password, safe='')) else: auth = self.password if self.password else self.user return '{schema}://{auth}@{hostname}{port}/{jids}?{args}'.format( - auth=self.quote(auth, safe=''), + auth=auth, schema=default_schema, - hostname=self.host, + hostname=NotifyXMPP.quote(self.host, safe=''), port='' if not self.port or self.port == default_port else ':{}'.format(self.port), jids=jids, - args=self.urlencode(args), + args=NotifyXMPP.urlencode(args), ) @staticmethod @@ -348,18 +339,20 @@ class NotifyXMPP(NotifyBase): # Get our targets; we ignore path slashes since they identify # our resources - results['targets'] = parse_list(results['fullpath']) + results['targets'] = NotifyXMPP.parse_list(results['fullpath']) # Over-ride the xep plugins if 'xep' in results['qsd'] and len(results['qsd']['xep']): - results['xep'] = parse_list(results['qsd']['xep']) + results['xep'] = \ + NotifyXMPP.parse_list(results['qsd']['xep']) # Over-ride the default (and detected) jid if 'jid' in results['qsd'] and len(results['qsd']['jid']): - results['jid'] = results['qsd']['jid'] + results['jid'] = NotifyXMPP.unquote(results['qsd']['jid']) # Over-ride the default (and detected) jid if 'to' in results['qsd'] and len(results['qsd']['to']): - results['to'] = results['qsd']['to'] + results['targets'] += \ + NotifyXMPP.parse_list(results['qsd']['to']) return results diff --git a/test/test_gitter_plugin.py b/test/test_gitter_plugin.py index 4bfd9d32..bb0594d1 100644 --- a/test/test_gitter_plugin.py +++ b/test/test_gitter_plugin.py @@ -101,6 +101,7 @@ def test_notify_gitter_plugin_general(mock_post, mock_get): obj = plugins.NotifyGitter(token=token, targets='apprise') assert isinstance(obj, plugins.NotifyGitter) is True assert isinstance(obj.url(), six.string_types) is True + # apprise room was found assert obj.send(body="test") is True diff --git a/test/test_glib_plugin.py b/test/test_glib_plugin.py index 4d6ce39d..1f86cdc2 100644 --- a/test/test_glib_plugin.py +++ b/test/test_glib_plugin.py @@ -176,6 +176,87 @@ def test_dbus_plugin(mock_mainloop, mock_byte, mock_bytearray, assert(obj.notify(title='', body='body', notify_type=apprise.NotifyType.INFO) is True) + # Test our arguments through the instantiate call + obj = apprise.Apprise.instantiate( + 'dbus://_/?image=True', suppress_exceptions=False) + assert(isinstance(obj, apprise.plugins.NotifyDBus) is True) + assert(isinstance(obj.url(), six.string_types) is True) + assert(obj.notify(title='title', body='body', + notify_type=apprise.NotifyType.INFO) is True) + + obj = apprise.Apprise.instantiate( + 'dbus://_/?image=False', suppress_exceptions=False) + assert(isinstance(obj, apprise.plugins.NotifyDBus) is True) + assert(isinstance(obj.url(), six.string_types) is True) + assert(obj.notify(title='title', body='body', + notify_type=apprise.NotifyType.INFO) is True) + + # Test priority (alias to urgency) handling + obj = apprise.Apprise.instantiate( + 'dbus://_/?priority=invalid', suppress_exceptions=False) + assert(isinstance(obj, apprise.plugins.NotifyDBus) is True) + assert(isinstance(obj.url(), six.string_types) is True) + assert(obj.notify(title='title', body='body', + notify_type=apprise.NotifyType.INFO) is True) + + obj = apprise.Apprise.instantiate( + 'dbus://_/?priority=high', suppress_exceptions=False) + assert(isinstance(obj, apprise.plugins.NotifyDBus) is True) + assert(isinstance(obj.url(), six.string_types) is True) + assert(obj.notify(title='title', body='body', + notify_type=apprise.NotifyType.INFO) is True) + + obj = apprise.Apprise.instantiate( + 'dbus://_/?priority=2', suppress_exceptions=False) + assert(isinstance(obj, apprise.plugins.NotifyDBus) is True) + assert(isinstance(obj.url(), six.string_types) is True) + assert(obj.notify(title='title', body='body', + notify_type=apprise.NotifyType.INFO) is True) + + # Test urgency handling + obj = apprise.Apprise.instantiate( + 'dbus://_/?urgency=invalid', suppress_exceptions=False) + assert(isinstance(obj, apprise.plugins.NotifyDBus) is True) + assert(isinstance(obj.url(), six.string_types) is True) + assert(obj.notify(title='title', body='body', + notify_type=apprise.NotifyType.INFO) is True) + + obj = apprise.Apprise.instantiate( + 'dbus://_/?urgency=high', suppress_exceptions=False) + assert(isinstance(obj, apprise.plugins.NotifyDBus) is True) + assert(isinstance(obj.url(), six.string_types) is True) + assert(obj.notify(title='title', body='body', + notify_type=apprise.NotifyType.INFO) is True) + + obj = apprise.Apprise.instantiate( + 'dbus://_/?urgency=2', suppress_exceptions=False) + assert(isinstance(obj, apprise.plugins.NotifyDBus) is True) + assert(isinstance(obj.url(), six.string_types) is True) + assert(obj.notify(title='title', body='body', + notify_type=apprise.NotifyType.INFO) is True) + + obj = apprise.Apprise.instantiate( + 'dbus://_/?urgency=', suppress_exceptions=False) + assert(isinstance(obj, apprise.plugins.NotifyDBus) is True) + assert(isinstance(obj.url(), six.string_types) is True) + assert(obj.notify(title='title', body='body', + notify_type=apprise.NotifyType.INFO) is True) + + # Test x/y + obj = apprise.Apprise.instantiate( + 'dbus://_/?x=5&y=5', suppress_exceptions=False) + assert(isinstance(obj, apprise.plugins.NotifyDBus) is True) + assert(isinstance(obj.url(), six.string_types) is True) + assert(obj.notify(title='title', body='body', + notify_type=apprise.NotifyType.INFO) is True) + + obj = apprise.Apprise.instantiate( + 'dbus://_/?x=invalid&y=invalid', suppress_exceptions=False) + assert(isinstance(obj, apprise.plugins.NotifyDBus) is True) + assert(isinstance(obj.url(), six.string_types) is True) + assert(obj.notify(title='title', body='body', + notify_type=apprise.NotifyType.INFO) is True) + # If our underlining object throws for whatever reason, we will # gracefully fail mock_notify = mock.Mock() diff --git a/test/test_gnome_plugin.py b/test/test_gnome_plugin.py index 2debeab8..ddb58ec8 100644 --- a/test/test_gnome_plugin.py +++ b/test/test_gnome_plugin.py @@ -116,24 +116,80 @@ def test_gnome_plugin(): obj.duration = 0 # Check that it found our mocked environments - assert(obj._enabled is True) + assert obj._enabled is True # Test url() call - assert(isinstance(obj.url(), six.string_types) is True) + assert isinstance(obj.url(), six.string_types) is True # test notifications - assert(obj.notify(title='title', body='body', - notify_type=apprise.NotifyType.INFO) is True) + assert obj.notify(title='title', body='body', + notify_type=apprise.NotifyType.INFO) is True # test notification without a title - assert(obj.notify(title='', body='body', - notify_type=apprise.NotifyType.INFO) is True) + assert obj.notify(title='', body='body', + notify_type=apprise.NotifyType.INFO) is True + + obj = apprise.Apprise.instantiate( + 'gnome://_/?image=True', suppress_exceptions=False) + assert isinstance(obj, apprise.plugins.NotifyGnome) is True + assert obj.notify(title='title', body='body', + notify_type=apprise.NotifyType.INFO) is True + + obj = apprise.Apprise.instantiate( + 'gnome://_/?image=False', suppress_exceptions=False) + assert isinstance(obj, apprise.plugins.NotifyGnome) is True + assert obj.notify(title='title', body='body', + notify_type=apprise.NotifyType.INFO) is True + + # Test Priority (alias of urgency) + obj = apprise.Apprise.instantiate( + 'gnome://_/?priority=invalid', suppress_exceptions=False) + assert isinstance(obj, apprise.plugins.NotifyGnome) is True + assert obj.urgency == 1 + assert obj.notify(title='title', body='body', + notify_type=apprise.NotifyType.INFO) is True + + obj = apprise.Apprise.instantiate( + 'gnome://_/?priority=high', suppress_exceptions=False) + assert isinstance(obj, apprise.plugins.NotifyGnome) is True + assert obj.urgency == 2 + assert obj.notify(title='title', body='body', + notify_type=apprise.NotifyType.INFO) is True + + obj = apprise.Apprise.instantiate( + 'gnome://_/?priority=2', suppress_exceptions=False) + assert isinstance(obj, apprise.plugins.NotifyGnome) is True + assert obj.urgency == 2 + assert obj.notify(title='title', body='body', + notify_type=apprise.NotifyType.INFO) is True + + # Test Urgeny + obj = apprise.Apprise.instantiate( + 'gnome://_/?urgency=invalid', suppress_exceptions=False) + assert obj.urgency == 1 + assert isinstance(obj, apprise.plugins.NotifyGnome) is True + assert obj.notify(title='title', body='body', + notify_type=apprise.NotifyType.INFO) is True + + obj = apprise.Apprise.instantiate( + 'gnome://_/?urgency=high', suppress_exceptions=False) + assert obj.urgency == 2 + assert isinstance(obj, apprise.plugins.NotifyGnome) is True + assert obj.notify(title='title', body='body', + notify_type=apprise.NotifyType.INFO) is True + + obj = apprise.Apprise.instantiate( + 'gnome://_/?urgency=2', suppress_exceptions=False) + assert isinstance(obj, apprise.plugins.NotifyGnome) is True + assert obj.urgency == 2 + assert obj.notify(title='title', body='body', + notify_type=apprise.NotifyType.INFO) is True # Test our loading of our icon exception; it will still allow the # notification to be sent mock_pixbuf.new_from_file.side_effect = AttributeError() - assert(obj.notify(title='title', body='body', - notify_type=apprise.NotifyType.INFO) is True) + assert obj.notify(title='title', body='body', + notify_type=apprise.NotifyType.INFO) is True # Undo our change mock_pixbuf.new_from_file.side_effect = None @@ -142,8 +198,8 @@ def test_gnome_plugin(): .Notification.new.return_value = None sys.modules['gi.repository.Notify']\ .Notification.new.side_effect = AttributeError() - assert(obj.notify(title='title', body='body', - notify_type=apprise.NotifyType.INFO) is False) + assert obj.notify(title='title', body='body', + notify_type=apprise.NotifyType.INFO) is False # Undo our change sys.modules['gi.repository.Notify']\ @@ -152,11 +208,11 @@ def test_gnome_plugin(): # Toggle our testing for when we can't send notifications because the # package has been made unavailable to us obj._enabled = False - assert(obj.notify(title='title', body='body', - notify_type=apprise.NotifyType.INFO) is False) + assert obj.notify(title='title', body='body', + notify_type=apprise.NotifyType.INFO) is False - # Test the setting of a the urgency - apprise.plugins.NotifyGnome(urgency=0) + # Test the setting of a the urgency (through priority keyword) + apprise.plugins.NotifyGnome(priority=0) # Verify this all works in the event a ValueError is also thronw # out of the call to gi.require_version() @@ -178,10 +234,10 @@ def test_gnome_plugin(): # Create our instance obj = apprise.Apprise.instantiate('gnome://', suppress_exceptions=False) - assert(isinstance(obj, apprise.plugins.NotifyGnome) is True) + assert isinstance(obj, apprise.plugins.NotifyGnome) is True obj.duration = 0 # Our notifications can not work without our gi library having been # loaded. - assert(obj.notify(title='title', body='body', - notify_type=apprise.NotifyType.INFO) is False) + assert obj.notify(title='title', body='body', + notify_type=apprise.NotifyType.INFO) is False diff --git a/test/test_matrix_plugin.py b/test/test_matrix_plugin.py index d3be340a..7e3e646f 100644 --- a/test/test_matrix_plugin.py +++ b/test/test_matrix_plugin.py @@ -61,25 +61,25 @@ def test_notify_matrix_plugin_general(mock_post, mock_get): mock_post.return_value = request # Variation Initializations - obj = plugins.NotifyMatrix(rooms='#abcd') + obj = plugins.NotifyMatrix(targets='#abcd') assert isinstance(obj, plugins.NotifyMatrix) is True assert isinstance(obj.url(), six.string_types) is True # Registration successful assert obj.send(body="test") is True - obj = plugins.NotifyMatrix(user='user', rooms='#abcd') + obj = plugins.NotifyMatrix(user='user', targets='#abcd') assert isinstance(obj, plugins.NotifyMatrix) is True assert isinstance(obj.url(), six.string_types) is True # Registration successful assert obj.send(body="test") is True - obj = plugins.NotifyMatrix(password='passwd', rooms='#abcd') + obj = plugins.NotifyMatrix(password='passwd', targets='#abcd') assert isinstance(obj, plugins.NotifyMatrix) is True assert isinstance(obj.url(), six.string_types) is True # A username gets automatically generated in these cases assert obj.send(body="test") is True - obj = plugins.NotifyMatrix(user='user', password='passwd', rooms='#abcd') + obj = plugins.NotifyMatrix(user='user', password='passwd', targets='#abcd') assert isinstance(obj.url(), six.string_types) is True assert isinstance(obj, plugins.NotifyMatrix) is True # Registration Successful @@ -94,17 +94,17 @@ def test_notify_matrix_plugin_general(mock_post, mock_get): # Fails because we couldn't register because of 404 errors assert obj.send(body="test") is False - obj = plugins.NotifyMatrix(user='test', rooms='#abcd') + obj = plugins.NotifyMatrix(user='test', targets='#abcd') assert isinstance(obj, plugins.NotifyMatrix) is True # Fails because we still couldn't register assert obj.send(user='test', password='passwd', body="test") is False - obj = plugins.NotifyMatrix(user='test', password='passwd', rooms='#abcd') + obj = plugins.NotifyMatrix(user='test', password='passwd', targets='#abcd') assert isinstance(obj, plugins.NotifyMatrix) is True # Fails because we still couldn't register assert obj.send(body="test") is False - obj = plugins.NotifyMatrix(password='passwd', rooms='#abcd') + obj = plugins.NotifyMatrix(password='passwd', targets='#abcd') # Fails because we still couldn't register assert isinstance(obj, plugins.NotifyMatrix) is True assert obj.send(body="test") is False @@ -132,7 +132,7 @@ def test_notify_matrix_plugin_general(mock_post, mock_get): request.content = dumps(response_obj) request.status_code = requests.codes.ok - obj = plugins.NotifyMatrix(rooms=None) + obj = plugins.NotifyMatrix(targets=None) assert isinstance(obj, plugins.NotifyMatrix) is True # Force a empty joined list response @@ -191,7 +191,8 @@ def test_notify_matrix_plugin_fetch(mock_post, mock_get): mock_get.side_effect = fetch_failed mock_post.side_effect = fetch_failed - obj = plugins.NotifyMatrix(user='user', password='passwd', thumbnail=True) + obj = plugins.NotifyMatrix( + user='user', password='passwd', include_image=True) assert isinstance(obj, plugins.NotifyMatrix) is True # We would hve failed to send our image notification assert obj.send(user='test', password='passwd', body="test") is False @@ -518,3 +519,23 @@ def test_notify_matrix_plugin_rooms(mock_post, mock_get): request.status_code = 403 obj._room_cache = {} assert obj._room_id('#abc123:localhost') is None + + +def test_notify_matrix_url_parsing(): + """ + API: NotifyMatrix() URL Testing + + """ + result = plugins.NotifyMatrix.parse_url( + 'matrix://user:token@localhost?to=#room') + assert isinstance(result, dict) is True + assert len(result['targets']) == 1 + assert '#room' in result['targets'] + + result = plugins.NotifyMatrix.parse_url( + 'matrix://user:token@localhost?to=#room1,#room2,#room3') + assert isinstance(result, dict) is True + assert len(result['targets']) == 3 + assert '#room1' in result['targets'] + assert '#room2' in result['targets'] + assert '#room3' in result['targets'] diff --git a/test/test_notify_base.py b/test/test_notify_base.py index 79b97e7e..d483e070 100644 --- a/test/test_notify_base.py +++ b/test/test_notify_base.py @@ -194,13 +194,54 @@ def test_notify_base(): "'\t \n", convert_new_lines=True) == \ '<content>'  <br/></content>' + # Test invalid data + assert NotifyBase.split_path(None) == [] + assert NotifyBase.split_path(object()) == [] + assert NotifyBase.split_path(42) == [] + assert NotifyBase.split_path( '/path/?name=Dr%20Disrespect', unquote=False) == \ ['path', '?name=Dr%20Disrespect'] assert NotifyBase.split_path( '/path/?name=Dr%20Disrespect', unquote=True) == \ - ['path', '?name=Dr', 'Disrespect'] + ['path', '?name=Dr Disrespect'] + + # a slash found inside the path, if escaped properly will not be broken + # by split_path while additional concatinated slashes are ignored + # FYI: %2F = / + assert NotifyBase.split_path( + '/%2F///%2F%2F////%2F%2F%2F////', unquote=True) == \ + ['/', '//', '///'] + + # Test invalid data + assert NotifyBase.parse_list(None) == [] + assert NotifyBase.parse_list(42) == ['42', ] + + result = NotifyBase.parse_list( + ',path,?name=Dr%20Disrespect', unquote=False) + assert isinstance(result, list) is True + assert len(result) == 2 + assert 'path' in result + assert '?name=Dr%20Disrespect' in result + + result = NotifyBase.parse_list(',path,?name=Dr%20Disrespect', unquote=True) + assert isinstance(result, list) is True + assert len(result) == 2 + assert 'path' in result + assert '?name=Dr Disrespect' in result + + # by parse_list while additional concatinated slashes are ignored + # FYI: %2F = / + # In this lit there are actually 4 entries, however parse_list + # eliminates duplicates in addition to unquoting content by default + result = NotifyBase.parse_list( + ',%2F,%2F%2F, , , ,%2F%2F%2F, %2F', unquote=True) + assert isinstance(result, list) is True + assert len(result) == 3 + assert '/' in result + assert '//' in result + assert '///' in result # Give nothing, get nothing assert NotifyBase.escape_html("") == "" diff --git a/test/test_pushjet_plugin.py b/test/test_pushjet_plugin.py index f21ef98c..4d2e3082 100644 --- a/test/test_pushjet_plugin.py +++ b/test/test_pushjet_plugin.py @@ -45,9 +45,12 @@ TEST_URLS = ( ('pjets://', { 'instance': None, }), + ('pjet://:@/', { + 'instance': None, + }), # You must specify a username ('pjet://%s' % ('a' * 32), { - 'instance': None, + 'instance': TypeError, }), # Specify your own server ('pjet://%s@localhost' % ('a' * 32), { @@ -57,9 +60,6 @@ TEST_URLS = ( ('pjets://%s@localhost:8080' % ('a' * 32), { 'instance': plugins.NotifyPushjet, }), - ('pjet://:@/', { - 'instance': None, - }), ('pjet://%s@localhost:8081' % ('a' * 32), { 'instance': plugins.NotifyPushjet, # Throws a series of connection and transfer exceptions when this flag diff --git a/test/test_rest_plugins.py b/test/test_rest_plugins.py index 652c89af..f63034cc 100644 --- a/test/test_rest_plugins.py +++ b/test/test_rest_plugins.py @@ -64,9 +64,13 @@ TEST_URLS = ( ('boxcar://', { 'instance': None, }), + # A a bad url + ('boxcar://:@/', { + 'instance': TypeError, + }), # No secret specified ('boxcar://%s' % ('a' * 64), { - 'instance': None, + 'instance': TypeError, }), # An invalid access and secret key specified ('boxcar://access.key/secret.key/', { @@ -79,15 +83,19 @@ TEST_URLS = ( 'requests_response_code': requests.codes.created, }), # Test without image set - ('boxcar://%s/%s' % ('a' * 64, 'b' * 64), { + ('boxcar://%s/%s?image=True' % ('a' * 64, 'b' * 64), { 'instance': plugins.NotifyBoxcar, 'requests_response_code': requests.codes.created, - # don't include an image by default + # don't include an image in Asset by default 'include_image': False, }), + ('boxcar://%s/%s?image=False' % ('a' * 64, 'b' * 64), { + 'instance': plugins.NotifyBoxcar, + 'requests_response_code': requests.codes.created, + }), # our access, secret and device are all 64 characters # which is what we're doing here - ('boxcar://%s/%s/@tag1/tag2///%s/' % ( + ('boxcar://%s/%s/@tag1/tag2///%s/?to=tag3' % ( 'a' * 64, 'b' * 64, 'd' * 64), { 'instance': plugins.NotifyBoxcar, 'requests_response_code': requests.codes.created, @@ -97,9 +105,6 @@ TEST_URLS = ( 'instance': plugins.NotifyBoxcar, 'requests_response_code': requests.codes.created, }), - ('boxcar://:@/', { - 'instance': None, - }), ('boxcar://%s/%s/' % ('a' * 64, 'b' * 64), { 'instance': plugins.NotifyBoxcar, # force a failure @@ -139,19 +144,37 @@ TEST_URLS = ( 'instance': plugins.NotifyDiscord, 'requests_response_code': requests.codes.no_content, }), + # Enable other options + + # DEPRICATED reference to Thumbnail ('discord://%s/%s?format=markdown&footer=Yes&thumbnail=Yes' % ( 'i' * 24, 't' * 64), { 'instance': plugins.NotifyDiscord, 'requests_response_code': requests.codes.no_content, }), - ('discord://%s/%s?format=markdown&footer=Yes&thumbnail=Yes' % ( + ('discord://%s/%s?format=markdown&footer=Yes&thumbnail=No' % ( + 'i' * 24, 't' * 64), { + 'instance': plugins.NotifyDiscord, + 'requests_response_code': requests.codes.no_content, + }), + + # thumbnail= is depricated and image= is the proper entry + ('discord://%s/%s?format=markdown&footer=Yes&image=Yes' % ( 'i' * 24, 't' * 64), { 'instance': plugins.NotifyDiscord, 'requests_response_code': requests.codes.no_content, # don't include an image by default 'include_image': False, }), + ('discord://%s/%s?format=markdown&footer=Yes&image=No' % ( + 'i' * 24, 't' * 64), { + 'instance': plugins.NotifyDiscord, + 'requests_response_code': requests.codes.no_content, + # don't include an image by default + 'include_image': True, + }), + ('discord://%s/%s?format=markdown&avatar=No&footer=No' % ( 'i' * 24, 't' * 64), { 'instance': plugins.NotifyDiscord, @@ -280,6 +303,23 @@ TEST_URLS = ( ('flock://%s' % ('t' * 24), { 'instance': plugins.NotifyFlock, }), + # Image handling + ('flock://%s?image=True' % ('t' * 24), { + 'instance': plugins.NotifyFlock, + }), + ('flock://%s?image=False' % ('t' * 24), { + 'instance': plugins.NotifyFlock, + }), + ('flock://%s?image=True' % ('t' * 24), { + 'instance': plugins.NotifyFlock, + # Run test when image is set to True, but one couldn't actually be + # loaded from the Asset Object. + 'include_image': False, + }), + # Test to= + ('flock://%s?to=u:%s&format=markdown' % ('i' * 24, 'u' * 12), { + 'instance': plugins.NotifyFlock, + }), # Provide markdown format ('flock://%s?format=markdown' % ('i' * 24), { 'instance': plugins.NotifyFlock, @@ -479,6 +519,10 @@ TEST_URLS = ( ('ifttt://WebHookID@EventID/?+TemplateKey=TemplateVal', { 'instance': plugins.NotifyIFTTT, }), + # Test to= in which case we set the host to the webhook id + ('ifttt://WebHookID?to=EventID,EventID2', { + 'instance': plugins.NotifyIFTTT, + }), # Removing certain keys: ('ifttt://WebHookID@EventID/?-Value1=&-Value2', { 'instance': plugins.NotifyIFTTT, @@ -522,6 +566,18 @@ TEST_URLS = ( # Missing a channel 'instance': TypeError, }), + # APIKey + device (using to=) + ('join://%s?to=%s' % ('a' * 32, 'd' * 32), { + 'instance': plugins.NotifyJoin, + }), + # APIKey + device + ('join://%s@%s?image=True' % ('a' * 32, 'd' * 32), { + 'instance': plugins.NotifyJoin, + }), + # No image + ('join://%s@%s?image=False' % ('a' * 32, 'd' * 32), { + 'instance': plugins.NotifyJoin, + }), # APIKey + device ('join://%s/%s' % ('a' * 32, 'd' * 32), { 'instance': plugins.NotifyJoin, @@ -732,9 +788,24 @@ TEST_URLS = ( }), # Matrix supports webhooks too; the following tests this now: + ('matrix://user:token@localhost?mode=matrix&format=text', { + # user and token correctly specified with webhook + 'instance': plugins.NotifyMatrix, + 'response': False, + }), + ('matrix://user:token@localhost?mode=matrix&format=html', { + # user and token correctly specified with webhook + 'instance': plugins.NotifyMatrix, + }), + ('matrix://user:token@localhost?mode=slack&format=text', { + # user and token correctly specified with webhook + 'instance': plugins.NotifyMatrix, + }), + # Legacy (depricated) webhook reference ('matrix://user:token@localhost?webhook=matrix&format=text', { # user and token correctly specified with webhook 'instance': plugins.NotifyMatrix, + 'response': False, }), ('matrix://user:token@localhost?webhook=matrix&format=html', { # user and token correctly specified with webhook @@ -744,27 +815,45 @@ TEST_URLS = ( # user and token correctly specified with webhook 'instance': plugins.NotifyMatrix, }), - ('matrixs://user:token@localhost?webhook=SLACK&format=markdown', { + ('matrixs://user:token@localhost?mode=SLACK&format=markdown', { # user and token specified; slack webhook still detected # despite uppercase characters 'instance': plugins.NotifyMatrix, }), - ('matrix://user:token@localhost?webhook=On', { + # Image Reference + ('matrixs://user:token@localhost?mode=slack&format=markdown&image=True', { + # user and token specified; image set to True + 'instance': plugins.NotifyMatrix, + }), + ('matrixs://user:token@localhost?mode=slack&format=markdown&image=False', { + # user and token specified; image set to True + 'instance': plugins.NotifyMatrix, + }), + # Legacy (Depricated) image reference + ('matrixs://user:token@localhost?mode=slack&thumbnail=False', { + # user and token specified; image set to True + 'instance': plugins.NotifyMatrix, + }), + ('matrixs://user:token@localhost?mode=slack&thumbnail=True', { + # user and token specified; image set to True + 'instance': plugins.NotifyMatrix, + }), + ('matrix://user:token@localhost?mode=On', { # invalid webhook specified (unexpected boolean) 'instance': TypeError, }), - ('matrix://token@localhost/?webhook=Matrix', { + ('matrix://token@localhost/?mode=Matrix', { 'instance': plugins.NotifyMatrix, 'response': False, 'requests_response_code': requests.codes.internal_server_error, }), - ('matrix://user:token@localhost/webhook=matrix', { + ('matrix://user:token@localhost/mode=matrix', { 'instance': plugins.NotifyMatrix, # throw a bizzare code forcing us to fail to look it up 'response': False, 'requests_response_code': 999, }), - ('matrix://token@localhost:8080/?webhook=slack', { + ('matrix://token@localhost:8080/?mode=slack', { 'instance': plugins.NotifyMatrix, # Throws a series of connection and transfer exceptions when this flag # is set and tests that we gracfully handle them @@ -787,6 +876,20 @@ TEST_URLS = ( ('mmost://user@localhost/3ccdd113474722377935511fc85d3dd4?channel=test', { 'instance': plugins.NotifyMatterMost, }), + ('mmost://user@localhost/3ccdd113474722377935511fc85d3dd4?to=test', { + 'instance': plugins.NotifyMatterMost, + }), + ('mmost://localhost/3ccdd113474722377935511fc85d3dd4' + '?to=test&image=True', { + 'instance': plugins.NotifyMatterMost}), + ('mmost://localhost/3ccdd113474722377935511fc85d3dd4' \ + '?to=test&image=False', { + 'instance': plugins.NotifyMatterMost}), + ('mmost://localhost/3ccdd113474722377935511fc85d3dd4' \ + '?to=test&image=True', { + 'instance': plugins.NotifyMatterMost, + # don't include an image by default + 'include_image': False}), ('mmost://localhost:8080/3ccdd113474722377935511fc85d3dd4', { 'instance': plugins.NotifyMatterMost, }), @@ -915,6 +1018,10 @@ TEST_URLS = ( ('pbul://%s/#channel/' % ('a' * 32), { 'instance': plugins.NotifyPushBullet, }), + # APIKey + channel (via to= + ('pbul://%s/?to=#channel' % ('a' * 32), { + 'instance': plugins.NotifyPushBullet, + }), # APIKey + 2 channels ('pbul://%s/#channel1/#channel2' % ('a' * 32), { 'instance': plugins.NotifyPushBullet, @@ -999,6 +1106,10 @@ TEST_URLS = ( ('pushed://%s/%s/#channel/' % ('a' * 32, 'a' * 64), { 'instance': plugins.NotifyPushed, }), + # Application Key+Secret + channel (via to=) + ('pushed://%s/%s?to=channel' % ('a' * 32, 'a' * 64), { + 'instance': plugins.NotifyPushed, + }), # Application Key+Secret + dropped entry ('pushed://%s/%s/dropped/' % ('a' * 32, 'a' * 64), { 'instance': plugins.NotifyPushed, @@ -1100,6 +1211,10 @@ TEST_URLS = ( ('pover://%s@%s/DEVICE' % ('u' * 30, 'a' * 30), { 'instance': plugins.NotifyPushover, }), + # APIKey + Valid User + 1 Device (via to=) + ('pover://%s@%s?to=DEVICE' % ('u' * 30, 'a' * 30), { + 'instance': plugins.NotifyPushover, + }), # APIKey + Valid User + 2 Devices ('pover://%s@%s/DEVICE1/DEVICE2/' % ('u' * 30, 'a' * 30), { 'instance': plugins.NotifyPushover, @@ -1192,6 +1307,18 @@ TEST_URLS = ( }, }, }), + # A channel (using the to=) + ('rockets://user:pass@localhost?to=#channel', { + 'instance': plugins.NotifyRocketChat, + # The response text is expected to be the following on a success + 'requests_response_text': { + 'status': 'success', + 'data': { + 'authToken': 'abcd', + 'userId': 'user', + }, + }, + }), # A channel ('rockets://user:pass@localhost/#channel', { 'instance': plugins.NotifyRocketChat, @@ -1286,10 +1413,11 @@ TEST_URLS = ( }), ('ryver://apprise', { # Just org provided (no token) - 'instance': None, + 'instance': TypeError, }), ('ryver://abc,#/ckhrjW8w672m6HG', { - # Invalid org provided + # Invalid org provided (this isn't actually even a value url) + # because the hostname has ,# in it 'instance': None, }), ('ryver://a/ckhrjW8w672m6HG', { @@ -1304,11 +1432,28 @@ TEST_URLS = ( # Invalid webhook provided 'instance': TypeError, }), + ('ryver://apprise/ckhrjW8w672m6HG?mode=slack', { + # No username specified; this is still okay as we use whatever + # the user told the webhook to use; set our slack mode + 'instance': plugins.NotifyRyver, + }), + ('ryver://apprise/ckhrjW8w672m6HG?mode=ryver', { + # No username specified; this is still okay as we use whatever + # the user told the webhook to use; set our ryver mode + 'instance': plugins.NotifyRyver, + }), + # Legacy webhook mode setting: + # Legacy webhook mode setting: ('ryver://apprise/ckhrjW8w672m6HG?webhook=slack', { # No username specified; this is still okay as we use whatever # the user told the webhook to use; set our slack mode 'instance': plugins.NotifyRyver, }), + ('ryver://apprise/ckhrjW8w672m6HG?webhook=ryver', { + # No username specified; this is still okay as we use whatever + # the user told the webhook to use; set our ryver mode + 'instance': plugins.NotifyRyver, + }), ('ryver://caronc@apprise/ckhrjW8w672m6HG', { 'instance': plugins.NotifyRyver, # don't include an image by default @@ -1344,7 +1489,11 @@ TEST_URLS = ( }), ('slack://T1JJ3T3L2', { # Just Token 1 provided - 'instance': None, + 'instance': TypeError, + }), + ('slack://T1JJ3T3L2/A1BRTD4JD/', { + # Just 2 tokens provided + 'instance': TypeError, }), ('slack://T1JJ3T3L2/A1BRTD4JD/TIiajkdnlazkcOXrIdevi7FQ/#hmm/#-invalid-', { # No username specified; this is still okay as we sub in @@ -1361,11 +1510,15 @@ TEST_URLS = ( # don't include an image by default 'include_image': False, }), - ('slack://T1JJ3T3L2/A1BRTD4JD/TIiajkdnlazkcOXrIdevi7FQ/+id/%20/@id/', { + ('slack://T1JJ3T3L2/A1BRTD4JD/TIiajkdnlazkcOXrIdevi7FQ/+id/@id/', { # + encoded id, # @ userid 'instance': plugins.NotifySlack, }), + ('slack://username@T1JJ3T3L2/A1BRTD4JD/TIiajkdnlazkcOXrIdevi7FQ/' \ + '?to=#nuxref', { + 'instance': plugins.NotifySlack, + }), ('slack://username@T1JJ3T3L2/A1BRTD4JD/TIiajkdnlazkcOXrIdevi7FQ/#nuxref', { 'instance': plugins.NotifySlack, }), @@ -1433,6 +1586,11 @@ TEST_URLS = ( # Missing a topic and/or phone No 'instance': plugins.NotifySNS, }), + ('sns://T1JJ3T3L2/A1BRTD4JD/TIiajkdnlazkcOXrIdevi7FQ/us-east-1' \ + '?to=12223334444', { + # Missing a topic and/or phone No + 'instance': plugins.NotifySNS, + }), ('sns://T1JJ3T3L2/A1BRTD4JD/TIiajkdnlazkcevi7FQ/us-west-2/12223334444', { 'instance': plugins.NotifySNS, # throw a bizzare code forcing us to fail to look it up @@ -1466,6 +1624,10 @@ TEST_URLS = ( ('tgram://123456789:abcdefg_hijklmnop/id1/id2/', { 'instance': plugins.NotifyTelegram, }), + # Simple Message with multiple chat names + ('tgram://123456789:abcdefg_hijklmnop/?to=id1,id2', { + 'instance': plugins.NotifyTelegram, + }), # Simple Message with an invalid chat ID ('tgram://123456789:abcdefg_hijklmnop/%$/', { 'instance': plugins.NotifyTelegram, @@ -1594,6 +1756,15 @@ TEST_URLS = ( ('xbmc://localhost', { 'instance': plugins.NotifyXBMC, }), + ('xbmc://localhost?duration=14', { + 'instance': plugins.NotifyXBMC, + }), + ('xbmc://localhost?duration=invalid', { + 'instance': plugins.NotifyXBMC, + }), + ('xbmc://localhost?duration=-1', { + 'instance': plugins.NotifyXBMC, + }), ('xbmc://user:pass@localhost', { 'instance': plugins.NotifyXBMC, }), @@ -1810,13 +1981,13 @@ def test_rest_plugins(mock_post, mock_get): # Expected None but didn't get it print('%s instantiated %s (but expected None)' % ( url, str(obj))) - assert(False) + assert False - assert(isinstance(obj, instance)) + assert isinstance(obj, instance) is True if isinstance(obj, plugins.NotifyBase.NotifyBase): # We loaded okay; now lets make sure we can reverse this url - assert(isinstance(obj.url(), six.string_types) is True) + assert isinstance(obj.url(), six.string_types) is True # Instantiate the exact same object again using the URL from # the one that was already created properly @@ -1830,14 +2001,14 @@ def test_rest_plugins(mock_post, mock_get): # assertion failure makes things easier to debug later on print('TEST FAIL: {} regenerated as {}'.format( url, obj.url())) - assert(False) + assert False if self: # Iterate over our expected entries inside of our object for key, val in self.items(): # Test that our object has the desired key - assert(hasattr(key, obj)) - assert(getattr(key, obj) == val) + assert hasattr(key, obj) is True + assert getattr(key, obj) == val # # Stage 1: with title defined @@ -1947,9 +2118,11 @@ def test_rest_plugins(mock_post, mock_get): except Exception as e: # Handle our exception if(instance is None): + print('%s %s' % (url, str(e))) raise if not isinstance(e, instance): + print('%s %s' % (url, str(e))) raise @@ -1971,39 +2144,39 @@ def test_notify_boxcar_plugin(mock_post, mock_get): secret = '_' * 64 # Initializes the plugin with recipients set to None - plugins.NotifyBoxcar(access=access, secret=secret, recipients=None) + plugins.NotifyBoxcar(access=access, secret=secret, targets=None) # Initializes the plugin with a valid access, but invalid access key try: - plugins.NotifyBoxcar(access=None, secret=secret, recipients=None) - assert(False) + plugins.NotifyBoxcar(access=None, secret=secret, targets=None) + assert False except TypeError: # We should throw an exception for knowingly having an invalid - assert(True) + assert True # Initializes the plugin with a valid access, but invalid secret key try: - plugins.NotifyBoxcar(access=access, secret='invalid', recipients=None) - assert(False) + plugins.NotifyBoxcar(access=access, secret='invalid', targets=None) + assert False except TypeError: # We should throw an exception for knowingly having an invalid key - assert(True) + assert True # Initializes the plugin with a valid access, but invalid secret try: - plugins.NotifyBoxcar(access=access, secret=None, recipients=None) - assert(False) + plugins.NotifyBoxcar(access=access, secret=None, targets=None) + assert False except TypeError: # We should throw an exception for knowingly having an invalid - assert(True) + assert True # Initializes the plugin with recipients list # the below also tests our the variation of recipient types plugins.NotifyBoxcar( - access=access, secret=secret, recipients=[device, tag]) + access=access, secret=secret, targets=[device, tag]) mock_get.return_value = requests.Request() mock_post.return_value = requests.Request() @@ -2011,9 +2184,17 @@ def test_notify_boxcar_plugin(mock_post, mock_get): mock_get.return_value.status_code = requests.codes.created # Test notifications without a body or a title - p = plugins.NotifyBoxcar(access=access, secret=secret, recipients=None) + p = plugins.NotifyBoxcar(access=access, secret=secret, targets=None) - p.notify(body=None, title=None, notify_type=NotifyType.INFO) is True + assert p.notify(body=None, title=None, notify_type=NotifyType.INFO) is True + + # Test comma, separate values + device = 'a' * 64 + + p = plugins.NotifyBoxcar( + access=access, secret=secret, + targets=','.join([device, device, device])) + assert len(p.device_tokens) == 3 @mock.patch('requests.get') @@ -2039,11 +2220,11 @@ def test_notify_discord_plugin(mock_post, mock_get): # Empty Channel list try: plugins.NotifyDiscord(webhook_id=None, webhook_token=webhook_token) - assert(False) + assert False except TypeError: # we'll thrown because no webhook_id was specified - assert(True) + assert True obj = plugins.NotifyDiscord( webhook_id=webhook_id, @@ -2065,9 +2246,9 @@ def test_notify_discord_plugin(mock_post, mock_get): "#### Heading 5" results = obj.extract_markdown_sections(test_markdown) - assert(isinstance(results, list)) + assert isinstance(results, list) is True # We should have 5 sections (since there are 5 headers identified above) - assert(len(results) == 5) + assert len(results) == 5 # Use our test markdown string during a notification assert obj.notify( @@ -2465,7 +2646,7 @@ def test_notify_ifttt_plugin(mock_post, mock_get): plugins.NotifyBase.NotifyBase.request_rate_per_sec = 0 # Initialize some generic (but valid) tokens - webhook_id = 'webhookid' + webhook_id = 'webhook_id' events = ['event1', 'event2'] # Prepare Mock @@ -2476,17 +2657,26 @@ def test_notify_ifttt_plugin(mock_post, mock_get): mock_get.return_value.content = '{}' mock_post.return_value.content = '{}' + try: + obj = plugins.NotifyIFTTT(webhook_id=None, events=None) + # No webhook_id specified + assert False + + except TypeError: + # Exception should be thrown about the fact webhook_id was specified + assert True + try: obj = plugins.NotifyIFTTT(webhook_id=webhook_id, events=None) - # No token specified - assert(False) + # No events specified + assert False except TypeError: # Exception should be thrown about the fact no token was specified - assert(True) + assert True obj = plugins.NotifyIFTTT(webhook_id=webhook_id, events=events) - assert(isinstance(obj, plugins.NotifyIFTTT)) + assert isinstance(obj, plugins.NotifyIFTTT) is True assert obj.notify( body='body', title='title', notify_type=NotifyType.INFO) is True @@ -2496,7 +2686,7 @@ def test_notify_ifttt_plugin(mock_post, mock_get): webhook_id=webhook_id, events=events, add_tokens={'Test': 'ValueA', 'Test2': 'ValueB'}) - assert(isinstance(obj, plugins.NotifyIFTTT)) + assert isinstance(obj, plugins.NotifyIFTTT) is True assert obj.notify( body='body', title='title', notify_type=NotifyType.INFO) is True @@ -2515,7 +2705,7 @@ def test_notify_ifttt_plugin(mock_post, mock_get): # an exception. assert True - assert(isinstance(obj, plugins.NotifyIFTTT)) + assert isinstance(obj, plugins.NotifyIFTTT) is True assert obj.notify( body='body', title='title', notify_type=NotifyType.INFO) is True @@ -2531,7 +2721,7 @@ def test_notify_ifttt_plugin(mock_post, mock_get): plugins.NotifyIFTTT.ifttt_default_body_key, plugins.NotifyIFTTT.ifttt_default_type_key)) - assert(isinstance(obj, plugins.NotifyIFTTT)) + assert isinstance(obj, plugins.NotifyIFTTT) is True assert obj.notify( body='body', title='title', notify_type=NotifyType.INFO) is True @@ -2553,13 +2743,13 @@ def test_notify_join_plugin(mock_post, mock_get): apikey = 'a' * 32 # Initializes the plugin with devices set to a string - plugins.NotifyJoin(apikey=apikey, devices=group) + plugins.NotifyJoin(apikey=apikey, targets=group) # Initializes the plugin with devices set to None - plugins.NotifyJoin(apikey=apikey, devices=None) + plugins.NotifyJoin(apikey=apikey, targets=None) # Initializes the plugin with devices set to a set - p = plugins.NotifyJoin(apikey=apikey, devices=[group, device]) + p = plugins.NotifyJoin(apikey=apikey, targets=[group, device]) # Prepare our mock responses req = requests.Request() @@ -2573,6 +2763,45 @@ def test_notify_join_plugin(mock_post, mock_get): p.notify(body=None, title=None, notify_type=NotifyType.INFO) is False +def test_notify_pover_plugin(): + """ + API: NotifyPushover() Extra Checks + + """ + # Disable Throttling to speed testing + plugins.NotifyBase.NotifyBase.request_rate_per_sec = 0 + + # No token + try: + plugins.NotifyPushover(token=None) + assert False + + except TypeError: + # we'll thrown because we provided no token + assert True + + +def test_notify_ryver_plugin(): + """ + API: NotifyRyver() Extra Checks + + """ + # Disable Throttling to speed testing + plugins.NotifyBase.NotifyBase.request_rate_per_sec = 0 + + # must be 15 characters long + token = 'a' * 15 + + # No organization + try: + plugins.NotifyRyver(organization=None, token=token) + assert False + + except TypeError: + # we'll thrown because an empty list of channels was provided + assert True + + @mock.patch('requests.get') @mock.patch('requests.post') def test_notify_slack_plugin(mock_post, mock_get): @@ -2592,8 +2821,8 @@ def test_notify_slack_plugin(mock_post, mock_get): channels = 'chan1,#chan2,+id,@user,,,' obj = plugins.NotifySlack( - token_a=token_a, token_b=token_b, token_c=token_c, channels=channels) - assert(len(obj.channels) == 4) + token_a=token_a, token_b=token_b, token_c=token_c, targets=channels) + assert len(obj.channels) == 4 # Prepare Mock mock_get.return_value = requests.Request() @@ -2605,16 +2834,27 @@ def test_notify_slack_plugin(mock_post, mock_get): try: plugins.NotifySlack( token_a=token_a, token_b=token_b, token_c=token_c, - channels=None) - assert(False) + targets=None) + assert False except TypeError: # we'll thrown because an empty list of channels was provided - assert(True) + assert True + + # Missing first Token + try: + plugins.NotifySlack( + token_a=None, token_b=token_b, token_c=token_c, + targets=channels) + assert False + + except TypeError: + # we'll thrown because an empty list of channels was provided + assert True # Test include_image obj = plugins.NotifySlack( - token_a=token_a, token_b=token_b, token_c=token_c, channels=channels, + token_a=token_a, token_b=token_b, token_c=token_c, targets=channels, include_image=True) # This call includes an image with it's payload: @@ -2645,26 +2885,26 @@ def test_notify_pushbullet_plugin(mock_post, mock_get): mock_get.return_value.status_code = requests.codes.ok obj = plugins.NotifyPushBullet( - accesstoken=accesstoken, recipients=recipients) - assert(isinstance(obj, plugins.NotifyPushBullet)) - assert(len(obj.recipients) == 4) + accesstoken=accesstoken, targets=recipients) + assert isinstance(obj, plugins.NotifyPushBullet) is True + assert len(obj.targets) == 4 obj = plugins.NotifyPushBullet(accesstoken=accesstoken) - assert(isinstance(obj, plugins.NotifyPushBullet)) + assert isinstance(obj, plugins.NotifyPushBullet) is True # Default is to send to all devices, so there will be a # recipient here - assert(len(obj.recipients) == 1) + assert len(obj.targets) == 1 - obj = plugins.NotifyPushBullet(accesstoken=accesstoken, recipients=set()) - assert(isinstance(obj, plugins.NotifyPushBullet)) + obj = plugins.NotifyPushBullet(accesstoken=accesstoken, targets=set()) + assert isinstance(obj, plugins.NotifyPushBullet) is True # Default is to send to all devices, so there will be a # recipient here - assert(len(obj.recipients) == 1) + assert len(obj.targets) == 1 # Support the handling of an empty and invalid URL strings - assert(plugins.NotifyPushBullet.parse_url(None) is None) - assert(plugins.NotifyPushBullet.parse_url('') is None) - assert(plugins.NotifyPushBullet.parse_url(42) is None) + assert plugins.NotifyPushBullet.parse_url(None) is None + assert plugins.NotifyPushBullet.parse_url('') is None + assert plugins.NotifyPushBullet.parse_url(42) is None @mock.patch('requests.get') @@ -2696,12 +2936,12 @@ def test_notify_pushed_plugin(mock_post, mock_get): app_secret=None, recipients=None, ) - assert(False) + assert False except TypeError: # No application Secret was specified; it's a good thing if # this exception was thrown - assert(True) + assert True try: obj = plugins.NotifyPushed( @@ -2711,47 +2951,20 @@ def test_notify_pushed_plugin(mock_post, mock_get): ) # recipients list set to (None) is perfectly fine; in this # case it will notify the App - assert(True) + assert True except TypeError: # Exception should never be thrown! - assert(False) - - try: - obj = plugins.NotifyPushed( - app_key=app_key, - app_secret=app_secret, - recipients=object(), - ) - # invalid recipients list (object) - assert(False) - - except TypeError: - # Exception should be thrown about the fact no recipients were - # specified - assert(True) - - try: - obj = plugins.NotifyPushed( - app_key=app_key, - app_secret=app_secret, - recipients=set(), - ) - # Any empty set is acceptable - assert(True) - - except TypeError: - # Exception should never be thrown - assert(False) + assert False obj = plugins.NotifyPushed( app_key=app_key, app_secret=app_secret, - recipients=recipients, + targets=recipients, ) - assert(isinstance(obj, plugins.NotifyPushed)) - assert(len(obj.channels) == 2) - assert(len(obj.users) == 2) + assert isinstance(obj, plugins.NotifyPushed) is True + assert len(obj.channels) == 2 + assert len(obj.users) == 2 # Support the handling of an empty and invalid URL strings assert plugins.NotifyPushed.parse_url(None) is None @@ -2789,42 +3002,42 @@ def test_notify_pushover_plugin(mock_post, mock_get): mock_get.return_value.status_code = requests.codes.ok try: - obj = plugins.NotifyPushover(user=user, token=None) + obj = plugins.NotifyPushover(user=user, webhook_id=None) # No token specified - assert(False) + assert False except TypeError: # Exception should be thrown about the fact no token was specified - assert(True) + assert True - obj = plugins.NotifyPushover(user=user, token=token, devices=devices) - assert(isinstance(obj, plugins.NotifyPushover)) - assert(len(obj.devices) == 3) + obj = plugins.NotifyPushover(user=user, token=token, targets=devices) + assert isinstance(obj, plugins.NotifyPushover) is True + assert len(obj.targets) == 3 # This call fails because there is 1 invalid device assert obj.notify( body='body', title='title', notify_type=NotifyType.INFO) is False obj = plugins.NotifyPushover(user=user, token=token) - assert(isinstance(obj, plugins.NotifyPushover)) + assert isinstance(obj, plugins.NotifyPushover) is True # Default is to send to all devices, so there will be a # device defined here - assert(len(obj.devices) == 1) + assert len(obj.targets) == 1 # This call succeeds because all of the devices are valid assert obj.notify( body='body', title='title', notify_type=NotifyType.INFO) is True - obj = plugins.NotifyPushover(user=user, token=token, devices=set()) - assert(isinstance(obj, plugins.NotifyPushover)) + obj = plugins.NotifyPushover(user=user, token=token, targets=set()) + assert isinstance(obj, plugins.NotifyPushover) is True # Default is to send to all devices, so there will be a # device defined here - assert(len(obj.devices) == 1) + assert len(obj.targets) == 1 # Support the handling of an empty and invalid URL strings - assert(plugins.NotifyPushover.parse_url(None) is None) - assert(plugins.NotifyPushover.parse_url('') is None) - assert(plugins.NotifyPushover.parse_url(42) is None) + assert plugins.NotifyPushover.parse_url(None) is None + assert plugins.NotifyPushover.parse_url('') is None + assert plugins.NotifyPushover.parse_url(42) is None @mock.patch('requests.get') @@ -2852,44 +3065,11 @@ def test_notify_rocketchat_plugin(mock_post, mock_get): mock_post.return_value.content = '' mock_get.return_value.content = '' - try: - obj = plugins.NotifyRocketChat( - user=user, password=password, recipients=None) - # invalid recipients list (None) - assert(False) - - except TypeError: - # Exception should be thrown about the fact no recipients were - # specified - assert(True) - - try: - obj = plugins.NotifyRocketChat( - user=user, password=password, recipients=object()) - # invalid recipients list (object) - assert(False) - - except TypeError: - # Exception should be thrown about the fact no recipients were - # specified - assert(True) - - try: - obj = plugins.NotifyRocketChat( - user=user, password=password, recipients=set()) - # invalid recipient list/set (no entries) - assert(False) - - except TypeError: - # Exception should be thrown about the fact no recipients were - # specified - assert(True) - obj = plugins.NotifyRocketChat( - user=user, password=password, recipients=recipients) - assert(isinstance(obj, plugins.NotifyRocketChat)) - assert(len(obj.channels) == 2) - assert(len(obj.rooms) == 2) + user=user, password=password, targets=recipients) + assert isinstance(obj, plugins.NotifyRocketChat) is True + assert len(obj.channels) == 2 + assert len(obj.rooms) == 2 # # Logout @@ -2980,57 +3160,39 @@ def test_notify_telegram_plugin(mock_post, mock_get): mock_post.return_value.content = '{}' try: - obj = plugins.NotifyTelegram(bot_token=None, chat_ids=chat_ids) + obj = plugins.NotifyTelegram(bot_token=None, targets=chat_ids) # invalid bot token (None) - assert(False) + assert False except TypeError: # Exception should be thrown about the fact no token was specified - assert(True) + assert True try: obj = plugins.NotifyTelegram( - bot_token=invalid_bot_token, chat_ids=chat_ids) + bot_token=invalid_bot_token, targets=chat_ids) # invalid bot token - assert(False) + assert False except TypeError: # Exception should be thrown about the fact an invalid token was # specified - assert(True) + assert True - try: - obj = plugins.NotifyTelegram(bot_token=bot_token, chat_ids=None) - # No chat_ids specified - assert(False) - - except TypeError: - # Exception should be thrown about the fact no token was specified - assert(True) - - try: - obj = plugins.NotifyTelegram(bot_token=bot_token, chat_ids=set()) - # No chat_ids specified - assert(False) - - except TypeError: - # Exception should be thrown about the fact no token was specified - assert(True) - - obj = plugins.NotifyTelegram(bot_token=bot_token, chat_ids=chat_ids) - assert(isinstance(obj, plugins.NotifyTelegram)) - assert(len(obj.chat_ids) == 2) + obj = plugins.NotifyTelegram(bot_token=bot_token, targets=chat_ids) + assert isinstance(obj, plugins.NotifyTelegram) is True + assert len(obj.targets) == 2 # test url call - assert(isinstance(obj.url(), six.string_types)) + assert isinstance(obj.url(), six.string_types) is True # Test that we can load the string we generate back: obj = plugins.NotifyTelegram(**plugins.NotifyTelegram.parse_url(obj.url())) - assert(isinstance(obj, plugins.NotifyTelegram)) + assert isinstance(obj, plugins.NotifyTelegram) is True # Support the handling of an empty and invalid URL strings - assert(plugins.NotifyTelegram.parse_url(None) is None) - assert(plugins.NotifyTelegram.parse_url('') is None) - assert(plugins.NotifyTelegram.parse_url(42) is None) + assert plugins.NotifyTelegram.parse_url(None) is None + assert plugins.NotifyTelegram.parse_url('') is None + assert plugins.NotifyTelegram.parse_url(42) is None # Prepare Mock to fail response = mock.Mock() @@ -3044,7 +3206,7 @@ def test_notify_telegram_plugin(mock_post, mock_get): mock_post.return_value = response # No image asset - nimg_obj = plugins.NotifyTelegram(bot_token=bot_token, chat_ids=chat_ids) + nimg_obj = plugins.NotifyTelegram(bot_token=bot_token, targets=chat_ids) nimg_obj.asset = AppriseAsset(image_path_mask=False, image_url_mask=False) # Test that our default settings over-ride base settings since they are @@ -3065,8 +3227,8 @@ def test_notify_telegram_plugin(mock_post, mock_get): body='body', title='title', notify_type=NotifyType.INFO) is False # This tests erroneous messages involving a single chat id - obj = plugins.NotifyTelegram(bot_token=bot_token, chat_ids='l2g') - nimg_obj = plugins.NotifyTelegram(bot_token=bot_token, chat_ids='l2g') + obj = plugins.NotifyTelegram(bot_token=bot_token, targets='l2g') + nimg_obj = plugins.NotifyTelegram(bot_token=bot_token, targets='l2g') nimg_obj.asset = AppriseAsset(image_path_mask=False, image_url_mask=False) assert obj.notify( @@ -3111,9 +3273,9 @@ def test_notify_telegram_plugin(mock_post, mock_get): }) mock_post.return_value.status_code = requests.codes.ok - obj = plugins.NotifyTelegram(bot_token=bot_token, chat_ids=None) - assert(len(obj.chat_ids) == 1) - assert(obj.chat_ids[0] == '532389719') + obj = plugins.NotifyTelegram(bot_token=bot_token, targets=None) + assert len(obj.targets) == 1 + assert obj.targets[0] == '532389719' # Do the test again, but without the expected (parsed response) mock_post.return_value.content = dumps({ @@ -3125,50 +3287,54 @@ def test_notify_telegram_plugin(mock_post, mock_get): ], }) try: - obj = plugins.NotifyTelegram(bot_token=bot_token, chat_ids=None) + obj = plugins.NotifyTelegram(bot_token=bot_token, targets=None) # No chat_ids specified - assert(False) + assert False except TypeError: # Exception should be thrown about the fact no token was specified - assert(True) + assert True + + # Detect the bot with a bad response + mock_post.return_value.content = dumps({}) + obj.detect_bot_owner() # Test our bot detection with a internal server error mock_post.return_value.status_code = requests.codes.internal_server_error try: - obj = plugins.NotifyTelegram(bot_token=bot_token, chat_ids=None) + obj = plugins.NotifyTelegram(bot_token=bot_token, targets=None) # No chat_ids specified - assert(False) + assert False except TypeError: # Exception should be thrown about the fact no token was specified - assert(True) + assert True # Test our bot detection with an unmappable html error mock_post.return_value.status_code = 999 try: - obj = plugins.NotifyTelegram(bot_token=bot_token, chat_ids=None) + obj = plugins.NotifyTelegram(bot_token=bot_token, targets=None) # No chat_ids specified - assert(False) + assert False except TypeError: # Exception should be thrown about the fact no token was specified - assert(True) + assert True # Do it again but this time provide a failure message mock_post.return_value.content = dumps({'description': 'Failure Message'}) try: - obj = plugins.NotifyTelegram(bot_token=bot_token, chat_ids=None) + obj = plugins.NotifyTelegram(bot_token=bot_token, targets=None) # No chat_ids specified - assert(False) + assert False except TypeError: # Exception should be thrown about the fact no token was specified - assert(True) + assert True # Do it again but this time provide a failure message and perform a # notification without a bot detection by providing at least 1 chat id - obj = plugins.NotifyTelegram(bot_token=bot_token, chat_ids=['@abcd']) + obj = plugins.NotifyTelegram(bot_token=bot_token, targets=['@abcd']) assert nimg_obj.notify( body='body', title='title', notify_type=NotifyType.INFO) is False @@ -3176,13 +3342,13 @@ def test_notify_telegram_plugin(mock_post, mock_get): for _exception in REQUEST_EXCEPTIONS: mock_post.side_effect = _exception try: - obj = plugins.NotifyTelegram(bot_token=bot_token, chat_ids=None) + obj = plugins.NotifyTelegram(bot_token=bot_token, targets=None) # No chat_ids specified - assert(False) + assert False except TypeError: # Exception should be thrown about the fact no token was specified - assert(True) + assert True def test_notify_overflow_truncate(): diff --git a/test/test_sns_plugin.py b/test/test_sns_plugin.py index e1a5f8b8..bc5d5e74 100644 --- a/test/test_sns_plugin.py +++ b/test/test_sns_plugin.py @@ -51,7 +51,7 @@ def test_object_initialization(): access_key_id=None, secret_access_key=TEST_ACCESS_KEY_SECRET, region_name=TEST_REGION, - recipients='+1800555999', + targets='+1800555999', ) # The entries above are invalid, our code should never reach here assert(False) @@ -66,7 +66,7 @@ def test_object_initialization(): access_key_id=TEST_ACCESS_KEY_ID, secret_access_key=None, region_name=TEST_REGION, - recipients='+1800555999', + targets='+1800555999', ) # The entries above are invalid, our code should never reach here assert(False) @@ -81,7 +81,7 @@ def test_object_initialization(): access_key_id=TEST_ACCESS_KEY_ID, secret_access_key=TEST_ACCESS_KEY_SECRET, region_name=None, - recipients='+1800555999', + targets='+1800555999', ) # The entries above are invalid, our code should never reach here assert(False) @@ -96,7 +96,7 @@ def test_object_initialization(): access_key_id=TEST_ACCESS_KEY_ID, secret_access_key=TEST_ACCESS_KEY_SECRET, region_name=TEST_REGION, - recipients=None, + targets=None, ) # Still valid even without recipients assert(True) @@ -111,7 +111,7 @@ def test_object_initialization(): access_key_id=TEST_ACCESS_KEY_ID, secret_access_key=TEST_ACCESS_KEY_SECRET, region_name=TEST_REGION, - recipients=object(), + targets=object(), ) # Still valid even without recipients assert(True) @@ -127,7 +127,7 @@ def test_object_initialization(): access_key_id=TEST_ACCESS_KEY_ID, secret_access_key=TEST_ACCESS_KEY_SECRET, region_name=TEST_REGION, - recipients='+1809', + targets='+1809', ) # The recipient is invalid, but it's still okay; this Notification # still becomes pretty much useless at this point though @@ -144,7 +144,7 @@ def test_object_initialization(): access_key_id=TEST_ACCESS_KEY_ID, secret_access_key=TEST_ACCESS_KEY_SECRET, region_name=TEST_REGION, - recipients='#(invalid-topic-because-of-the-brackets)', + targets='#(invalid-topic-because-of-the-brackets)', ) # The recipient is invalid, but it's still okay; this Notification # still becomes pretty much useless at this point though @@ -169,7 +169,7 @@ def test_url_parsing(): ) # Confirm that there were no recipients found - assert(len(results['recipients']) == 0) + assert(len(results['targets']) == 0) assert('region_name' in results) assert(TEST_REGION == results['region_name']) assert('access_key_id' in results) @@ -188,9 +188,9 @@ def test_url_parsing(): ) # Confirm that our recipients were found - assert(len(results['recipients']) == 2) - assert('+18001234567' in results['recipients']) - assert('MyTopic' in results['recipients']) + assert(len(results['targets']) == 2) + assert('+18001234567' in results['targets']) + assert('MyTopic' in results['targets']) assert('region_name' in results) assert(TEST_REGION == results['region_name']) assert('access_key_id' in results) diff --git a/test/test_twitter_plugin.py b/test/test_twitter_plugin.py index 8f3b6a02..573ba79d 100644 --- a/test/test_twitter_plugin.py +++ b/test/test_twitter_plugin.py @@ -23,10 +23,16 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. +import six +import mock +from random import choice +from string import ascii_uppercase as str_alpha +from string import digits as str_num + from apprise import plugins from apprise import NotifyType from apprise import Apprise -import mock +from apprise import OverflowMode # Disable logging for a cleaner testing output import logging @@ -40,6 +46,9 @@ TEST_URLS = ( ('tweet://', { 'instance': None, }), + ('tweet://:@/', { + 'instance': None, + }), ('tweet://consumer_key', { # Missing Keys 'instance': TypeError, @@ -60,9 +69,11 @@ TEST_URLS = ( # We're good! 'instance': plugins.NotifyTwitter, }), - ('tweet://:@/', { - 'instance': None, - }), + ('tweet://usera@consumer_key/consumer_key/access_token/' + 'access_secret/?to=userb', { + # We're good! + 'instance': plugins.NotifyTwitter, + }), ) @@ -73,6 +84,22 @@ def test_plugin(mock_oauth, mock_api): API: NotifyTwitter Plugin() (pt1) """ + # Disable Throttling to speed testing + plugins.NotifyBase.NotifyBase.request_rate_per_sec = 0 + + # Define how many characters exist per line + row = 80 + + # Some variables we use to control the data we work with + body_len = 1024 + title_len = 1024 + + # Create a large body and title with random data + body = ''.join(choice(str_alpha + str_num + ' ') for _ in range(body_len)) + body = '\r\n'.join([body[i: i + row] for i in range(0, len(body), row)]) + + # Create our title using random data + title = ''.join(choice(str_alpha + str_num) for _ in range(title_len)) # iterate over our dictionary and test it out for (url, meta) in TEST_URLS: @@ -86,6 +113,9 @@ def test_plugin(mock_oauth, mock_api): # Our expected Query response (True, False, or exception type) response = meta.get('response', True) + # Allow notification type override, otherwise default to INFO + notify_type = meta.get('notify_type', NotifyType.INFO) + # Allow us to force the server response code to be something other then # the defaults response = meta.get( @@ -94,25 +124,69 @@ def test_plugin(mock_oauth, mock_api): try: obj = Apprise.instantiate(url, suppress_exceptions=False) - if instance is None: - # Check that we got what we came for - assert obj is instance + if obj is None: + if instance is not None: + # We're done (assuming this is what we were expecting) + print("{} didn't instantiate itself " + "(we expected it to)".format(url)) + assert False continue - assert(isinstance(obj, instance)) + if instance is None: + # Expected None but didn't get it + print('%s instantiated %s (but expected None)' % ( + url, str(obj))) + assert False + + assert isinstance(obj, instance) is True + + if isinstance(obj, plugins.NotifyBase.NotifyBase): + # We loaded okay; now lets make sure we can reverse this url + assert isinstance(obj.url(), six.string_types) is True + + # Instantiate the exact same object again using the URL from + # the one that was already created properly + obj_cmp = Apprise.instantiate(obj.url()) + + # Our object should be the same instance as what we had + # originally expected above. + if not isinstance(obj_cmp, plugins.NotifyBase.NotifyBase): + # Assert messages are hard to trace back with the way + # these tests work. Just printing before throwing our + # assertion failure makes things easier to debug later on + print('TEST FAIL: {} regenerated as {}'.format( + url, obj.url())) + assert False if self: # Iterate over our expected entries inside of our object for key, val in self.items(): # Test that our object has the desired key - assert(hasattr(key, obj)) - assert(getattr(key, obj) == val) + assert hasattr(key, obj) is True + assert getattr(key, obj) == val + + obj.request_rate_per_sec = 0 # check that we're as expected assert obj.notify( title='test', body='body', notify_type=NotifyType.INFO) == response + # check that this doesn't change using different overflow + # methods + assert obj.notify( + body=body, title=title, + notify_type=notify_type, + overflow=OverflowMode.UPSTREAM) == response + assert obj.notify( + body=body, title=title, + notify_type=notify_type, + overflow=OverflowMode.TRUNCATE) == response + assert obj.notify( + body=body, title=title, + notify_type=notify_type, + overflow=OverflowMode.SPLIT) == response + except AssertionError: # Don't mess with these entries raise diff --git a/test/test_windows_plugin.py b/test/test_windows_plugin.py index 13c580bc..9a08ead2 100644 --- a/test/test_windows_plugin.py +++ b/test/test_windows_plugin.py @@ -125,6 +125,43 @@ def test_windows_plugin(): assert(obj.notify(title='title', body='body', notify_type=apprise.NotifyType.INFO) is True) + obj = apprise.Apprise.instantiate( + 'windows://_/?image=True', suppress_exceptions=False) + obj.duration = 0 + assert(isinstance(obj.url(), six.string_types) is True) + assert(obj.notify(title='title', body='body', + notify_type=apprise.NotifyType.INFO) is True) + + obj = apprise.Apprise.instantiate( + 'windows://_/?image=False', suppress_exceptions=False) + obj.duration = 0 + assert(isinstance(obj.url(), six.string_types) is True) + assert(obj.notify(title='title', body='body', + notify_type=apprise.NotifyType.INFO) is True) + + obj = apprise.Apprise.instantiate( + 'windows://_/?duration=1', suppress_exceptions=False) + assert(isinstance(obj.url(), six.string_types) is True) + assert(obj.notify(title='title', body='body', + notify_type=apprise.NotifyType.INFO) is True) + # loads okay + assert obj.duration == 1 + + obj = apprise.Apprise.instantiate( + 'windows://_/?duration=invalid', suppress_exceptions=False) + # Falls back to default + assert obj.duration == obj.default_popup_duration_sec + + obj = apprise.Apprise.instantiate( + 'windows://_/?duration=-1', suppress_exceptions=False) + # Falls back to default + assert obj.duration == obj.default_popup_duration_sec + + obj = apprise.Apprise.instantiate( + 'windows://_/?duration=0', suppress_exceptions=False) + # Falls back to default + assert obj.duration == obj.default_popup_duration_sec + # Test our loading of our icon exception; it will still allow the # notification to be sent win32gui.LoadImage.side_effect = AttributeError diff --git a/test/test_xmpp_plugin.py b/test/test_xmpp_plugin.py index 697c5283..c065d09e 100644 --- a/test/test_xmpp_plugin.py +++ b/test/test_xmpp_plugin.py @@ -26,7 +26,7 @@ import six import mock import sys -# import types +import ssl import apprise @@ -129,6 +129,39 @@ def test_xmpp_plugin(tmpdir): # Not possible because no password was specified assert obj is None + # SSL Flags + if hasattr(ssl, "PROTOCOL_TLS"): + # Test cases where PROTOCOL_TLS simply isn't available + ssl_temp_swap = ssl.PROTOCOL_TLS + del ssl.PROTOCOL_TLS + + # Test our URL + url = 'xmpps://user:pass@example.com' + obj = apprise.Apprise.instantiate(url, suppress_exceptions=False) + # Test we loaded + assert isinstance(obj, apprise.plugins.NotifyXMPP) is True + assert obj.notify( + title='title', body='body', + notify_type=apprise.NotifyType.INFO) is True + + # Restore the variable for remaining tests + setattr(ssl, 'PROTOCOL_TLS', ssl_temp_swap) + + else: + # Handle case where it is not missing + setattr(ssl, 'PROTOCOL_TLS', ssl.PROTOCOL_TLSv1) + # Test our URL + url = 'xmpps://user:pass@example.com' + obj = apprise.Apprise.instantiate(url, suppress_exceptions=False) + # Test we loaded + assert isinstance(obj, apprise.plugins.NotifyXMPP) is True + assert obj.notify( + title='title', body='body', + notify_type=apprise.NotifyType.INFO) is True + + # Restore settings as they were + del ssl.PROTOCOL_TLS + # Try Different Variations of our URL for url in ( 'xmpps://user:pass@example.com',