normalize plugin classes mass code cleanup

This commit is contained in:
Chris Caron 2019-03-26 23:06:20 -04:00
parent c9b957c434
commit 842de28191
44 changed files with 2202 additions and 1228 deletions

View File

@ -106,7 +106,7 @@ class URLBase(object):
# Secure Mode # Secure Mode
self.secure = kwargs.get('secure', False) self.secure = kwargs.get('secure', False)
self.host = kwargs.get('host', '') self.host = URLBase.unquote(kwargs.get('host'))
self.port = kwargs.get('port') self.port = kwargs.get('port')
if self.port: if self.port:
try: try:
@ -116,13 +116,20 @@ class URLBase(object):
self.port = None self.port = None
self.user = kwargs.get('user') 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') 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: if 'tag' in kwargs:
# We want to associate some tags with our notification service. # We want to associate some tags with our notification service.
# the code below gets the 'tag' argument if defined, otherwise # the code below gets the 'tag' argument if defined, otherwise
# it just falls back to whatever was already defined globally # 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 # Tracks the time any i/o was made to the remote server. This value
# is automatically set and controlled through the throttle() call. # is automatically set and controlled through the throttle() call.
@ -161,7 +168,7 @@ class URLBase(object):
elapsed = (reference - self._last_io_datetime).total_seconds() elapsed = (reference - self._last_io_datetime).total_seconds()
if wait is not None: if wait is not None:
self.logger.debug('Throttling for {}s...'.format(wait)) self.logger.debug('Throttling forced for {}s...'.format(wait))
sleep(wait) sleep(wait)
elif elapsed < self.request_rate_per_sec: 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 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: if unquote:
return PATHSPLIT_LIST_DELIM.split( content = \
URLBase.unquote(path).lstrip('/')) [URLBase.unquote(x) for x in filter(bool, content)]
return PATHSPLIT_LIST_DELIM.split(path.lstrip('/'))
return content
@property @property
def app_id(self): def app_id(self):

View File

@ -33,9 +33,6 @@ from ..common import NOTIFY_FORMATS
from ..common import OverflowMode from ..common import OverflowMode
from ..common import OVERFLOW_MODES from ..common import OVERFLOW_MODES
# HTML New Line Delimiter
NOTIFY_NEWLINE = '\r\n'
class NotifyBase(URLBase): class NotifyBase(URLBase):
""" """
@ -94,12 +91,10 @@ class NotifyBase(URLBase):
# Store the specified format if specified # Store the specified format if specified
notify_format = kwargs.get('format', '') notify_format = kwargs.get('format', '')
if notify_format.lower() not in NOTIFY_FORMATS: if notify_format.lower() not in NOTIFY_FORMATS:
self.logger.error( msg = 'Invalid notification format %s'.format(notify_format)
'Invalid notification format %s' % notify_format, self.logger.error(msg)
) raise TypeError(msg)
raise TypeError(
'Invalid notification format %s' % notify_format,
)
# Provide override # Provide override
self.notify_format = notify_format self.notify_format = notify_format
@ -107,12 +102,10 @@ class NotifyBase(URLBase):
# Store the specified format if specified # Store the specified format if specified
overflow = kwargs.get('overflow', '') overflow = kwargs.get('overflow', '')
if overflow.lower() not in OVERFLOW_MODES: if overflow.lower() not in OVERFLOW_MODES:
self.logger.error( msg = 'Invalid overflow method {}'.format(overflow)
'Invalid overflow method %s' % overflow, self.logger.error(msg)
) raise TypeError(msg)
raise TypeError(
'Invalid overflow method %s' % overflow,
)
# Provide override # Provide override
self.overflow_mode = overflow self.overflow_mode = overflow

View File

@ -38,6 +38,7 @@ except ImportError:
from urllib.parse import urlparse from urllib.parse import urlparse
from .NotifyBase import NotifyBase from .NotifyBase import NotifyBase
from ..utils import parse_bool
from ..common import NotifyType from ..common import NotifyType
from ..common import NotifyImageSize from ..common import NotifyImageSize
@ -51,8 +52,8 @@ DEFAULT_TAG = '@all'
IS_TAG = re.compile(r'^[@](?P<name>[A-Z0-9]{1,63})$', re.I) IS_TAG = re.compile(r'^[@](?P<name>[A-Z0-9]{1,63})$', re.I)
# Device tokens are only referenced when developing. # Device tokens are only referenced when developing.
# it's not likely you'll send a message directly to a device, but # It's not likely you'll send a message directly to a device, but if you do;
# if you do; this plugin supports it. # this plugin supports it.
IS_DEVICETOKEN = re.compile(r'^[A-Z0-9]{64}$', re.I) 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 # 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_ACCESS = re.compile(r'[A-Z0-9_-]{64}', re.I)
VALIDATE_SECRET = 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 # Used to break apart list of potential tags by their delimiter into a useable
# into a usable list. # list.
TAGS_LIST_DELIM = re.compile(r'[ \t\r\n,\\/]+') 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 # The maximum allowable characters allowed in the body per message
body_maxlen = 10000 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 Initialize Boxcar Object
""" """
@ -108,66 +110,62 @@ class NotifyBoxcar(NotifyBase):
self.access = access.strip() self.access = access.strip()
except AttributeError: except AttributeError:
self.logger.warning( msg = 'The specified access key is invalid.'
'The specified access key specified is invalid.', self.logger.warning(msg)
) raise TypeError(msg)
raise TypeError(
'The specified access key specified is invalid.',
)
try: try:
# Secret Key (associated with project) # Secret Key (associated with project)
self.secret = secret.strip() self.secret = secret.strip()
except AttributeError: except AttributeError:
self.logger.warning( msg = 'The specified secret key is invalid.'
'The specified secret key specified is invalid.', self.logger.warning(msg)
) raise TypeError(msg)
raise TypeError(
'The specified secret key specified is invalid.',
)
if not VALIDATE_ACCESS.match(self.access): if not VALIDATE_ACCESS.match(self.access):
self.logger.warning( msg = 'The access key specified ({}) is invalid.'\
'The access key specified (%s) is invalid.' % self.access, .format(self.access)
) self.logger.warning(msg)
raise TypeError( raise TypeError(msg)
'The access key specified (%s) is invalid.' % self.access,
)
if not VALIDATE_SECRET.match(self.secret): if not VALIDATE_SECRET.match(self.secret):
self.logger.warning( msg = 'The secret key specified ({}) is invalid.'\
'The secret key specified (%s) is invalid.' % self.secret, .format(self.secret)
) self.logger.warning(msg)
raise TypeError( raise TypeError(msg)
'The secret key specified (%s) is invalid.' % self.secret,
)
if not recipients: if not targets:
self.tags.append(DEFAULT_TAG) self.tags.append(DEFAULT_TAG)
recipients = [] targets = []
elif isinstance(recipients, six.string_types): elif isinstance(targets, six.string_types):
recipients = [x for x in filter(bool, TAGS_LIST_DELIM.split( targets = [x for x in filter(bool, TAGS_LIST_DELIM.split(
recipients, targets,
))] ))]
# Validate recipients and drop bad ones: # Validate targets and drop bad ones:
for recipient in recipients: for target in targets:
if IS_TAG.match(recipient): if IS_TAG.match(target):
# store valid tag/alias # 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 # store valid device
self.device_tokens.append(recipient) self.device_tokens.append(target)
else: else:
self.logger.warning( self.logger.warning(
'Dropped invalid tag/alias/device_token ' '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): def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs):
""" """
Perform Boxcar Notification Perform Boxcar Notification
@ -200,7 +198,9 @@ class NotifyBoxcar(NotifyBase):
payload['device_tokens'] = self.device_tokens payload['device_tokens'] = self.device_tokens
# Source picture should be <= 450 DP wide, ~2:1 aspect. # 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: if image_url:
# Set our image # Set our image
payload['@img'] = image_url payload['@img'] = image_url
@ -218,7 +218,7 @@ class NotifyBoxcar(NotifyBase):
sha1, sha1,
) )
params = self.urlencode({ params = NotifyBoxcar.urlencode({
"publishkey": self.access, "publishkey": self.access,
"signature": h.hexdigest(), "signature": h.hexdigest(),
}) })
@ -244,7 +244,7 @@ class NotifyBoxcar(NotifyBase):
if r.status_code != requests.codes.created: if r.status_code != requests.codes.created:
# We had a problem # We had a problem
status_str = \ status_str = \
NotifyBase.http_response_code_lookup(r.status_code) NotifyBoxcar.http_response_code_lookup(r.status_code)
self.logger.warning( self.logger.warning(
'Failed to send Boxcar notification: ' 'Failed to send Boxcar notification: '
@ -282,16 +282,17 @@ class NotifyBoxcar(NotifyBase):
args = { args = {
'format': self.notify_format, 'format': self.notify_format,
'overflow': self.overflow_mode, '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, schema=self.secure_protocol,
access=self.quote(self.access), access=NotifyBoxcar.quote(self.access, safe=''),
secret=self.quote(self.secret), secret=NotifyBoxcar.quote(self.secret, safe=''),
recipients='/'.join([ targets='/'.join([
self.quote(x) for x in chain( NotifyBoxcar.quote(x, safe='') for x in chain(
self.tags, self.device_tokens) if x != DEFAULT_TAG]), self.tags, self.device_tokens) if x != DEFAULT_TAG]),
args=self.urlencode(args), args=NotifyBoxcar.urlencode(args),
) )
@staticmethod @staticmethod
@ -307,23 +308,30 @@ class NotifyBoxcar(NotifyBase):
return None return None
# The first token is stored in the hostname # The first token is stored in the hostname
access = results['host'] results['access'] = NotifyBoxcar.unquote(results['host'])
# Now fetch the remaining tokens # Get our entries; split_path() looks after unquoting content for us
secret = NotifyBase.split_path(results['fullpath'])[0] # by default
entries = NotifyBoxcar.split_path(results['fullpath'])
# Our recipients try:
recipients = ','.join( # Now fetch the remaining tokens
NotifyBase.split_path(results['fullpath'])[1:]) results['secret'] = entries.pop(0)
if not (access and secret): except IndexError:
# If we did not recive an access and/or secret code # secret wasn't specified
# then we're done results['secret'] = None
return None
# Store our required content # Our recipients make up the remaining entries of our array
results['recipients'] = recipients if recipients else None results['targets'] = entries
results['access'] = access
results['secret'] = secret # 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 return results

View File

@ -30,6 +30,7 @@ from .NotifyBase import NotifyBase
from ..common import NotifyImageSize from ..common import NotifyImageSize
from ..common import NotifyType from ..common import NotifyType
from ..utils import GET_SCHEMA_RE from ..utils import GET_SCHEMA_RE
from ..utils import parse_bool
# Default our global support flag # Default our global support flag
NOTIFY_DBUS_SUPPORT_ENABLED = False NOTIFY_DBUS_SUPPORT_ENABLED = False
@ -170,7 +171,8 @@ class NotifyDBus(NotifyBase):
# let me know! :) # let me know! :)
_enabled = NOTIFY_DBUS_SUPPORT_ENABLED _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 Initialize DBus Object
""" """
@ -184,13 +186,10 @@ class NotifyDBus(NotifyBase):
self.schema = kwargs.get('schema', 'dbus') self.schema = kwargs.get('schema', 'dbus')
if self.schema not in MAINLOOP_MAP: if self.schema not in MAINLOOP_MAP:
# Unsupported Schema msg = 'The schema specified ({}) is not supported.' \
self.logger.warning( .format(self.schema)
'The schema specified ({}) is not supported.' self.logger.warning(msg)
.format(self.schema)) raise TypeError(msg)
raise TypeError(
'The schema specified ({}) is not supported.'
.format(self.schema))
# The urgency of the message # The urgency of the message
if urgency not in DBUS_URGENCIES: if urgency not in DBUS_URGENCIES:
@ -200,8 +199,12 @@ class NotifyDBus(NotifyBase):
self.urgency = urgency self.urgency = urgency
# Our x/y axis settings # Our x/y axis settings
self.x_axis = x_axis self.x_axis = x_axis if isinstance(x_axis, int) else None
self.y_axis = y_axis 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): def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs):
""" """
@ -229,7 +232,8 @@ class NotifyDBus(NotifyBase):
) )
# image path # 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 # Our meta payload
meta_payload = { meta_payload = {
@ -241,7 +245,7 @@ class NotifyDBus(NotifyBase):
meta_payload['x'] = self.x_axis meta_payload['x'] = self.x_axis
meta_payload['y'] = self.y_axis meta_payload['y'] = self.y_axis
if NOTIFY_DBUS_IMAGE_SUPPORT is True: if NOTIFY_DBUS_IMAGE_SUPPORT and icon_path:
try: try:
# Use Pixbuf to create the proper image type # Use Pixbuf to create the proper image type
image = GdkPixbuf.Pixbuf.new_from_file(icon_path) image = GdkPixbuf.Pixbuf.new_from_file(icon_path)
@ -299,7 +303,33 @@ class NotifyDBus(NotifyBase):
Returns the URL built dynamically based on specified arguments. 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 @staticmethod
def parse_url(url): def parse_url(url):
@ -314,23 +344,58 @@ class NotifyDBus(NotifyBase):
# Content is simply not parseable # Content is simply not parseable
return None return None
# return a very basic set of requirements results = NotifyBase.parse_url(url)
return { if not results:
'schema': schema.group('schema').lower(), results = {
'user': None, 'schema': schema.group('schema').lower(),
'password': None, 'user': None,
'port': None, 'password': None,
'host': 'localhost', 'port': None,
'fullpath': None, 'host': '_',
'path': None, 'fullpath': None,
'url': url, 'path': None,
'qsd': {}, 'url': url,
# screen lat/lon (in pixels) where x=0 and y=0 if you want to put 'qsd': {},
# the notification in the top left hand side. Accept defaults if }
# set to None
'x_axis': None, # Include images with our message
'y_axis': None, results['include_image'] = \
# Set the urgency to None so that we fall back to the default parse_bool(results['qsd'].get('image', True))
# value.
'urgency': None, # 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

View File

@ -78,7 +78,7 @@ class NotifyDiscord(NotifyBase):
body_maxlen = 2000 body_maxlen = 2000
def __init__(self, webhook_id, webhook_token, tts=False, avatar=True, 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 Initialize Discord Object
@ -86,14 +86,14 @@ class NotifyDiscord(NotifyBase):
super(NotifyDiscord, self).__init__(**kwargs) super(NotifyDiscord, self).__init__(**kwargs)
if not webhook_id: if not webhook_id:
raise TypeError( msg = 'An invalid Client ID was specified.'
'An invalid Client ID was specified.' self.logger.warning(msg)
) raise TypeError(msg)
if not webhook_token: if not webhook_token:
raise TypeError( msg = 'An invalid Webhook Token was specified.'
'An invalid Webhook Token was specified.' self.logger.warning(msg)
) raise TypeError(msg)
# Store our data # Store our data
self.webhook_id = webhook_id self.webhook_id = webhook_id
@ -105,11 +105,14 @@ class NotifyDiscord(NotifyBase):
# Over-ride Avatar Icon # Over-ride Avatar Icon
self.avatar = avatar self.avatar = avatar
# Place a footer icon # Place a footer
self.footer = footer self.footer = footer
# include a footer_logo in footer
self.footer_logo = footer_logo
# Place a thumbnail image inline with the message body # Place a thumbnail image inline with the message body
self.thumbnail = thumbnail self.include_image = include_image
return return
@ -163,15 +166,18 @@ class NotifyDiscord(NotifyBase):
payload['embeds'][0]['fields'] = fields[1:] payload['embeds'][0]['fields'] = fields[1:]
if self.footer: if self.footer:
# Acquire logo URL
logo_url = self.image_url(notify_type, logo=True) logo_url = self.image_url(notify_type, logo=True)
# Set Footer text to our app description
payload['embeds'][0]['footer'] = { payload['embeds'][0]['footer'] = {
'text': self.app_desc, 'text': self.app_desc,
} }
if logo_url: if self.footer_logo and logo_url:
payload['embeds'][0]['footer']['icon_url'] = 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'] = { payload['embeds'][0]['thumbnail'] = {
'url': image_url, 'url': image_url,
'height': 256, 'height': 256,
@ -256,14 +262,15 @@ class NotifyDiscord(NotifyBase):
'tts': 'yes' if self.tts else 'no', 'tts': 'yes' if self.tts else 'no',
'avatar': 'yes' if self.avatar else 'no', 'avatar': 'yes' if self.avatar else 'no',
'footer': 'yes' if self.footer 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( return '{schema}://{webhook_id}/{webhook_token}/?{args}'.format(
schema=self.secure_protocol, schema=self.secure_protocol,
webhook_id=self.quote(self.webhook_id), webhook_id=NotifyDiscord.quote(self.webhook_id, safe=''),
webhook_token=self.quote(self.webhook_token), webhook_token=NotifyDiscord.quote(self.webhook_token, safe=''),
args=self.urlencode(args), args=NotifyDiscord.urlencode(args),
) )
@staticmethod @staticmethod
@ -283,14 +290,14 @@ class NotifyDiscord(NotifyBase):
return results return results
# Store our webhook ID # Store our webhook ID
webhook_id = results['host'] webhook_id = NotifyDiscord.unquote(results['host'])
# Now fetch our tokens # Now fetch our tokens
try: try:
webhook_token = [x for x in filter(bool, NotifyBase.split_path( webhook_token = \
results['fullpath']))][0] NotifyDiscord.split_path(results['fullpath'])[0]
except (ValueError, AttributeError, IndexError): except IndexError:
# Force some bad values that will get caught # Force some bad values that will get caught
# in parsing later # in parsing later
webhook_token = None webhook_token = None
@ -304,12 +311,27 @@ class NotifyDiscord(NotifyBase):
# Use Footer # Use Footer
results['footer'] = parse_bool(results['qsd'].get('footer', False)) 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 # Update Avatar Icon
results['avatar'] = parse_bool(results['qsd'].get('avatar', True)) results['avatar'] = parse_bool(results['qsd'].get('avatar', True))
# Use Thumbnail # Use Thumbnail
results['thumbnail'] = \ if 'thumbnail' in results['qsd']:
parse_bool(results['qsd'].get('thumbnail', False)) # 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 return results

View File

@ -450,13 +450,13 @@ class NotifyEmail(NotifyBase):
auth = '' auth = ''
if self.user and self.password: if self.user and self.password:
auth = '{user}:{password}@'.format( auth = '{user}:{password}@'.format(
user=self.quote(user, safe=''), user=NotifyEmail.quote(user, safe=''),
password=self.quote(self.password, safe=''), password=NotifyEmail.quote(self.password, safe=''),
) )
else: else:
# user url # user url
auth = '{user}@'.format( auth = '{user}@'.format(
user=self.quote(user, safe=''), user=NotifyEmail.quote(user, safe=''),
) )
# Default Port setup # Default Port setup
@ -466,10 +466,10 @@ class NotifyEmail(NotifyBase):
return '{schema}://{auth}{hostname}{port}/?{args}'.format( return '{schema}://{auth}{hostname}{port}/?{args}'.format(
schema=self.secure_protocol if self.secure else self.protocol, schema=self.secure_protocol if self.secure else self.protocol,
auth=auth, auth=auth,
hostname=self.host, hostname=NotifyEmail.quote(self.host, safe=''),
port='' if self.port is None or self.port == default_port port='' if self.port is None or self.port == default_port
else ':{}'.format(self.port), else ':{}'.format(self.port),
args=self.urlencode(args), args=NotifyEmail.urlencode(args),
) )
@staticmethod @staticmethod
@ -485,21 +485,28 @@ class NotifyEmail(NotifyBase):
# We're done early as we couldn't load the results # We're done early as we couldn't load the results
return results return results
# Apply our settings now # The To: address is pre-determined if to= is not otherwise
# specified.
to_addr = '' 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 = '' from_addr = ''
# The server we connect to to send our mail to
smtp_host = '' smtp_host = ''
# Attempt to detect 'from' email address # Attempt to detect 'from' email address
if 'from' in results['qsd'] and len(results['qsd']['from']): 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: else:
# get 'To' email address # get 'To' email address
from_addr = '%s@%s' % ( from_addr = '%s@%s' % (
re.split( re.split(
r'[\s@]+', NotifyBase.unquote(results['user']))[0], r'[\s@]+', NotifyEmail.unquote(results['user']))[0],
results.get('host', '') results.get('host', '')
) )
# Lets be clever and attempt to make the from # Lets be clever and attempt to make the from
@ -511,7 +518,7 @@ class NotifyEmail(NotifyBase):
# Attempt to detect 'to' email address # Attempt to detect 'to' email address
if 'to' in results['qsd'] and len(results['qsd']['to']): 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: if not to_addr:
# Send to ourselves if not otherwise specified to do so # 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']): if 'name' in results['qsd'] and len(results['qsd']['name']):
# Extract from name to associate with from address # 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']): if 'timeout' in results['qsd'] and len(results['qsd']['timeout']):
# Extract the timeout to associate with smtp server # Extract the timeout to associate with smtp server
@ -528,7 +535,7 @@ class NotifyEmail(NotifyBase):
# Store SMTP Host if specified # Store SMTP Host if specified
if 'smtp' in results['qsd'] and len(results['qsd']['smtp']): if 'smtp' in results['qsd'] and len(results['qsd']['smtp']):
# Extract the smtp server # 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']): if 'mode' in results['qsd'] and len(results['qsd']['mode']):
# Extract the secure mode to over-ride the default # Extract the secure mode to over-ride the default

View File

@ -96,9 +96,10 @@ class NotifyEmby(NotifyBase):
self.modal = modal self.modal = modal
if not self.user: if not self.user:
# Token was None # User was not specified
self.logger.warning('No Username was specified.') msg = 'No Username was specified.'
raise TypeError('No Username was specified.') self.logger.warning(msg)
raise TypeError(msg)
return return
@ -169,7 +170,7 @@ class NotifyEmby(NotifyBase):
if r.status_code != requests.codes.ok: if r.status_code != requests.codes.ok:
# We had a problem # We had a problem
status_str = \ status_str = \
NotifyBase.http_response_code_lookup(r.status_code) NotifyEmby.http_response_code_lookup(r.status_code)
self.logger.warning( self.logger.warning(
'Failed to authenticate Emby user {} details: ' 'Failed to authenticate Emby user {} details: '
@ -329,7 +330,7 @@ class NotifyEmby(NotifyBase):
if r.status_code != requests.codes.ok: if r.status_code != requests.codes.ok:
# We had a problem # We had a problem
status_str = \ status_str = \
NotifyBase.http_response_code_lookup(r.status_code) NotifyEmby.http_response_code_lookup(r.status_code)
self.logger.warning( self.logger.warning(
'Failed to acquire Emby session for user {}: ' 'Failed to acquire Emby session for user {}: '
@ -412,7 +413,7 @@ class NotifyEmby(NotifyBase):
# We had a problem # We had a problem
status_str = \ status_str = \
NotifyBase.http_response_code_lookup(r.status_code) NotifyEmby.http_response_code_lookup(r.status_code)
self.logger.warning( self.logger.warning(
'Failed to logoff Emby user {}: ' 'Failed to logoff Emby user {}: '
@ -508,7 +509,7 @@ class NotifyEmby(NotifyBase):
requests.codes.no_content): requests.codes.no_content):
# We had a problem # We had a problem
status_str = \ status_str = \
NotifyBase.http_response_code_lookup(r.status_code) NotifyEmby.http_response_code_lookup(r.status_code)
self.logger.warning( self.logger.warning(
'Failed to send Emby notification: ' 'Failed to send Emby notification: '
@ -555,21 +556,21 @@ class NotifyEmby(NotifyBase):
auth = '' auth = ''
if self.user and self.password: if self.user and self.password:
auth = '{user}:{password}@'.format( auth = '{user}:{password}@'.format(
user=self.quote(self.user, safe=''), user=NotifyEmby.quote(self.user, safe=''),
password=self.quote(self.password, safe=''), password=NotifyEmby.quote(self.password, safe=''),
) )
else: # self.user is set else: # self.user is set
auth = '{user}@'.format( auth = '{user}@'.format(
user=self.quote(self.user, safe=''), user=NotifyEmby.quote(self.user, safe=''),
) )
return '{schema}://{auth}{hostname}{port}/?{args}'.format( return '{schema}://{auth}{hostname}{port}/?{args}'.format(
schema=self.secure_protocol if self.secure else self.protocol, schema=self.secure_protocol if self.secure else self.protocol,
auth=auth, auth=auth,
hostname=self.host, hostname=NotifyEmby.quote(self.host, safe=''),
port='' if self.port is None or self.port == self.emby_default_port port='' if self.port is None or self.port == self.emby_default_port
else ':{}'.format(self.port), else ':{}'.format(self.port),
args=self.urlencode(args), args=NotifyEmby.urlencode(args),
) )
@property @property

View File

@ -27,6 +27,7 @@ import requests
from .NotifyBase import NotifyBase from .NotifyBase import NotifyBase
from ..common import NotifyImageSize from ..common import NotifyImageSize
from ..common import NotifyType from ..common import NotifyType
from ..utils import parse_bool
class NotifyFaast(NotifyBase): class NotifyFaast(NotifyBase):
@ -52,14 +53,18 @@ class NotifyFaast(NotifyBase):
# Allows the user to specify the NotifyImageSize object # Allows the user to specify the NotifyImageSize object
image_size = NotifyImageSize.XY_72 image_size = NotifyImageSize.XY_72
def __init__(self, authtoken, **kwargs): def __init__(self, authtoken, include_image=True, **kwargs):
""" """
Initialize Faast Object Initialize Faast Object
""" """
super(NotifyFaast, self).__init__(**kwargs) super(NotifyFaast, self).__init__(**kwargs)
# Store the Authentication Token
self.authtoken = authtoken self.authtoken = authtoken
# Associate an image with our post
self.include_image = include_image
def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs): def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs):
""" """
Perform Faast Notification Perform Faast Notification
@ -77,7 +82,10 @@ class NotifyFaast(NotifyBase):
'message': body, '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: if image_url:
payload['icon_url'] = image_url payload['icon_url'] = image_url
@ -99,7 +107,7 @@ class NotifyFaast(NotifyBase):
if r.status_code != requests.codes.ok: if r.status_code != requests.codes.ok:
# We had a problem # We had a problem
status_str = \ status_str = \
NotifyBase.http_response_code_lookup(r.status_code) NotifyFaast.http_response_code_lookup(r.status_code)
self.logger.warning( self.logger.warning(
'Failed to send Faast notification:' 'Failed to send Faast notification:'
@ -136,12 +144,13 @@ class NotifyFaast(NotifyBase):
args = { args = {
'format': self.notify_format, 'format': self.notify_format,
'overflow': self.overflow_mode, 'overflow': self.overflow_mode,
'image': 'yes' if self.include_image else 'no',
} }
return '{schema}://{authtoken}/?{args}'.format( return '{schema}://{authtoken}/?{args}'.format(
schema=self.protocol, schema=self.protocol,
authtoken=self.quote(self.authtoken, safe=''), authtoken=NotifyFaast.quote(self.authtoken, safe=''),
args=self.urlencode(args), args=NotifyFaast.urlencode(args),
) )
@staticmethod @staticmethod
@ -157,9 +166,11 @@ class NotifyFaast(NotifyBase):
# We're done early as we couldn't load the results # We're done early as we couldn't load the results
return results return results
# Apply our settings now
# Store our authtoken using the host # 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 return results

View File

@ -25,15 +25,17 @@
# To use this plugin, you need to first access https://dev.flock.com/webhooks # To use this plugin, you need to first access https://dev.flock.com/webhooks
# Specifically https://dev.flock.com/webhooks/incoming # 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 # 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: # recieve a URL that looks something like this:
# https://api.flock.com/hooks/sendMessage/134b8gh0-eba0-4fa9-ab9c-257ced0e8221 # https://api.flock.com/hooks/sendMessage/134b8gh0-eba0-4fa9-ab9c-257ced0e8221
# ^ # ^
# | # |
# This is important <----------------------------------------^ # This is important <----------------------------------------^
# #
# It becomes your 'token' that you will pass into this class
# #
import re import re
import requests import requests
@ -44,6 +46,7 @@ from ..common import NotifyType
from ..common import NotifyFormat from ..common import NotifyFormat
from ..common import NotifyImageSize from ..common import NotifyImageSize
from ..utils import parse_list from ..utils import parse_list
from ..utils import parse_bool
# Extend HTTP Error Messages # Extend HTTP Error Messages
@ -89,7 +92,7 @@ class NotifyFlock(NotifyBase):
# Allows the user to specify the NotifyImageSize object # Allows the user to specify the NotifyImageSize object
image_size = NotifyImageSize.XY_72 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 Initialize Flock Object
""" """
@ -134,6 +137,10 @@ class NotifyFlock(NotifyBase):
self.logger.warning(msg) self.logger.warning(msg)
raise TypeError(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): def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs):
""" """
Perform Flock Notification Perform Flock Notification
@ -151,8 +158,8 @@ class NotifyFlock(NotifyBase):
body = '<flockml>{}</flockml>'.format(body) body = '<flockml>{}</flockml>'.format(body)
else: else:
title = NotifyBase.escape_html(title, whitespace=False) title = NotifyFlock.escape_html(title, whitespace=False)
body = NotifyBase.escape_html(body, whitespace=False) body = NotifyFlock.escape_html(body, whitespace=False)
body = '<flockml>{}{}</flockml>'.format( body = '<flockml>{}{}</flockml>'.format(
'' if not title else '<b>{}</b><br/>'.format(title), body) '' if not title else '<b>{}</b><br/>'.format(title), body)
@ -162,7 +169,10 @@ class NotifyFlock(NotifyBase):
'flockml': body, 'flockml': body,
'sendAs': { 'sendAs': {
'name': FLOCK_DEFAULT_USER if not self.user else self.user, '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: if r.status_code != requests.codes.ok:
# We had a problem # We had a problem
status_str = \ status_str = \
NotifyBase.http_response_code_lookup( NotifyFlock.http_response_code_lookup(
r.status_code, FLOCK_HTTP_ERROR_MAP) r.status_code, FLOCK_HTTP_ERROR_MAP)
self.logger.warning( self.logger.warning(
@ -251,15 +261,17 @@ class NotifyFlock(NotifyBase):
args = { args = {
'format': self.notify_format, 'format': self.notify_format,
'overflow': self.overflow_mode, 'overflow': self.overflow_mode,
'image': 'yes' if self.include_image else 'no',
} }
return '{schema}://{token}/{targets}?{args}'\ return '{schema}://{token}/{targets}?{args}'\
.format( .format(
schema=self.secure_protocol, schema=self.secure_protocol,
token=self.quote(self.token, safe=''), token=NotifyFlock.quote(self.token, safe=''),
targets='/'.join( targets='/'.join(
[self.quote(target, safe='') for target in self.targets]), [NotifyFlock.quote(target, safe='')
args=self.urlencode(args), for target in self.targets]),
args=NotifyFlock.urlencode(args),
) )
@staticmethod @staticmethod
@ -274,12 +286,19 @@ class NotifyFlock(NotifyBase):
# We're done early as we couldn't load the results # We're done early as we couldn't load the results
return 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( # The 'to' makes it easier to use yaml configuration
bool, NotifyBase.split_path(results['fullpath']))] 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 # 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 return results

View File

@ -55,7 +55,7 @@ from ..utils import parse_bool
# API Gitter URL # API Gitter URL
GITTER_API_URL = 'https://api.gitter.im/v1' 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) VALIDATE_TOKEN = re.compile(r'^[a-z0-9]{40}$', re.I)
# Used to break path apart into list of targets # Used to break path apart into list of targets
@ -95,9 +95,11 @@ class NotifyGitter(NotifyBase):
# For Tracking Purposes # For Tracking Purposes
ratelimit_reset = datetime.utcnow() ratelimit_reset = datetime.utcnow()
# Default to 1 # Default to 1
ratelimit_remaining = 1 ratelimit_remaining = 1
# Default Notification Format
notify_format = NotifyFormat.MARKDOWN notify_format = NotifyFormat.MARKDOWN
def __init__(self, token, targets, include_image=True, **kwargs): def __init__(self, token, targets, include_image=True, **kwargs):
@ -107,7 +109,7 @@ class NotifyGitter(NotifyBase):
super(NotifyGitter, self).__init__(**kwargs) super(NotifyGitter, self).__init__(**kwargs)
try: try:
# The token associated with the account # The personal access token associated with the account
self.token = token.strip() self.token = token.strip()
except AttributeError: except AttributeError:
@ -117,7 +119,8 @@ class NotifyGitter(NotifyBase):
raise TypeError(msg) raise TypeError(msg)
if not VALIDATE_TOKEN.match(self.token): 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) self.logger.warning(msg)
raise TypeError(msg) raise TypeError(msg)
@ -140,10 +143,11 @@ class NotifyGitter(NotifyBase):
# error tracking (used for function return) # error tracking (used for function return)
has_error = False 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 image_url:
if self.include_image and image_url:
body = '![alt]({})\n{}'.format(image_url, body) body = '![alt]({})\n{}'.format(image_url, body)
# Create a copy of the targets list # Create a copy of the targets list
@ -288,7 +292,7 @@ class NotifyGitter(NotifyBase):
if r.status_code != requests.codes.ok: if r.status_code != requests.codes.ok:
# We had a problem # We had a problem
status_str = \ status_str = \
NotifyBase.http_response_code_lookup(r.status_code) NotifyGitter.http_response_code_lookup(r.status_code)
self.logger.warning( self.logger.warning(
'Failed to send Gitter POST to {}: ' 'Failed to send Gitter POST to {}: '
@ -342,14 +346,15 @@ class NotifyGitter(NotifyBase):
args = { args = {
'format': self.notify_format, 'format': self.notify_format,
'overflow': self.overflow_mode, 'overflow': self.overflow_mode,
'image': self.include_image, 'image': 'yes' if self.include_image else 'no',
} }
return '{schema}://{token}/{targets}/?{args}'.format( return '{schema}://{token}/{targets}/?{args}'.format(
schema=self.secure_protocol, schema=self.secure_protocol,
token=self.quote(self.token, safe=''), token=NotifyGitter.quote(self.token, safe=''),
targets='/'.join(self.targets), targets='/'.join(
args=self.urlencode(args)) [NotifyGitter.quote(x, safe='') for x in self.targets]),
args=NotifyGitter.urlencode(args))
@staticmethod @staticmethod
def parse_url(url): def parse_url(url):
@ -364,15 +369,16 @@ class NotifyGitter(NotifyBase):
# We're done early as we couldn't load the results # We're done early as we couldn't load the results
return results return results
results['token'] = results['host'] results['token'] = NotifyGitter.unquote(results['host'])
results['targets'] = \
[NotifyBase.unquote(x) for x in filter(bool, NotifyBase.split_path( # Get our entries; split_path() looks after unquoting content for us
results['fullpath']))] # by default
results['targets'] = NotifyGitter.split_path(results['fullpath'])
# Support the 'to' variable so that we can support targets this way too # Support the 'to' variable so that we can support targets this way too
# The 'to' makes it easier to use yaml configuration # The 'to' makes it easier to use yaml configuration
if 'to' in results['qsd'] and len(results['qsd']['to']): 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 # Include images with our message
results['include_image'] = \ results['include_image'] = \

View File

@ -29,6 +29,7 @@ from __future__ import print_function
from .NotifyBase import NotifyBase from .NotifyBase import NotifyBase
from ..common import NotifyImageSize from ..common import NotifyImageSize
from ..common import NotifyType from ..common import NotifyType
from ..utils import parse_bool
# Default our global support flag # Default our global support flag
NOTIFY_GNOME_SUPPORT_ENABLED = False NOTIFY_GNOME_SUPPORT_ENABLED = False
@ -109,7 +110,7 @@ class NotifyGnome(NotifyBase):
# let me know! :) # let me know! :)
_enabled = NOTIFY_GNOME_SUPPORT_ENABLED _enabled = NOTIFY_GNOME_SUPPORT_ENABLED
def __init__(self, urgency=None, **kwargs): def __init__(self, urgency=None, include_image=True, **kwargs):
""" """
Initialize Gnome Object Initialize Gnome Object
""" """
@ -123,6 +124,10 @@ class NotifyGnome(NotifyBase):
else: else:
self.urgency = urgency 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): def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs):
""" """
Perform Gnome Notification Perform Gnome Notification
@ -138,7 +143,8 @@ class NotifyGnome(NotifyBase):
Notify.init(self.app_id) Notify.init(self.app_id)
# image path # 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 # Build message body
notification = Notify.Notification.new(body) notification = Notify.Notification.new(body)
@ -149,18 +155,19 @@ class NotifyGnome(NotifyBase):
# Always call throttle before any remote server i/o is made # Always call throttle before any remote server i/o is made
self.throttle() self.throttle()
try: if icon_path:
# Use Pixbuf to create the proper image type try:
image = GdkPixbuf.Pixbuf.new_from_file(icon_path) # Use Pixbuf to create the proper image type
image = GdkPixbuf.Pixbuf.new_from_file(icon_path)
# Associate our image to our notification # Associate our image to our notification
notification.set_icon_from_pixbuf(image) notification.set_icon_from_pixbuf(image)
notification.set_image_from_pixbuf(image) notification.set_image_from_pixbuf(image)
except Exception as e: except Exception as e:
self.logger.warning( self.logger.warning(
"Could not load Gnome notification icon ({}): {}" "Could not load Gnome notification icon ({}): {}"
.format(icon_path, e)) .format(icon_path, e))
notification.show() notification.show()
self.logger.info('Sent Gnome notification.') self.logger.info('Sent Gnome notification.')
@ -177,7 +184,25 @@ class NotifyGnome(NotifyBase):
Returns the URL built dynamically based on specified arguments. 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 @staticmethod
def parse_url(url): def parse_url(url):
@ -188,18 +213,43 @@ class NotifyGnome(NotifyBase):
""" """
# return a very basic set of requirements results = NotifyBase.parse_url(url)
return { if not results:
'schema': NotifyGnome.protocol, results = {
'user': None, 'schema': NotifyGnome.protocol,
'password': None, 'user': None,
'port': None, 'password': None,
'host': 'localhost', 'port': None,
'fullpath': None, 'host': '_',
'path': None, 'fullpath': None,
'url': url, 'path': None,
'qsd': {}, 'url': url,
# Set the urgency to None so that we fall back to the default 'qsd': {},
# value. }
'urgency': None,
} # 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

View File

@ -103,7 +103,7 @@ class NotifyGotify(NotifyBase):
# Our access token does not get created until we first # Our access token does not get created until we first
# authenticate with our Gotify server. The same goes for the # authenticate with our Gotify server. The same goes for the
# user id below. # user id below.
self.access_token = token self.token = token
return return
@ -121,12 +121,12 @@ class NotifyGotify(NotifyBase):
# Define our parameteers # Define our parameteers
params = { params = {
'token': self.access_token, 'token': self.token,
} }
# Prepare Gotify Object # Prepare Gotify Object
payload = { payload = {
'priority': 2, 'priority': self.priority,
'title': title, 'title': title,
'message': body, 'message': body,
} }
@ -156,7 +156,7 @@ class NotifyGotify(NotifyBase):
if r.status_code != requests.codes.ok: if r.status_code != requests.codes.ok:
# We had a problem # We had a problem
status_str = \ status_str = \
NotifyBase.http_response_code_lookup(r.status_code) NotifyGotify.http_response_code_lookup(r.status_code)
self.logger.warning( self.logger.warning(
'Failed to send Gotify notification: ' 'Failed to send Gotify notification: '
@ -201,11 +201,11 @@ class NotifyGotify(NotifyBase):
return '{schema}://{hostname}{port}/{token}/?{args}'.format( return '{schema}://{hostname}{port}/{token}/?{args}'.format(
schema=self.secure_protocol if self.secure else self.protocol, 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 port='' if self.port is None or self.port == default_port
else ':{}'.format(self.port), else ':{}'.format(self.port),
token=self.access_token, token=NotifyGotify.quote(self.token, safe=''),
args=self.urlencode(args), args=NotifyGotify.urlencode(args),
) )
@staticmethod @staticmethod
@ -220,13 +220,17 @@ class NotifyGotify(NotifyBase):
# We're done early # We're done early
return results return results
# Retrieve our escaped entries found on the fullpath
entries = NotifyBase.split_path(results['fullpath'])
# optionally find the provider key # optionally find the provider key
try: try:
token = [x for x in filter( # The first entry is our token
bool, NotifyBase.split_path(results['fullpath']))][0] results['token'] = entries.pop(0)
except (AttributeError, IndexError): except IndexError:
token = None # No token was set
results['token'] = None
if 'priority' in results['qsd'] and len(results['qsd']['priority']): if 'priority' in results['qsd'] and len(results['qsd']['priority']):
_map = { _map = {
@ -244,7 +248,4 @@ class NotifyGotify(NotifyBase):
# No priority was set # No priority was set
pass pass
# Set our token
results['token'] = token
return results return results

View File

@ -28,6 +28,7 @@ from .gntp import errors
from ..NotifyBase import NotifyBase from ..NotifyBase import NotifyBase
from ...common import NotifyImageSize from ...common import NotifyImageSize
from ...common import NotifyType from ...common import NotifyType
from ...utils import parse_bool
# Priorities # Priorities
@ -86,7 +87,7 @@ class NotifyGrowl(NotifyBase):
# Default Growl Port # Default Growl Port
default_port = 23053 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 Initialize Growl Object
""" """
@ -129,28 +130,26 @@ class NotifyGrowl(NotifyBase):
) )
except errors.NetworkError: except errors.NetworkError:
self.logger.warning( msg = 'A network error occured sending Growl ' \
'A network error occured sending Growl ' 'notification to {}.'.format(self.host)
'notification to %s.' % self.host) self.logger.warning(msg)
raise TypeError( raise TypeError(msg)
'A network error occured sending Growl '
'notification to %s.' % self.host)
except errors.AuthError: except errors.AuthError:
self.logger.warning( msg = 'An authentication error occured sending Growl ' \
'An authentication error occured sending Growl ' 'notification to {}.'.format(self.host)
'notification to %s.' % self.host) self.logger.warning(msg)
raise TypeError( raise TypeError(msg)
'An authentication error occured sending Growl '
'notification to %s.' % self.host)
except errors.UnsupportedError: except errors.UnsupportedError:
self.logger.warning( msg = 'An unsupported error occured sending Growl ' \
'An unsupported error occured sending Growl ' 'notification to {}.'.format(self.host)
'notification to %s.' % self.host) self.logger.warning(msg)
raise TypeError( raise TypeError(msg)
'An unsupported error occured sending Growl '
'notification to %s.' % self.host) # Track whether or not we want to send an image with our notification
# or not.
self.include_image = include_image
return return
@ -162,11 +161,13 @@ class NotifyGrowl(NotifyBase):
icon = None icon = None
if self.version >= 2: if self.version >= 2:
# URL Based # URL Based
icon = self.image_url(notify_type) icon = None if not self.include_image \
else self.image_url(notify_type)
else: else:
# Raw # Raw
icon = self.image_raw(notify_type) icon = None if not self.include_image \
else self.image_raw(notify_type)
payload = { payload = {
'noteType': GROWL_NOTIFICATION_TYPE, 'noteType': GROWL_NOTIFICATION_TYPE,
@ -232,6 +233,7 @@ class NotifyGrowl(NotifyBase):
args = { args = {
'format': self.notify_format, 'format': self.notify_format,
'overflow': self.overflow_mode, 'overflow': self.overflow_mode,
'image': 'yes' if self.include_image else 'no',
'priority': 'priority':
_map[GrowlPriority.NORMAL] if self.priority not in _map _map[GrowlPriority.NORMAL] if self.priority not in _map
else _map[self.priority], else _map[self.priority],
@ -239,18 +241,19 @@ class NotifyGrowl(NotifyBase):
} }
auth = '' auth = ''
if self.password: if self.user:
# The growl password is stored in the user field
auth = '{password}@'.format( auth = '{password}@'.format(
password=self.quote(self.user, safe=''), password=NotifyGrowl.quote(self.user, safe=''),
) )
return '{schema}://{auth}{hostname}{port}/?{args}'.format( return '{schema}://{auth}{hostname}{port}/?{args}'.format(
schema=self.secure_protocol if self.secure else self.protocol, schema=self.secure_protocol if self.secure else self.protocol,
auth=auth, auth=auth,
hostname=self.host, hostname=NotifyGrowl.quote(self.host, safe=''),
port='' if self.port is None or self.port == self.default_port port='' if self.port is None or self.port == self.default_port
else ':{}'.format(self.port), else ':{}'.format(self.port),
args=self.urlencode(args), args=NotifyGrowl.urlencode(args),
) )
@staticmethod @staticmethod
@ -272,11 +275,11 @@ class NotifyGrowl(NotifyBase):
# Allow the user to specify the version of the protocol to use. # Allow the user to specify the version of the protocol to use.
try: try:
version = int( version = int(
NotifyBase.unquote( NotifyGrowl.unquote(
results['qsd']['version']).strip().split('.')[0]) results['qsd']['version']).strip().split('.')[0])
except (AttributeError, IndexError, TypeError, ValueError): except (AttributeError, IndexError, TypeError, ValueError):
NotifyBase.logger.warning( NotifyGrowl.logger.warning(
'An invalid Growl version of "%s" was specified and will ' 'An invalid Growl version of "%s" was specified and will '
'be ignored.' % results['qsd']['version'] 'be ignored.' % results['qsd']['version']
) )
@ -306,6 +309,11 @@ class NotifyGrowl(NotifyBase):
if results.get('password', None) is None: if results.get('password', None) is None:
results['password'] = results.get('user', 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: if version:
results['version'] = version results['version'] = version

View File

@ -108,14 +108,17 @@ class NotifyIFTTT(NotifyBase):
super(NotifyIFTTT, self).__init__(**kwargs) super(NotifyIFTTT, self).__init__(**kwargs)
if not webhook_id: 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 # Store our Events we wish to trigger
self.events = parse_list(events) self.events = parse_list(events)
if not self.events: if not self.events:
raise TypeError( msg = 'You must specify at least one event you wish to trigger on.'
'You must specify at least one event you wish to trigger on.') self.logger.warning(msg)
raise TypeError(msg)
# Store our APIKey # Store our APIKey
self.webhook_id = webhook_id self.webhook_id = webhook_id
@ -132,9 +135,10 @@ class NotifyIFTTT(NotifyBase):
self.del_tokens = del_tokens self.del_tokens = del_tokens
else: else:
raise TypeError( msg = 'del_token must be a list; {} was provided'.format(
'del_token must be a list; {} was provided'.format( str(type(del_tokens)))
str(type(del_tokens)))) self.logger.warning(msg)
raise TypeError(msg)
def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs): def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs):
""" """
@ -202,7 +206,7 @@ class NotifyIFTTT(NotifyBase):
if r.status_code != requests.codes.ok: if r.status_code != requests.codes.ok:
# We had a problem # We had a problem
status_str = \ status_str = \
NotifyBase.http_response_code_lookup(r.status_code) NotifyIFTTT.http_response_code_lookup(r.status_code)
self.logger.warning( self.logger.warning(
'Failed to send IFTTT notification to {}: ' 'Failed to send IFTTT notification to {}: '
@ -253,9 +257,10 @@ class NotifyIFTTT(NotifyBase):
return '{schema}://{webhook_id}@{events}/?{args}'.format( return '{schema}://{webhook_id}@{events}/?{args}'.format(
schema=self.secure_protocol, schema=self.secure_protocol,
webhook_id=self.webhook_id, webhook_id=NotifyIFTTT.quote(self.webhook_id, safe=''),
events='/'.join([self.quote(x, safe='') for x in self.events]), events='/'.join([NotifyIFTTT.quote(x, safe='')
args=self.urlencode(args), for x in self.events]),
args=NotifyIFTTT.urlencode(args),
) )
@staticmethod @staticmethod
@ -271,15 +276,26 @@ class NotifyIFTTT(NotifyBase):
# We're done early as we couldn't load the results # We're done early as we couldn't load the results
return 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 # Our Event
results['events'] = list() results['events'] = list()
results['events'].append(results['host']) if results['user']:
# If a user was defined, then the hostname is actually a event
# Our API Key # too
results['webhook_id'] = results['user'] results['events'].append(NotifyIFTTT.unquote(results['host']))
# Now fetch the remaining tokens # Now fetch the remaining tokens
results['events'].extend([x for x in filter( results['events'].extend(NotifyIFTTT.split_path(results['fullpath']))
bool, NotifyBase.split_path(results['fullpath']))][0:])
# 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 return results

View File

@ -56,7 +56,7 @@ class NotifyJSON(NotifyBase):
# local anyway # local anyway
request_rate_per_sec = 0 request_rate_per_sec = 0
def __init__(self, headers, **kwargs): def __init__(self, headers=None, **kwargs):
""" """
Initialize JSON Object Initialize JSON Object
@ -66,12 +66,6 @@ class NotifyJSON(NotifyBase):
""" """
super(NotifyJSON, self).__init__(**kwargs) super(NotifyJSON, self).__init__(**kwargs)
if self.secure:
self.schema = 'https'
else:
self.schema = 'http'
self.fullpath = kwargs.get('fullpath') self.fullpath = kwargs.get('fullpath')
if not isinstance(self.fullpath, six.string_types): if not isinstance(self.fullpath, six.string_types):
self.fullpath = '/' self.fullpath = '/'
@ -101,12 +95,12 @@ class NotifyJSON(NotifyBase):
auth = '' auth = ''
if self.user and self.password: if self.user and self.password:
auth = '{user}:{password}@'.format( auth = '{user}:{password}@'.format(
user=self.quote(self.user, safe=''), user=NotifyJSON.quote(self.user, safe=''),
password=self.quote(self.password, safe=''), password=NotifyJSON.quote(self.password, safe=''),
) )
elif self.user: elif self.user:
auth = '{user}@'.format( auth = '{user}@'.format(
user=self.quote(self.user, safe=''), user=NotifyJSON.quote(self.user, safe=''),
) )
default_port = 443 if self.secure else 80 default_port = 443 if self.secure else 80
@ -114,10 +108,10 @@ class NotifyJSON(NotifyBase):
return '{schema}://{auth}{hostname}{port}/?{args}'.format( return '{schema}://{auth}{hostname}{port}/?{args}'.format(
schema=self.secure_protocol if self.secure else self.protocol, schema=self.secure_protocol if self.secure else self.protocol,
auth=auth, auth=auth,
hostname=self.host, hostname=NotifyJSON.quote(self.host, safe=''),
port='' if self.port is None or self.port == default_port port='' if self.port is None or self.port == default_port
else ':{}'.format(self.port), else ':{}'.format(self.port),
args=self.urlencode(args), args=NotifyJSON.urlencode(args),
) )
def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs): def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs):
@ -148,7 +142,10 @@ class NotifyJSON(NotifyBase):
if self.user: if self.user:
auth = (self.user, self.password) 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): if isinstance(self.port, int):
url += ':%d' % self.port url += ':%d' % self.port
@ -173,7 +170,7 @@ class NotifyJSON(NotifyBase):
if r.status_code != requests.codes.ok: if r.status_code != requests.codes.ok:
# We had a problem # We had a problem
status_str = \ status_str = \
NotifyBase.http_response_code_lookup(r.status_code) NotifyJSON.http_response_code_lookup(r.status_code)
self.logger.warning( self.logger.warning(
'Failed to send JSON notification: ' 'Failed to send JSON notification: '
@ -219,4 +216,8 @@ class NotifyJSON(NotifyBase):
results['headers'] = results['qsd-'] results['headers'] = results['qsd-']
results['headers'].update(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 return results

View File

@ -34,12 +34,13 @@
# https://play.google.com/store/apps/details?id=com.joaomgcd.join # https://play.google.com/store/apps/details?id=com.joaomgcd.join
import re import re
import six
import requests import requests
from .NotifyBase import NotifyBase from .NotifyBase import NotifyBase
from ..common import NotifyImageSize from ..common import NotifyImageSize
from ..common import NotifyType from ..common import NotifyType
from ..utils import parse_list
from ..utils import parse_bool
# Token required as part of the API request # Token required as part of the API request
VALIDATE_APIKEY = re.compile(r'[A-Za-z0-9]{32}') VALIDATE_APIKEY = re.compile(r'[A-Za-z0-9]{32}')
@ -49,9 +50,6 @@ JOIN_HTTP_ERROR_MAP = {
401: 'Unauthorized - Invalid Token.', 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 # Used to detect a device
IS_DEVICE_RE = re.compile(r'([A-Za-z0-9]{32})') 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 # The default group to use if none is specified
default_join_group = 'group.all' default_join_group = 'group.all'
def __init__(self, apikey, devices, **kwargs): def __init__(self, apikey, targets, include_image=True, **kwargs):
""" """
Initialize Join Object Initialize Join Object
""" """
super(NotifyJoin, self).__init__(**kwargs) super(NotifyJoin, self).__init__(**kwargs)
if not VALIDATE_APIKEY.match(apikey.strip()): if not VALIDATE_APIKEY.match(apikey.strip()):
self.logger.warning( msg = 'The JOIN API Token specified ({}) is invalid.'\
'The first API Token specified (%s) is invalid.' % apikey, .format(apikey)
) self.logger.warning(msg)
raise TypeError(msg)
raise TypeError(
'The first API Token specified (%s) is invalid.' % apikey,
)
# The token associated with the account # The token associated with the account
self.apikey = apikey.strip() self.apikey = apikey.strip()
if isinstance(devices, six.string_types): # Parse devices specified
self.devices = [x for x in filter(bool, DEVICE_LIST_DELIM.split( self.devices = parse_list(targets)
devices,
))]
elif isinstance(devices, (set, tuple, list)):
self.devices = devices
else:
self.devices = list()
if len(self.devices) == 0: if len(self.devices) == 0:
# Default to everyone # Default to everyone
self.devices.append(self.default_join_group) 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): def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs):
""" """
Perform Join Notification Perform Join Notification
@ -151,13 +142,12 @@ class NotifyJoin(NotifyBase):
device = devices.pop(0) device = devices.pop(0)
group_re = IS_GROUP_RE.match(device) group_re = IS_GROUP_RE.match(device)
if group_re: 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): elif not IS_DEVICE_RE.match(device):
self.logger.warning( self.logger.warning(
"The specified device/group '%s' is invalid; skipping." % ( 'Skipping specified invalid device/group "{}"'
device, .format(device)
)
) )
# Mark our failure # Mark our failure
has_error = True has_error = True
@ -170,7 +160,10 @@ class NotifyJoin(NotifyBase):
'text': body, '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: if image_url:
url_args['icon'] = image_url url_args['icon'] = image_url
@ -178,7 +171,7 @@ class NotifyJoin(NotifyBase):
payload = {} payload = {}
# Prepare the URL # 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)' % ( self.logger.debug('Join POST URL: %s (cert_verify=%r)' % (
url, self.verify_certificate, url, self.verify_certificate,
@ -199,7 +192,7 @@ class NotifyJoin(NotifyBase):
if r.status_code != requests.codes.ok: if r.status_code != requests.codes.ok:
# We had a problem # We had a problem
status_str = \ status_str = \
NotifyBase.http_response_code_lookup( NotifyJoin.http_response_code_lookup(
r.status_code, JOIN_HTTP_ERROR_MAP) r.status_code, JOIN_HTTP_ERROR_MAP)
self.logger.warning( self.logger.warning(
@ -242,13 +235,15 @@ class NotifyJoin(NotifyBase):
args = { args = {
'format': self.notify_format, 'format': self.notify_format,
'overflow': self.overflow_mode, 'overflow': self.overflow_mode,
'image': 'yes' if self.include_image else 'no',
} }
return '{schema}://{apikey}/{devices}/?{args}'.format( return '{schema}://{apikey}/{devices}/?{args}'.format(
schema=self.secure_protocol, schema=self.secure_protocol,
apikey=self.quote(self.apikey, safe=''), apikey=NotifyJoin.quote(self.apikey, safe=''),
devices='/'.join([self.quote(x) for x in self.devices]), devices='/'.join([NotifyJoin.quote(x, safe='')
args=self.urlencode(args)) for x in self.devices]),
args=NotifyJoin.urlencode(args))
@staticmethod @staticmethod
def parse_url(url): def parse_url(url):
@ -263,11 +258,30 @@ class NotifyJoin(NotifyBase):
# We're done early as we couldn't load the results # We're done early as we couldn't load the results
return results return results
# Apply our settings now # Our API Key is the hostname if no user is specified
devices = ' '.join( results['apikey'] = \
filter(bool, NotifyBase.split_path(results['fullpath']))) results['user'] if results['user'] else results['host']
results['apikey'] = results['host'] # Unquote our API Key
results['devices'] = devices 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 return results

View File

@ -39,6 +39,7 @@ from ..common import NotifyType
from ..common import NotifyImageSize from ..common import NotifyImageSize
from ..common import NotifyFormat from ..common import NotifyFormat
from ..utils import parse_bool from ..utils import parse_bool
from ..utils import parse_list
# Define default path # Define default path
MATRIX_V2_API_PATH = '/_matrix/client/r0' MATRIX_V2_API_PATH = '/_matrix/client/r0'
@ -50,10 +51,6 @@ MATRIX_HTTP_ERROR_MAP = {
429: 'Rate limit imposed; wait 2s and try again', 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 # Matrix Room Syntax
IS_ROOM_ALIAS = re.compile( IS_ROOM_ALIAS = re.compile(
r'^\s*(#|%23)?(?P<room>[a-z0-9-]+)((:|%3A)' r'^\s*(#|%23)?(?P<room>[a-z0-9-]+)((:|%3A)'
@ -120,30 +117,15 @@ class NotifyMatrix(NotifyBase):
# the server doesn't remind us how long we shoul wait for # the server doesn't remind us how long we shoul wait for
default_wait_ms = 1000 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 Initialize Matrix Object
""" """
super(NotifyMatrix, self).__init__(**kwargs) super(NotifyMatrix, self).__init__(**kwargs)
# Prepare a list of rooms to connect and notify # Prepare a list of rooms to connect and notify
if isinstance(rooms, six.string_types): self.rooms = parse_list(targets)
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)
# our home server gets populated after a login/registration # our home server gets populated after a login/registration
self.home_server = None self.home_server = None
@ -154,23 +136,31 @@ class NotifyMatrix(NotifyBase):
# This gets initialized after a login/registration # This gets initialized after a login/registration
self.access_token = None self.access_token = None
# Place a thumbnail image inline with the message body # Place an image inline with the message body
self.thumbnail = thumbnail self.include_image = include_image
# maintain a lookup of room alias's we already paired with their id # maintain a lookup of room alias's we already paired with their id
# to speed up future requests # to speed up future requests
self._room_cache = {} 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): def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs):
""" """
Perform Matrix Notification Perform Matrix Notification
""" """
# Call the _send_ function applicable to whatever mode we're in # Call the _send_ function applicable to whatever mode we're in
# - calls _send_webhook_notification if the webhook variable is set # - calls _send_webhook_notification if the mode variable is set
# - calls _send_server_notification if the webhook variable is not set # - calls _send_server_notification if the mode variable is not set
return getattr(self, '_send_{}_notification'.format( 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) body=body, title=title, notify_type=notify_type, **kwargs)
def _send_webhook_notification(self, body, title='', def _send_webhook_notification(self, body, title='',
@ -200,7 +190,7 @@ class NotifyMatrix(NotifyBase):
) )
# Retrieve our payload # 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) body=body, title=title, notify_type=notify_type, **kwargs)
self.logger.debug('Matrix POST URL: %s (cert_verify=%r)' % ( self.logger.debug('Matrix POST URL: %s (cert_verify=%r)' % (
@ -221,7 +211,7 @@ class NotifyMatrix(NotifyBase):
if r.status_code != requests.codes.ok: if r.status_code != requests.codes.ok:
# We had a problem # We had a problem
status_str = \ status_str = \
NotifyBase.http_response_code_lookup( NotifyMatrix.http_response_code_lookup(
r.status_code, MATRIX_HTTP_ERROR_MAP) r.status_code, MATRIX_HTTP_ERROR_MAP)
self.logger.warning( self.logger.warning(
@ -318,8 +308,8 @@ class NotifyMatrix(NotifyBase):
else: # TEXT or MARKDOWN else: # TEXT or MARKDOWN
# Ensure our content is escaped # Ensure our content is escaped
title = NotifyBase.escape_html(title) title = NotifyMatrix.escape_html(title)
body = NotifyBase.escape_html(body) body = NotifyMatrix.escape_html(body)
payload['text'] = '{}{}'.format( payload['text'] = '{}{}'.format(
'' if not title else '<h4>{}</h4>'.format(title), body) '' if not title else '<h4>{}</h4>'.format(title), body)
@ -375,8 +365,11 @@ class NotifyMatrix(NotifyBase):
title='' if not title else '{}\r\n'.format(title), title='' if not title else '{}\r\n'.format(title),
body=body) body=body)
image_url = self.image_url(notify_type) # Acquire our image url if we're configured to do so
if self.thumbnail and image_url: image_url = None if not self.include_image else \
self.image_url(notify_type)
if image_url:
# Define our payload # Define our payload
image_payload = { image_payload = {
'msgtype': 'm.image', 'msgtype': 'm.image',
@ -385,7 +378,7 @@ class NotifyMatrix(NotifyBase):
} }
# Build our path # Build our path
path = '/rooms/{}/send/m.room.message'.format( path = '/rooms/{}/send/m.room.message'.format(
NotifyBase.quote(room_id)) NotifyMatrix.quote(room_id))
# Post our content # Post our content
postokay, response = self._fetch(path, payload=image_payload) postokay, response = self._fetch(path, payload=image_payload)
@ -402,7 +395,7 @@ class NotifyMatrix(NotifyBase):
# Build our path # Build our path
path = '/rooms/{}/send/m.room.message'.format( path = '/rooms/{}/send/m.room.message'.format(
NotifyBase.quote(room_id)) NotifyMatrix.quote(room_id))
# Post our content # Post our content
postokay, response = self._fetch(path, payload=payload) postokay, response = self._fetch(path, payload=payload)
@ -446,7 +439,7 @@ class NotifyMatrix(NotifyBase):
# Register # Register
postokay, response = \ postokay, response = \
self._fetch('/register', payload=payload, params=params) self._fetch('/register', payload=payload, params=params)
if not postokay: if not (postokay and isinstance(response, dict)):
# Failed to register # Failed to register
return False return False
@ -489,7 +482,7 @@ class NotifyMatrix(NotifyBase):
# Build our URL # Build our URL
postokay, response = self._fetch('/login', payload=payload) postokay, response = self._fetch('/login', payload=payload)
if not postokay: if not (postokay and isinstance(response, dict)):
# Failed to login # Failed to login
return False return False
@ -581,7 +574,7 @@ class NotifyMatrix(NotifyBase):
) )
# Build our URL # Build our URL
path = '/join/{}'.format(NotifyBase.quote(room_id)) path = '/join/{}'.format(NotifyMatrix.quote(room_id))
# Make our query # Make our query
postokay, _ = self._fetch(path, payload=payload) postokay, _ = self._fetch(path, payload=payload)
@ -612,7 +605,7 @@ class NotifyMatrix(NotifyBase):
# If we reach here, we need to join the channel # If we reach here, we need to join the channel
# Build our URL # Build our URL
path = '/join/{}'.format(NotifyBase.quote(room)) path = '/join/{}'.format(NotifyMatrix.quote(room))
# Attempt to join the channel # Attempt to join the channel
postokay, response = self._fetch(path, payload=payload) postokay, response = self._fetch(path, payload=payload)
@ -695,7 +688,7 @@ class NotifyMatrix(NotifyBase):
return list() return list()
postokay, response = self._fetch( postokay, response = self._fetch(
'/joined_rooms', payload=None, fn=requests.get) '/joined_rooms', payload=None, method='GET')
if not postokay: if not postokay:
# Failed to retrieve listings # Failed to retrieve listings
return list() return list()
@ -736,14 +729,14 @@ class NotifyMatrix(NotifyBase):
# Make our request # Make our request
postokay, response = self._fetch( postokay, response = self._fetch(
"/directory/room/{}".format( "/directory/room/{}".format(
self.quote(room)), payload=None, fn=requests.get) NotifyMatrix.quote(room)), payload=None, method='GET')
if postokay: if postokay:
return response.get("room_id") return response.get("room_id")
return None 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 Wrapper to request.post() to manage it's response better and make
the send() function cleaner and easier to maintain. the send() function cleaner and easier to maintain.
@ -775,6 +768,9 @@ class NotifyMatrix(NotifyBase):
# Our response object # Our response object
response = {} 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 # Define how many attempts we'll make if we get caught in a throttle
# event # event
retries = self.default_retries if self.default_retries > 0 else 1 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)) self.logger.debug('Matrix Payload: %s' % str(payload))
try: try:
r = requests.post( r = fn(
url, url,
data=dumps(payload), data=dumps(payload),
params=params, params=params,
@ -826,7 +822,7 @@ class NotifyMatrix(NotifyBase):
elif r.status_code != requests.codes.ok: elif r.status_code != requests.codes.ok:
# We had a problem # We had a problem
status_str = \ status_str = \
NotifyBase.http_response_code_lookup( NotifyMatrix.http_response_code_lookup(
r.status_code, MATRIX_HTTP_ERROR_MAP) r.status_code, MATRIX_HTTP_ERROR_MAP)
self.logger.warning( self.logger.warning(
@ -877,22 +873,23 @@ class NotifyMatrix(NotifyBase):
args = { args = {
'format': self.notify_format, 'format': self.notify_format,
'overflow': self.overflow_mode, 'overflow': self.overflow_mode,
'image': 'yes' if self.include_image else 'no',
} }
if self.webhook: if self.mode:
args['webhook'] = self.webhook args['mode'] = self.mode
# Determine Authentication method # Determine Authentication
auth = '' auth = ''
if self.user and self.password: if self.user and self.password:
auth = '{user}:{password}@'.format( auth = '{user}:{password}@'.format(
user=self.quote(self.user, safe=''), user=NotifyMatrix.quote(self.user, safe=''),
password=self.quote(self.password, safe=''), password=NotifyMatrix.quote(self.password, safe=''),
) )
elif self.user: elif self.user:
auth = '{user}@'.format( auth = '{user}@'.format(
user=self.quote(self.user, safe=''), user=NotifyMatrix.quote(self.user, safe=''),
) )
default_port = 443 if self.secure else 80 default_port = 443 if self.secure else 80
@ -900,11 +897,11 @@ class NotifyMatrix(NotifyBase):
return '{schema}://{auth}{hostname}{port}/{rooms}?{args}'.format( return '{schema}://{auth}{hostname}{port}/{rooms}?{args}'.format(
schema=self.secure_protocol if self.secure else self.protocol, schema=self.secure_protocol if self.secure else self.protocol,
auth=auth, auth=auth,
hostname=self.host, hostname=NotifyMatrix.quote(self.host, safe=''),
port='' if self.port is None port='' if self.port is None
or self.port == default_port else ':{}'.format(self.port), or self.port == default_port else ':{}'.format(self.port),
rooms=self.quote('/'.join(self.rooms)), rooms=NotifyMatrix.quote('/'.join(self.rooms)),
args=self.urlencode(args), args=NotifyMatrix.urlencode(args),
) )
@staticmethod @staticmethod
@ -921,15 +918,40 @@ class NotifyMatrix(NotifyBase):
return results return results
# Get our rooms # Get our rooms
results['rooms'] = [ results['targets'] = NotifyMatrix.split_path(results['fullpath'])
NotifyBase.unquote(x) for x in filter(bool, NotifyBase.split_path(
results['fullpath']))][0:]
# Use Thumbnail # Support the 'to' variable so that we can support rooms this way too
results['thumbnail'] = \ # The 'to' makes it easier to use yaml configuration
parse_bool(results['qsd'].get('thumbnail', False)) if 'to' in results['qsd'] and len(results['qsd']['to']):
results['targets'] += NotifyMatrix.parse_list(results['qsd']['to'])
# Webhook # Thumbnail (old way)
results['webhook'] = results['qsd'].get('webhook') 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 return results

View File

@ -30,6 +30,8 @@ from json import dumps
from .NotifyBase import NotifyBase from .NotifyBase import NotifyBase
from ..common import NotifyImageSize from ..common import NotifyImageSize
from ..common import NotifyType from ..common import NotifyType
from ..utils import parse_bool
from ..utils import parse_list
# Some Reference Locations: # Some Reference Locations:
# - https://docs.mattermost.com/developer/webhooks-incoming.html # - https://docs.mattermost.com/developer/webhooks-incoming.html
@ -71,7 +73,8 @@ class NotifyMatterMost(NotifyBase):
# Mattermost does not have a title # Mattermost does not have a title
title_maxlen = 0 title_maxlen = 0
def __init__(self, authtoken, channel=None, **kwargs): def __init__(self, authtoken, channels=None, include_image=True,
**kwargs):
""" """
Initialize MatterMost Object Initialize MatterMost Object
""" """
@ -88,27 +91,24 @@ class NotifyMatterMost(NotifyBase):
# Validate authtoken # Validate authtoken
if not authtoken: if not authtoken:
self.logger.warning( msg = 'Missing MatterMost Authorization Token.'
'Missing MatterMost Authorization Token.' self.logger.warning(msg)
) raise TypeError(msg)
raise TypeError(
'Missing MatterMost Authorization Token.'
)
if not VALIDATE_AUTHTOKEN.match(authtoken): if not VALIDATE_AUTHTOKEN.match(authtoken):
self.logger.warning( msg = 'Invalid MatterMost Authorization Token Specified.'
'Invalid MatterMost Authorization Token Specified.' self.logger.warning(msg)
) raise TypeError(msg)
raise TypeError(
'Invalid MatterMost Authorization Token Specified.'
)
# A Channel (optional) # Optional Channels
self.channel = channel self.channels = parse_list(channels)
if not self.port: if not self.port:
self.port = self.default_port self.port = self.default_port
# Place a thumbnail image inline with the message body
self.include_image = include_image
return return
def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs): def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs):
@ -116,6 +116,9 @@ class NotifyMatterMost(NotifyBase):
Perform MatterMost Notification Perform MatterMost Notification
""" """
# Create a copy of our channels, otherwise place a dummy entry
channels = list(self.channels) if self.channels else [None, ]
headers = { headers = {
'User-Agent': self.app_id, 'User-Agent': self.app_id,
'Content-Type': 'application/json' 'Content-Type': 'application/json'
@ -124,67 +127,91 @@ class NotifyMatterMost(NotifyBase):
# prepare JSON Object # prepare JSON Object
payload = { payload = {
'text': body, 'text': body,
'icon_url': self.image_url(notify_type), 'icon_url': None,
} }
if self.user: # Acquire our image url if configured to do so
payload['username'] = self.user image_url = None if not self.include_image \
else self.image_url(notify_type)
else: if image_url:
payload['username'] = self.app_id # Set our image configuration if told to do so
payload['icon_url'] = image_url
if self.channel: # Set our user
payload['channel'] = self.channel payload['username'] = self.user if self.user else self.app_id
url = '%s://%s:%d' % (self.schema, self.host, self.port) # For error tracking
url += '/hooks/%s' % self.authtoken has_error = False
self.logger.debug('MatterMost POST URL: %s (cert_verify=%r)' % ( while len(channels):
url, self.verify_certificate, # Pop a channel off of the list
)) channel = channels.pop(0)
self.logger.debug('MatterMost Payload: %s' % str(payload))
# Always call throttle before any remote server i/o is made if channel:
self.throttle() payload['channel'] = channel
try: url = '%s://%s:%d' % (self.schema, self.host, self.port)
r = requests.post( url += '/hooks/%s' % self.authtoken
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)
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( self.logger.warning(
'Failed to send MatterMost notification: ' 'A Connection error occured sending MatterMost '
'{}{}error={}.'.format( 'notification{}.'.format(
status_str, '' if not channel
', ' if status_str else '', else ' to channel {}'.format(channel)))
r.status_code)) 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 our overall status
return False return not has_error
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
def url(self): def url(self):
""" """
@ -195,18 +222,25 @@ class NotifyMatterMost(NotifyBase):
args = { args = {
'format': self.notify_format, 'format': self.notify_format,
'overflow': self.overflow_mode, '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_port = 443 if self.secure else self.default_port
default_schema = self.secure_protocol if self.secure else self.protocol default_schema = self.secure_protocol if self.secure else self.protocol
return '{schema}://{hostname}{port}/{authtoken}/?{args}'.format( return '{schema}://{hostname}{port}/{authtoken}/?{args}'.format(
schema=default_schema, schema=default_schema,
hostname=self.host, hostname=NotifyMatterMost.quote(self.host, safe=''),
port='' if not self.port or self.port == default_port port='' if not self.port or self.port == default_port
else ':{}'.format(self.port), else ':{}'.format(self.port),
authtoken=self.quote(self.authtoken, safe=''), authtoken=NotifyMatterMost.quote(self.authtoken, safe=''),
args=self.urlencode(args), args=NotifyMatterMost.urlencode(args),
) )
@staticmethod @staticmethod
@ -222,15 +256,31 @@ class NotifyMatterMost(NotifyBase):
# We're done early as we couldn't load the results # We're done early as we couldn't load the results
return results return results
# Apply our settings now try:
authtoken = NotifyBase.split_path(results['fullpath'])[0] # 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']): if 'channel' in results['qsd'] and len(results['qsd']['channel']):
# Allow the user to specify the channel to post to # 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 # Image manipulation
results['channel'] = channel results['include_image'] = \
parse_bool(results['qsd'].get('image', False))
return results return results

View File

@ -103,12 +103,9 @@ class NotifyProwl(NotifyBase):
self.priority = priority self.priority = priority
if not VALIDATE_APIKEY.match(apikey): if not VALIDATE_APIKEY.match(apikey):
self.logger.warning( msg = 'The API key specified ({}) is invalid.'.format(apikey)
'The API key specified (%s) is invalid.' % apikey, self.logger.warning(msg)
) raise TypeError(msg)
raise TypeError(
'The API key specified (%s) is invalid.' % apikey,
)
# Store the API key # Store the API key
self.apikey = apikey self.apikey = apikey
@ -116,13 +113,12 @@ class NotifyProwl(NotifyBase):
# Store the provider key (if specified) # Store the provider key (if specified)
if providerkey: if providerkey:
if not VALIDATE_PROVIDERKEY.match(providerkey): if not VALIDATE_PROVIDERKEY.match(providerkey):
self.logger.warning( msg = \
'The Provider key specified (%s) ' 'The Provider key specified ({}) is invalid.' \
'is invalid.' % providerkey) .format(providerkey)
raise TypeError( self.logger.warning(msg)
'The Provider key specified (%s) ' raise TypeError(msg)
'is invalid.' % providerkey)
# Store the Provider Key # Store the Provider Key
self.providerkey = providerkey self.providerkey = providerkey
@ -218,10 +214,10 @@ class NotifyProwl(NotifyBase):
return '{schema}://{apikey}/{providerkey}/?{args}'.format( return '{schema}://{apikey}/{providerkey}/?{args}'.format(
schema=self.secure_protocol, schema=self.secure_protocol,
apikey=self.quote(self.apikey, safe=''), apikey=NotifyProwl.quote(self.apikey, safe=''),
providerkey='' if not self.providerkey providerkey='' if not self.providerkey
else self.quote(self.providerkey, safe=''), else NotifyProwl.quote(self.providerkey, safe=''),
args=self.urlencode(args), args=NotifyProwl.urlencode(args),
) )
@staticmethod @staticmethod
@ -237,15 +233,16 @@ class NotifyProwl(NotifyBase):
# We're done early as we couldn't load the results # We're done early as we couldn't load the results
return 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: try:
providerkey = [x for x in filter( results['providerkey'] = \
bool, NotifyBase.split_path(results['fullpath']))][0] NotifyProwl.split_path(results['fullpath'])[0]
except (AttributeError, IndexError): except IndexError:
providerkey = None pass
if 'priority' in results['qsd'] and len(results['qsd']['priority']): if 'priority' in results['qsd'] and len(results['qsd']['priority']):
_map = { _map = {
@ -263,7 +260,4 @@ class NotifyProwl(NotifyBase):
# No priority was set # No priority was set
pass pass
results['apikey'] = results['host']
results['providerkey'] = providerkey
return results return results

View File

@ -23,22 +23,17 @@
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE. # THE SOFTWARE.
import re
import six
import requests import requests
from json import dumps from json import dumps
from .NotifyBase import NotifyBase from .NotifyBase import NotifyBase
from ..utils import GET_EMAIL_RE from ..utils import GET_EMAIL_RE
from ..common import NotifyType from ..common import NotifyType
from ..utils import parse_list
# Flag used as a placeholder to sending to all devices # Flag used as a placeholder to sending to all devices
PUSHBULLET_SEND_TO_ALL = '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: # Provide some known codes Pushbullet uses and what they translate to:
PUSHBULLET_HTTP_ERROR_MAP = { PUSHBULLET_HTTP_ERROR_MAP = {
401: 'Unauthorized - Invalid Token.', 401: 'Unauthorized - Invalid Token.',
@ -65,25 +60,17 @@ class NotifyPushBullet(NotifyBase):
# PushBullet uses the http protocol with JSON requests # PushBullet uses the http protocol with JSON requests
notify_url = 'https://api.pushbullet.com/v2/pushes' 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 Initialize PushBullet Object
""" """
super(NotifyPushBullet, self).__init__(**kwargs) super(NotifyPushBullet, self).__init__(**kwargs)
self.accesstoken = accesstoken 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.targets = parse_list(targets)
self.recipients = recipients if len(self.targets) == 0:
self.targets = (PUSHBULLET_SEND_TO_ALL, )
else:
self.recipients = list()
if len(self.recipients) == 0:
self.recipients = (PUSHBULLET_SEND_TO_ALL, )
def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs): def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs):
""" """
@ -99,10 +86,10 @@ class NotifyPushBullet(NotifyBase):
# error tracking (used for function return) # error tracking (used for function return)
has_error = False has_error = False
# Create a copy of the recipients list # Create a copy of the targets list
recipients = list(self.recipients) targets = list(self.targets)
while len(recipients): while len(targets):
recipient = recipients.pop(0) recipient = targets.pop(0)
# prepare JSON Object # prepare JSON Object
payload = { payload = {
@ -149,7 +136,7 @@ class NotifyPushBullet(NotifyBase):
if r.status_code != requests.codes.ok: if r.status_code != requests.codes.ok:
# We had a problem # We had a problem
status_str = \ status_str = \
NotifyBase.http_response_code_lookup( NotifyPushBullet.http_response_code_lookup(
r.status_code, PUSHBULLET_HTTP_ERROR_MAP) r.status_code, PUSHBULLET_HTTP_ERROR_MAP)
self.logger.warning( self.logger.warning(
@ -195,17 +182,17 @@ class NotifyPushBullet(NotifyBase):
'overflow': self.overflow_mode, 'overflow': self.overflow_mode,
} }
recipients = '/'.join([self.quote(x) for x in self.recipients]) targets = '/'.join([NotifyPushBullet.quote(x) for x in self.targets])
if recipients == PUSHBULLET_SEND_TO_ALL: if targets == PUSHBULLET_SEND_TO_ALL:
# keyword is reserved for internal usage only; it's safe to remove # keyword is reserved for internal usage only; it's safe to remove
# it from the recipients list # it from the recipients list
recipients = '' targets = ''
return '{schema}://{accesstoken}/{recipients}/?{args}'.format( return '{schema}://{accesstoken}/{targets}/?{args}'.format(
schema=self.secure_protocol, schema=self.secure_protocol,
accesstoken=self.quote(self.accesstoken, safe=''), accesstoken=NotifyPushBullet.quote(self.accesstoken, safe=''),
recipients=recipients, targets=targets,
args=self.urlencode(args)) args=NotifyPushBullet.urlencode(args))
@staticmethod @staticmethod
def parse_url(url): def parse_url(url):
@ -220,10 +207,17 @@ class NotifyPushBullet(NotifyBase):
# We're done early as we couldn't load the results # We're done early as we couldn't load the results
return results return results
# Apply our settings now # Fetch our targets
recipients = NotifyBase.unquote(results['fullpath']) results['targets'] = \
NotifyPushBullet.split_path(results['fullpath'])
results['accesstoken'] = results['host'] # The 'to' makes it easier to use yaml configuration
results['recipients'] = recipients 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 return results

View File

@ -24,13 +24,13 @@
# THE SOFTWARE. # THE SOFTWARE.
import re import re
import six
import requests import requests
from json import dumps from json import dumps
from itertools import chain from itertools import chain
from .NotifyBase import NotifyBase from .NotifyBase import NotifyBase
from ..common import NotifyType from ..common import NotifyType
from ..utils import parse_list
# Used to detect and parse channels # Used to detect and parse channels
IS_CHANNEL = re.compile(r'^#(?P<name>[A-Za-z0-9]+)$') IS_CHANNEL = re.compile(r'^#(?P<name>[A-Za-z0-9]+)$')
@ -38,10 +38,6 @@ IS_CHANNEL = re.compile(r'^#(?P<name>[A-Za-z0-9]+)$')
# Used to detect and parse a users push id # Used to detect and parse a users push id
IS_USER_PUSHED_ID = re.compile(r'^@(?P<name>[A-Za-z0-9]+)$') IS_USER_PUSHED_ID = re.compile(r'^@(?P<name>[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): class NotifyPushed(NotifyBase):
""" """
@ -71,7 +67,7 @@ class NotifyPushed(NotifyBase):
# The maximum allowable characters allowed in the body per message # The maximum allowable characters allowed in the body per message
body_maxlen = 140 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 Initialize Pushed Object
@ -79,14 +75,14 @@ class NotifyPushed(NotifyBase):
super(NotifyPushed, self).__init__(**kwargs) super(NotifyPushed, self).__init__(**kwargs)
if not app_key: if not app_key:
raise TypeError( msg = 'An invalid Application Key was specified.'
'An invalid Application Key was specified.' self.logger.warning(msg)
) raise TypeError(msg)
if not app_secret: if not app_secret:
raise TypeError( msg = 'An invalid Application Secret was specified.'
'An invalid Application Secret was specified.' self.logger.warning(msg)
) raise TypeError(msg)
# Initialize channel list # Initialize channel list
self.channels = list() self.channels = list()
@ -94,28 +90,15 @@ class NotifyPushed(NotifyBase):
# Initialize user list # Initialize user list
self.users = 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: # Validate recipients and drop bad ones:
for recipient in recipients: for target in parse_list(targets):
result = IS_CHANNEL.match(recipient) result = IS_CHANNEL.match(target)
if result: if result:
# store valid device # store valid device
self.channels.append(result.group('name')) self.channels.append(result.group('name'))
continue continue
result = IS_USER_PUSHED_ID.match(recipient) result = IS_USER_PUSHED_ID.match(target)
if result: if result:
# store valid room # store valid room
self.users.append(result.group('name')) self.users.append(result.group('name'))
@ -123,7 +106,7 @@ class NotifyPushed(NotifyBase):
self.logger.warning( self.logger.warning(
'Dropped invalid channel/userid ' 'Dropped invalid channel/userid '
'(%s) specified.' % recipient, '(%s) specified.' % target,
) )
# Store our data # Store our data
@ -229,7 +212,7 @@ class NotifyPushed(NotifyBase):
if r.status_code != requests.codes.ok: if r.status_code != requests.codes.ok:
# We had a problem # We had a problem
status_str = \ status_str = \
NotifyBase.http_response_code_lookup(r.status_code) NotifyPushed.http_response_code_lookup(r.status_code)
self.logger.warning( self.logger.warning(
'Failed to send Pushed notification:' 'Failed to send Pushed notification:'
@ -269,16 +252,16 @@ class NotifyPushed(NotifyBase):
return '{schema}://{app_key}/{app_secret}/{targets}/?{args}'.format( return '{schema}://{app_key}/{app_secret}/{targets}/?{args}'.format(
schema=self.secure_protocol, schema=self.secure_protocol,
app_key=self.quote(self.app_key, safe=''), app_key=NotifyPushed.quote(self.app_key, safe=''),
app_secret=self.quote(self.app_secret, safe=''), app_secret=NotifyPushed.quote(self.app_secret, safe=''),
targets='/'.join( targets='/'.join(
[self.quote(x) for x in chain( [NotifyPushed.quote(x) for x in chain(
# Channels are prefixed with a pound/hashtag symbol # Channels are prefixed with a pound/hashtag symbol
['#{}'.format(x) for x in self.channels], ['#{}'.format(x) for x in self.channels],
# Users are prefixed with an @ symbol # Users are prefixed with an @ symbol
['@{}'.format(x) for x in self.users], ['@{}'.format(x) for x in self.users],
)]), )]),
args=self.urlencode(args)) args=NotifyPushed.urlencode(args))
@staticmethod @staticmethod
def parse_url(url): def parse_url(url):
@ -296,30 +279,28 @@ class NotifyPushed(NotifyBase):
# Apply our settings now # Apply our settings now
# The first token is stored in the hostname # The first token is stored in the hostname
app_key = results['host'] app_key = NotifyPushed.unquote(results['host'])
# Initialize our recipients
recipients = None
entries = NotifyPushed.split_path(results['fullpath'])
# Now fetch the remaining tokens # Now fetch the remaining tokens
try: try:
app_secret = \ app_secret = entries.pop(0)
[x for x in filter(bool, NotifyBase.split_path(
results['fullpath']))][0]
except (ValueError, AttributeError, IndexError): except IndexError:
# Force some bad values that will get caught # Force some bad values that will get caught
# in parsing later # in parsing later
app_secret = None app_secret = None
app_key = None app_key = None
# Get our recipients # Get our recipients (based on remaining entries)
recipients = \ results['targets'] = entries
[x for x in filter(bool, NotifyBase.split_path(
results['fullpath']))][1:] # 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_key'] = app_key
results['app_secret'] = app_secret results['app_secret'] = app_secret
results['recipients'] = recipients
return results return results

View File

@ -62,6 +62,12 @@ class NotifyPushjet(NotifyBase):
""" """
super(NotifyPushjet, self).__init__(**kwargs) 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 # store our key
self.secret_key = secret_key self.secret_key = secret_key
@ -107,11 +113,11 @@ class NotifyPushjet(NotifyBase):
return '{schema}://{secret_key}@{hostname}{port}/?{args}'.format( return '{schema}://{secret_key}@{hostname}{port}/?{args}'.format(
schema=self.secure_protocol if self.secure else self.protocol, schema=self.secure_protocol if self.secure else self.protocol,
secret_key=self.quote(self.secret_key, safe=''), secret_key=NotifyPushjet.quote(self.secret_key, safe=''),
hostname=self.host, hostname=NotifyPushjet.quote(self.host, safe=''),
port='' if self.port is None or self.port == default_port port='' if self.port is None or self.port == default_port
else ':{}'.format(self.port), else ':{}'.format(self.port),
args=self.urlencode(args), args=NotifyPushjet.urlencode(args),
) )
@staticmethod @staticmethod
@ -133,11 +139,8 @@ class NotifyPushjet(NotifyBase):
# We're done early as we couldn't load the results # We're done early as we couldn't load the results
return results return results
if not results.get('user'):
# a username is required
return None
# Store it as it's value # Store it as it's value
results['secret_key'] = results.get('user') results['secret_key'] = \
NotifyPushjet.unquote(results.get('user'))
return results return results

View File

@ -24,11 +24,11 @@
# THE SOFTWARE. # THE SOFTWARE.
import re import re
import six
import requests import requests
from .NotifyBase import NotifyBase from .NotifyBase import NotifyBase
from ..common import NotifyType from ..common import NotifyType
from ..utils import parse_list
# Flag used as a placeholder to sending to all devices # Flag used as a placeholder to sending to all devices
PUSHOVER_SEND_TO_ALL = 'ALL_DEVICES' PUSHOVER_SEND_TO_ALL = 'ALL_DEVICES'
@ -60,9 +60,6 @@ PUSHOVER_PRIORITIES = (
PushoverPriority.EMERGENCY, PushoverPriority.EMERGENCY,
) )
# Used to break path apart into list of devices
DEVICE_LIST_DELIM = re.compile(r'[ \t\r\n,\\/]+')
# Extend HTTP Error Messages # Extend HTTP Error Messages
PUSHOVER_HTTP_ERROR_MAP = { PUSHOVER_HTTP_ERROR_MAP = {
401: 'Unauthorized - Invalid Token.', 401: 'Unauthorized - Invalid Token.',
@ -92,7 +89,7 @@ class NotifyPushover(NotifyBase):
# The maximum allowable characters allowed in the body per message # The maximum allowable characters allowed in the body per message
body_maxlen = 512 body_maxlen = 512
def __init__(self, token, devices=None, priority=None, **kwargs): def __init__(self, token, targets=None, priority=None, **kwargs):
""" """
Initialize Pushover Object Initialize Pushover Object
""" """
@ -104,30 +101,18 @@ class NotifyPushover(NotifyBase):
except AttributeError: except AttributeError:
# Token was None # Token was None
self.logger.warning('No API Token was specified.') msg = 'No API Token was specified.'
raise TypeError('No API Token was specified.') self.logger.warning(msg)
raise TypeError(msg)
if not VALIDATE_TOKEN.match(self.token): if not VALIDATE_TOKEN.match(self.token):
self.logger.warning( msg = 'The API Token specified (%s) is invalid.'.format(token)
'The API Token specified (%s) is invalid.' % token, self.logger.warning(msg)
) raise TypeError(msg)
raise TypeError(
'The API Token specified (%s) is invalid.' % token,
)
if isinstance(devices, six.string_types): self.targets = parse_list(targets)
self.devices = [x for x in filter(bool, DEVICE_LIST_DELIM.split( if len(self.targets) == 0:
devices, self.targets = (PUSHOVER_SEND_TO_ALL, )
))]
elif isinstance(devices, (set, tuple, list)):
self.devices = devices
else:
self.devices = list()
if len(self.devices) == 0:
self.devices = (PUSHOVER_SEND_TO_ALL, )
# The Priority of the message # The Priority of the message
if priority not in PUSHOVER_PRIORITIES: if priority not in PUSHOVER_PRIORITIES:
@ -137,16 +122,14 @@ class NotifyPushover(NotifyBase):
self.priority = priority self.priority = priority
if not self.user: if not self.user:
self.logger.warning('No user was specified.') msg = 'No user was specified.'
raise TypeError('No user was specified.') self.logger.warning(msg)
raise TypeError(msg)
if not VALIDATE_USERGROUP.match(self.user): if not VALIDATE_USERGROUP.match(self.user):
self.logger.warning( msg = 'The user/group specified (%s) is invalid.' % self.user
'The user/group specified (%s) is invalid.' % self.user, self.logger.warning(msg)
) raise TypeError(msg)
raise TypeError(
'The user/group specified (%s) is invalid.' % self.user,
)
def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs): def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs):
""" """
@ -163,7 +146,7 @@ class NotifyPushover(NotifyBase):
has_error = False has_error = False
# Create a copy of the devices list # Create a copy of the devices list
devices = list(self.devices) devices = list(self.targets)
while len(devices): while len(devices):
device = devices.pop(0) device = devices.pop(0)
@ -205,7 +188,7 @@ class NotifyPushover(NotifyBase):
if r.status_code != requests.codes.ok: if r.status_code != requests.codes.ok:
# We had a problem # We had a problem
status_str = \ status_str = \
NotifyBase.http_response_code_lookup( NotifyPushover.http_response_code_lookup(
r.status_code, PUSHOVER_HTTP_ERROR_MAP) r.status_code, PUSHOVER_HTTP_ERROR_MAP)
self.logger.warning( self.logger.warning(
@ -262,7 +245,10 @@ class NotifyPushover(NotifyBase):
else _map[self.priority], 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: if devices == PUSHOVER_SEND_TO_ALL:
# keyword is reserved for internal usage only; it's safe to remove # keyword is reserved for internal usage only; it's safe to remove
# it from the devices list # it from the devices list
@ -271,10 +257,11 @@ class NotifyPushover(NotifyBase):
return '{schema}://{auth}{token}/{devices}/?{args}'.format( return '{schema}://{auth}{token}/{devices}/?{args}'.format(
schema=self.secure_protocol, schema=self.secure_protocol,
auth='' if not self.user auth='' if not self.user
else '{user}@'.format(user=self.quote(self.user, safe='')), else '{user}@'.format(
token=self.quote(self.token, safe=''), user=NotifyPushover.quote(self.user, safe='')),
token=NotifyPushover.quote(self.token, safe=''),
devices=devices, devices=devices,
args=self.urlencode(args)) args=NotifyPushover.urlencode(args))
@staticmethod @staticmethod
def parse_url(url): def parse_url(url):
@ -289,21 +276,14 @@ class NotifyPushover(NotifyBase):
# We're done early as we couldn't load the results # We're done early as we couldn't load the results
return results return results
# Apply our settings now # Set our priority
devices = NotifyBase.unquote(results['fullpath'])
if 'priority' in results['qsd'] and len(results['qsd']['priority']): if 'priority' in results['qsd'] and len(results['qsd']['priority']):
_map = { _map = {
'l': PushoverPriority.LOW, 'l': PushoverPriority.LOW,
'-2': PushoverPriority.LOW,
'm': PushoverPriority.MODERATE, 'm': PushoverPriority.MODERATE,
'-1': PushoverPriority.MODERATE,
'n': PushoverPriority.NORMAL, 'n': PushoverPriority.NORMAL,
'0': PushoverPriority.NORMAL,
'h': PushoverPriority.HIGH, 'h': PushoverPriority.HIGH,
'1': PushoverPriority.HIGH,
'e': PushoverPriority.EMERGENCY, 'e': PushoverPriority.EMERGENCY,
'2': PushoverPriority.EMERGENCY,
} }
try: try:
results['priority'] = \ results['priority'] = \
@ -313,7 +293,15 @@ class NotifyPushover(NotifyBase):
# No priority was set # No priority was set
pass pass
results['token'] = results['host'] # Retrieve all of our targets
results['devices'] = devices 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 return results

View File

@ -24,13 +24,13 @@
# THE SOFTWARE. # THE SOFTWARE.
import re import re
import six
import requests import requests
from json import loads from json import loads
from itertools import chain from itertools import chain
from .NotifyBase import NotifyBase from .NotifyBase import NotifyBase
from ..common import NotifyType from ..common import NotifyType
from ..utils import parse_list
IS_CHANNEL = re.compile(r'^#(?P<name>[A-Za-z0-9]+)$') IS_CHANNEL = re.compile(r'^#(?P<name>[A-Za-z0-9]+)$')
IS_ROOM_ID = re.compile(r'^(?P<name>[A-Za-z0-9]+)$') IS_ROOM_ID = re.compile(r'^(?P<name>[A-Za-z0-9]+)$')
@ -72,17 +72,14 @@ class NotifyRocketChat(NotifyBase):
# The maximum size of the message # The maximum size of the message
body_maxlen = 200 body_maxlen = 200
def __init__(self, recipients=None, **kwargs): def __init__(self, targets=None, **kwargs):
""" """
Initialize Notify Rocket.Chat Object Initialize Notify Rocket.Chat Object
""" """
super(NotifyRocketChat, self).__init__(**kwargs) super(NotifyRocketChat, self).__init__(**kwargs)
if self.secure: # Set our schema
self.schema = 'https' self.schema = 'https' if self.secure else 'http'
else:
self.schema = 'http'
# Prepare our URL # Prepare our URL
self.api_url = '%s://%s' % (self.schema, self.host) self.api_url = '%s://%s' % (self.schema, self.host)
@ -98,17 +95,6 @@ class NotifyRocketChat(NotifyBase):
# Initialize room list # Initialize room list
self.rooms = 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): if not (self.user and self.password):
# Username & Password is required for Rocket Chat to work # Username & Password is required for Rocket Chat to work
raise TypeError( raise TypeError(
@ -116,7 +102,7 @@ class NotifyRocketChat(NotifyBase):
) )
# Validate recipients and drop bad ones: # Validate recipients and drop bad ones:
for recipient in recipients: for recipient in parse_list(targets):
result = IS_CHANNEL.match(recipient) result = IS_CHANNEL.match(recipient)
if result: if result:
# store valid device # store valid device
@ -135,9 +121,9 @@ class NotifyRocketChat(NotifyBase):
) )
if len(self.rooms) == 0 and len(self.channels) == 0: if len(self.rooms) == 0 and len(self.channels) == 0:
raise TypeError( msg = 'No Rocket.Chat room and/or channels specified to notify.'
'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) # Used to track token headers upon authentication (if successful)
self.headers = {} self.headers = {}
@ -155,8 +141,8 @@ class NotifyRocketChat(NotifyBase):
# Determine Authentication # Determine Authentication
auth = '{user}:{password}@'.format( auth = '{user}:{password}@'.format(
user=self.quote(self.user, safe=''), user=NotifyRocketChat.quote(self.user, safe=''),
password=self.quote(self.password, safe=''), password=NotifyRocketChat.quote(self.password, safe=''),
) )
default_port = 443 if self.secure else 80 default_port = 443 if self.secure else 80
@ -164,17 +150,17 @@ class NotifyRocketChat(NotifyBase):
return '{schema}://{auth}{hostname}{port}/{targets}/?{args}'.format( return '{schema}://{auth}{hostname}{port}/{targets}/?{args}'.format(
schema=self.secure_protocol if self.secure else self.protocol, schema=self.secure_protocol if self.secure else self.protocol,
auth=auth, auth=auth,
hostname=self.host, hostname=NotifyRocketChat.quote(self.host, safe=''),
port='' if self.port is None or self.port == default_port port='' if self.port is None or self.port == default_port
else ':{}'.format(self.port), else ':{}'.format(self.port),
targets='/'.join( 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 # Channels are prefixed with a pound/hashtag symbol
['#{}'.format(x) for x in self.channels], ['#{}'.format(x) for x in self.channels],
# Rooms are as is # Rooms are as is
self.rooms, self.rooms,
)]), )]),
args=self.urlencode(args), args=NotifyRocketChat.urlencode(args),
) )
def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs): def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs):
@ -252,7 +238,7 @@ class NotifyRocketChat(NotifyBase):
if r.status_code != requests.codes.ok: if r.status_code != requests.codes.ok:
# We had a problem # We had a problem
status_str = \ status_str = \
NotifyBase.http_response_code_lookup( NotifyRocketChat.http_response_code_lookup(
r.status_code, RC_HTTP_ERROR_MAP) r.status_code, RC_HTTP_ERROR_MAP)
self.logger.warning( self.logger.warning(
@ -300,7 +286,7 @@ class NotifyRocketChat(NotifyBase):
if r.status_code != requests.codes.ok: if r.status_code != requests.codes.ok:
# We had a problem # We had a problem
status_str = \ status_str = \
NotifyBase.http_response_code_lookup( NotifyRocketChat.http_response_code_lookup(
r.status_code, RC_HTTP_ERROR_MAP) r.status_code, RC_HTTP_ERROR_MAP)
self.logger.warning( self.logger.warning(
@ -353,7 +339,7 @@ class NotifyRocketChat(NotifyBase):
if r.status_code != requests.codes.ok: if r.status_code != requests.codes.ok:
# We had a problem # We had a problem
status_str = \ status_str = \
NotifyBase.http_response_code_lookup( NotifyRocketChat.http_response_code_lookup(
r.status_code, RC_HTTP_ERROR_MAP) r.status_code, RC_HTTP_ERROR_MAP)
self.logger.warning( self.logger.warning(
@ -396,7 +382,12 @@ class NotifyRocketChat(NotifyBase):
# We're done early as we couldn't load the results # We're done early as we couldn't load the results
return results return results
# Apply our settings now # Apply our targets
results['recipients'] = NotifyBase.unquote(results['fullpath']) 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 return results

View File

@ -32,12 +32,14 @@
# These are important <---^----------------------------------------^ # These are important <---^----------------------------------------^
# #
import re import re
import six
import requests import requests
from json import dumps from json import dumps
from .NotifyBase import NotifyBase from .NotifyBase import NotifyBase
from ..common import NotifyImageSize from ..common import NotifyImageSize
from ..common import NotifyType from ..common import NotifyType
from ..utils import parse_bool
# Token required as part of the API request # Token required as part of the API request
VALIDATE_TOKEN = re.compile(r'[A-Za-z0-9]{15}') 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}') 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' SLACK = 'slack'
RYVER = 'ryver' RYVER = 'ryver'
# Define the types in a list for validation purposes # Define the types in a list for validation purposes
RYVER_WEBHOOK_TYPES = ( RYVER_WEBHOOK_MODES = (
RyverWebhookType.SLACK, RyverWebhookMode.SLACK,
RyverWebhookType.RYVER, RyverWebhookMode.RYVER,
) )
@ -84,39 +86,44 @@ class NotifyRyver(NotifyBase):
# The maximum allowable characters allowed in the body per message # The maximum allowable characters allowed in the body per message
body_maxlen = 1000 body_maxlen = 1000
def __init__(self, organization, token, webhook=RyverWebhookType.RYVER, def __init__(self, organization, token, mode=RyverWebhookMode.RYVER,
**kwargs): include_image=True, **kwargs):
""" """
Initialize Ryver Object Initialize Ryver Object
""" """
super(NotifyRyver, self).__init__(**kwargs) 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()): if not VALIDATE_TOKEN.match(token.strip()):
self.logger.warning( msg = 'The Ryver token specified ({}) is invalid.'\
'The token specified (%s) is invalid.' % token, .format(token)
) self.logger.warning(msg)
raise TypeError( raise TypeError(msg)
'The token specified (%s) is invalid.' % token,
)
if not VALIDATE_ORG.match(organization.strip()): if not VALIDATE_ORG.match(organization.strip()):
self.logger.warning( msg = 'The Ryver organization specified ({}) is invalid.'\
'The organization specified (%s) is invalid.' % organization, .format(organization)
) self.logger.warning(msg)
raise TypeError( raise TypeError(msg)
'The organization specified (%s) is invalid.' % organization,
)
# Store our webhook type # Store our webhook mode
self.webhook = webhook self.mode = None \
if not isinstance(mode, six.string_types) else mode.lower()
if self.webhook not in RYVER_WEBHOOK_TYPES: if self.mode not in RYVER_WEBHOOK_MODES:
self.logger.warning( msg = 'The Ryver webhook mode specified ({}) is invalid.' \
'The webhook specified (%s) is invalid.' % webhook, .format(mode)
) self.logger.warning(msg)
raise TypeError( raise TypeError(msg)
'The webhook specified (%s) is invalid.' % webhook,
)
# The organization associated with the account # The organization associated with the account
self.organization = organization.strip() self.organization = organization.strip()
@ -124,6 +131,9 @@ class NotifyRyver(NotifyBase):
# The token associated with the account # The token associated with the account
self.token = token.strip() 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: # Slack formatting requirements are defined here which Ryver supports:
# https://api.slack.com/docs/message-formatting # https://api.slack.com/docs/message-formatting
self._re_formatting_map = { self._re_formatting_map = {
@ -151,7 +161,7 @@ class NotifyRyver(NotifyBase):
'Content-Type': 'application/json', 'Content-Type': 'application/json',
} }
if self.webhook == RyverWebhookType.SLACK: if self.mode == RyverWebhookMode.SLACK:
# Perform Slack formatting # Perform Slack formatting
title = self._re_formatting_rules.sub( # pragma: no branch title = self._re_formatting_rules.sub( # pragma: no branch
lambda x: self._re_formatting_map[x.group()], title, 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, 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.organization,
self.token, self.token,
) )
# prepare JSON Object # prepare JSON Object
payload = { payload = {
"body": body if not title else '**{}**\r\n{}'.format(title, body), 'body': body if not title else '**{}**\r\n{}'.format(title, body),
'createSource': { 'createSource': {
"displayName": self.user, 'displayName': self.user,
"avatar": self.image_url(notify_type), '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)' % ( self.logger.debug('Ryver POST URL: %s (cert_verify=%r)' % (
url, self.verify_certificate, url, self.verify_certificate,
)) ))
@ -229,22 +246,23 @@ class NotifyRyver(NotifyBase):
args = { args = {
'format': self.notify_format, 'format': self.notify_format,
'overflow': self.overflow_mode, '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 # Determine if there is a botname present
botname = '' botname = ''
if self.user: if self.user:
botname = '{botname}@'.format( botname = '{botname}@'.format(
botname=self.quote(self.user, safe=''), botname=NotifyRyver.quote(self.user, safe=''),
) )
return '{schema}://{botname}{organization}/{token}/?{args}'.format( return '{schema}://{botname}{organization}/{token}/?{args}'.format(
schema=self.secure_protocol, schema=self.secure_protocol,
botname=botname, botname=botname,
organization=self.quote(self.organization, safe=''), organization=NotifyRyver.quote(self.organization, safe=''),
token=self.quote(self.token, safe=''), token=NotifyRyver.quote(self.token, safe=''),
args=self.urlencode(args), args=NotifyRyver.urlencode(args),
) )
@staticmethod @staticmethod
@ -254,31 +272,41 @@ class NotifyRyver(NotifyBase):
us to substantiate this object. us to substantiate this object.
""" """
results = NotifyBase.parse_url(url) results = NotifyBase.parse_url(url)
if not results: if not results:
# We're done early as we couldn't load the results # We're done early as we couldn't load the results
return results return results
# Apply our settings now
# The first token is stored in the hostname # The first token is stored in the hostname
organization = results['host'] results['organization'] = NotifyRyver.unquote(results['host'])
# Now fetch the remaining tokens # Now fetch the remaining tokens
try: try:
token = [x for x in filter( results['token'] = \
bool, NotifyBase.split_path(results['fullpath']))][0] NotifyRyver.split_path(results['fullpath'])[0]
except (ValueError, AttributeError, IndexError): except IndexError:
# We're done # no token
return None results['token'] = None
if 'webhook' in results['qsd'] and len(results['qsd']['webhook']): if 'webhook' in results['qsd']:
results['webhook'] = results['qsd']\ # Deprication Notice issued for v0.7.5
.get('webhook', RyverWebhookType.RYVER).lower() 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 # use mode= for consistency with the other plugins but we also
results['token'] = token # 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 return results

View File

@ -24,7 +24,6 @@
# THE SOFTWARE. # THE SOFTWARE.
import re import re
import six
import hmac import hmac
import requests import requests
from hashlib import sha256 from hashlib import sha256
@ -35,6 +34,7 @@ from itertools import chain
from .NotifyBase import NotifyBase from .NotifyBase import NotifyBase
from ..common import NotifyType from ..common import NotifyType
from ..utils import parse_list
# Some Phone Number Detection # Some Phone Number Detection
IS_PHONE_NO = re.compile(r'^\+?(?P<phone>[0-9\s)(+-]+)\s*$') IS_PHONE_NO = re.compile(r'^\+?(?P<phone>[0-9\s)(+-]+)\s*$')
@ -50,10 +50,6 @@ IS_PHONE_NO = re.compile(r'^\+?(?P<phone>[0-9\s)(+-]+)\s*$')
# ambiguity between a topic that is comprised of all digits and a phone number # ambiguity between a topic that is comprised of all digits and a phone number
IS_TOPIC = re.compile(r'^#?(?P<name>[A-Za-z0-9_-]+)\s*$') IS_TOPIC = re.compile(r'^#?(?P<name>[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 # 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 # 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 # users of this product search though this Access Key Secret and escape all
@ -97,26 +93,26 @@ class NotifySNS(NotifyBase):
title_maxlen = 0 title_maxlen = 0
def __init__(self, access_key_id, secret_access_key, region_name, def __init__(self, access_key_id, secret_access_key, region_name,
recipients=None, **kwargs): targets=None, **kwargs):
""" """
Initialize Notify AWS SNS Object Initialize Notify AWS SNS Object
""" """
super(NotifySNS, self).__init__(**kwargs) super(NotifySNS, self).__init__(**kwargs)
if not access_key_id: if not access_key_id:
raise TypeError( msg = 'An invalid AWS Access Key ID was specified.'
'An invalid AWS Access Key ID was specified.' self.logger.warning(msg)
) raise TypeError(msg)
if not secret_access_key: if not secret_access_key:
raise TypeError( msg = 'An invalid AWS Secret Access Key was specified.'
'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)): if not (region_name and IS_REGION.match(region_name)):
raise TypeError( msg = 'An invalid AWS Region was specified.'
'An invalid AWS Region was specified.' self.logger.warning(msg)
) raise TypeError(msg)
# Initialize topic list # Initialize topic list
self.topics = list() self.topics = list()
@ -147,20 +143,12 @@ class NotifySNS(NotifyBase):
self.aws_auth_algorithm = 'AWS4-HMAC-SHA256' self.aws_auth_algorithm = 'AWS4-HMAC-SHA256'
self.aws_auth_request = 'aws4_request' self.aws_auth_request = 'aws4_request'
if recipients is None: # Get our targets
recipients = [] targets = parse_list(targets)
elif isinstance(recipients, six.string_types): # Validate targets and drop bad ones:
recipients = [x for x in filter(bool, LIST_DELIM.split( for target in targets:
recipients, result = IS_PHONE_NO.match(target)
))]
elif not isinstance(recipients, (set, tuple, list)):
recipients = []
# Validate recipients and drop bad ones:
for recipient in recipients:
result = IS_PHONE_NO.match(recipient)
if result: if result:
# Further check our phone # for it's digit count # Further check our phone # for it's digit count
# if it's less than 10, then we can assume it's # 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: if len(result) < 11 or len(result) > 14:
self.logger.warning( self.logger.warning(
'Dropped invalid phone # ' 'Dropped invalid phone # '
'(%s) specified.' % recipient, '(%s) specified.' % target,
) )
continue continue
@ -177,7 +165,7 @@ class NotifySNS(NotifyBase):
self.phone.append('+{}'.format(result)) self.phone.append('+{}'.format(result))
continue continue
result = IS_TOPIC.match(recipient) result = IS_TOPIC.match(target)
if result: if result:
# store valid topic # store valid topic
self.topics.append(result.group('name')) self.topics.append(result.group('name'))
@ -185,12 +173,12 @@ class NotifySNS(NotifyBase):
self.logger.warning( self.logger.warning(
'Dropped invalid phone/topic ' 'Dropped invalid phone/topic '
'(%s) specified.' % recipient, '(%s) specified.' % target,
) )
if len(self.phone) == 0 and len(self.topics) == 0: if len(self.phone) == 0 and len(self.topics) == 0:
self.logger.warning( 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): def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs):
""" """
@ -278,7 +266,7 @@ class NotifySNS(NotifyBase):
self.throttle() self.throttle()
# Convert our payload from a dict() into a urlencoded string # Convert our payload from a dict() into a urlencoded string
payload = self.urlencode(payload) payload = NotifySNS.urlencode(payload)
# Prepare our Notification URL # Prepare our Notification URL
# Prepare our AWS Headers based on our payload # Prepare our AWS Headers based on our payload
@ -300,7 +288,7 @@ class NotifySNS(NotifyBase):
if r.status_code != requests.codes.ok: if r.status_code != requests.codes.ok:
# We had a problem # We had a problem
status_str = \ status_str = \
NotifyBase.http_response_code_lookup( NotifySNS.http_response_code_lookup(
r.status_code, AWS_HTTP_ERROR_MAP) r.status_code, AWS_HTTP_ERROR_MAP)
self.logger.warning( self.logger.warning(
@ -541,17 +529,18 @@ class NotifySNS(NotifyBase):
return '{schema}://{key_id}/{key_secret}/{region}/{targets}/'\ return '{schema}://{key_id}/{key_secret}/{region}/{targets}/'\
'?{args}'.format( '?{args}'.format(
schema=self.secure_protocol, schema=self.secure_protocol,
key_id=self.quote(self.aws_access_key_id, safe=''), key_id=NotifySNS.quote(self.aws_access_key_id, safe=''),
key_secret=self.quote(self.aws_secret_access_key, safe=''), key_secret=NotifySNS.quote(
region=self.quote(self.aws_region_name, safe=''), self.aws_secret_access_key, safe=''),
region=NotifySNS.quote(self.aws_region_name, safe=''),
targets='/'.join( targets='/'.join(
[self.quote(x) for x in chain( [NotifySNS.quote(x) for x in chain(
# Phone # are prefixed with a plus symbol # Phone # are prefixed with a plus symbol
['+{}'.format(x) for x in self.phone], ['+{}'.format(x) for x in self.phone],
# Topics are prefixed with a pound/hashtag symbol # Topics are prefixed with a pound/hashtag symbol
['#{}'.format(x) for x in self.topics], ['#{}'.format(x) for x in self.topics],
)]), )]),
args=self.urlencode(args), args=NotifySNS.urlencode(args),
) )
@staticmethod @staticmethod
@ -567,12 +556,8 @@ class NotifySNS(NotifyBase):
# We're done early as we couldn't load the results # We're done early as we couldn't load the results
return results return results
#
# Apply our settings now
#
# The AWS Access Key ID is stored in the hostname # 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 # Our AWS Access Key Secret contains slashes in it which unfortunately
# means it is of variable length after the hostname. Since we require # means it is of variable length after the hostname. Since we require
@ -586,9 +571,12 @@ class NotifySNS(NotifyBase):
# accumulated data. # accumulated data.
secret_access_key_parts = list() 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 # Section 1: Get Region and Access Secret
index = 0 index = 0
for i, entry in enumerate(NotifyBase.split_path(results['fullpath'])): for i, entry in enumerate(entries):
# Are we at the region yet? # Are we at the region yet?
result = IS_REGION.match(entry) result = IS_REGION.match(entry)
@ -615,9 +603,13 @@ class NotifySNS(NotifyBase):
secret_access_key_parts.append(entry) secret_access_key_parts.append(entry)
# Section 2: Get our Recipients (basically all remaining entries) # Section 2: Get our Recipients (basically all remaining entries)
results['recipients'] = [ results['targets'] = entries[index:]
NotifyBase.unquote(x) for x in filter(bool, NotifyBase.split_path(
results['fullpath']))][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) # Store our other detected data (if at all)
results['region_name'] = region_name results['region_name'] = region_name

View File

@ -45,6 +45,7 @@ from .NotifyBase import NotifyBase
from ..common import NotifyImageSize from ..common import NotifyImageSize
from ..common import NotifyType from ..common import NotifyType
from ..common import NotifyFormat from ..common import NotifyFormat
from ..utils import parse_bool
# Token required as part of the API request # Token required as part of the API request
# /AAAAAAAAA/........./........................ # /AAAAAAAAA/........./........................
@ -101,41 +102,51 @@ class NotifySlack(NotifyBase):
notify_format = NotifyFormat.MARKDOWN 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 Initialize Slack Object
""" """
super(NotifySlack, self).__init__(**kwargs) 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()): if not VALIDATE_TOKEN_A.match(token_a.strip()):
self.logger.warning( msg = 'The first API token specified ({}) is invalid.'\
'The first API Token specified (%s) is invalid.' % token_a, .format(token_a)
) self.logger.warning(msg)
raise TypeError( raise TypeError(msg)
'The first API Token specified (%s) is invalid.' % token_a,
)
# The token associated with the account # The token associated with the account
self.token_a = token_a.strip() self.token_a = token_a.strip()
if not VALIDATE_TOKEN_B.match(token_b.strip()): if not VALIDATE_TOKEN_B.match(token_b.strip()):
self.logger.warning( msg = 'The second API token specified ({}) is invalid.'\
'The second API Token specified (%s) is invalid.' % token_b, .format(token_b)
) self.logger.warning(msg)
raise TypeError( raise TypeError(msg)
'The second API Token specified (%s) is invalid.' % token_b,
)
# The token associated with the account # The token associated with the account
self.token_b = token_b.strip() self.token_b = token_b.strip()
if not VALIDATE_TOKEN_C.match(token_c.strip()): if not VALIDATE_TOKEN_C.match(token_c.strip()):
self.logger.warning( msg = 'The third API token specified ({}) is invalid.'\
'The third API Token specified (%s) is invalid.' % token_c, .format(token_c)
) self.logger.warning(msg)
raise TypeError( raise TypeError(msg)
'The third API Token specified (%s) is invalid.' % token_c,
)
# The token associated with the account # The token associated with the account
self.token_c = token_c.strip() self.token_c = token_c.strip()
@ -144,20 +155,21 @@ class NotifySlack(NotifyBase):
self.logger.warning( self.logger.warning(
'No user was specified; using %s.' % SLACK_DEFAULT_USER) '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( self.channels = [x for x in filter(bool, CHANNEL_LIST_DELIM.split(
channels, targets,
))] ))]
elif isinstance(channels, (set, tuple, list)): elif isinstance(targets, (set, tuple, list)):
self.channels = channels self.channels = targets
else: else:
self.channels = list() self.channels = list()
if len(self.channels) == 0: if len(self.channels) == 0:
self.logger.warning('No channel(s) were specified.') msg = 'No channel(s) were specified.'
raise TypeError('No channel(s) were specified.') self.logger.warning(msg)
raise TypeError(msg)
# Formatting requirements are defined here: # Formatting requirements are defined here:
# https://api.slack.com/docs/message-formatting # https://api.slack.com/docs/message-formatting
@ -176,6 +188,9 @@ class NotifySlack(NotifyBase):
re.IGNORECASE, 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): def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs):
""" """
Perform Slack Notification Perform Slack Notification
@ -203,8 +218,6 @@ class NotifySlack(NotifyBase):
self.token_c, self.token_c,
) )
image_url = self.image_url(notify_type)
# Create a copy of the channel list # Create a copy of the channel list
channels = list(self.channels) channels = list(self.channels)
while len(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: if image_url:
payload['attachments'][0]['footer_icon'] = image_url payload['attachments'][0]['footer_icon'] = image_url
@ -267,7 +284,7 @@ class NotifySlack(NotifyBase):
if r.status_code != requests.codes.ok: if r.status_code != requests.codes.ok:
# We had a problem # We had a problem
status_str = \ status_str = \
NotifyBase.http_response_code_lookup( NotifySlack.http_response_code_lookup(
r.status_code, SLACK_HTTP_ERROR_MAP) r.status_code, SLACK_HTTP_ERROR_MAP)
self.logger.warning( self.logger.warning(
@ -311,25 +328,26 @@ class NotifySlack(NotifyBase):
args = { args = {
'format': self.notify_format, 'format': self.notify_format,
'overflow': self.overflow_mode, 'overflow': self.overflow_mode,
'image': 'yes' if self.include_image else 'no',
} }
# Determine if there is a botname present # Determine if there is a botname present
botname = '' botname = ''
if self.user: if self.user:
botname = '{botname}@'.format( 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}/'\ return '{schema}://{botname}{token_a}/{token_b}/{token_c}/{targets}/'\
'?{args}'.format( '?{args}'.format(
schema=self.secure_protocol, schema=self.secure_protocol,
botname=botname, botname=botname,
token_a=self.quote(self.token_a, safe=''), token_a=NotifySlack.quote(self.token_a, safe=''),
token_b=self.quote(self.token_b, safe=''), token_b=NotifySlack.quote(self.token_b, safe=''),
token_c=self.quote(self.token_c, safe=''), token_c=NotifySlack.quote(self.token_c, safe=''),
targets='/'.join( targets='/'.join(
[self.quote(x, safe='') for x in self.channels]), [NotifySlack.quote(x, safe='') for x in self.channels]),
args=self.urlencode(args), args=NotifySlack.urlencode(args),
) )
@staticmethod @staticmethod
@ -345,26 +363,39 @@ class NotifySlack(NotifyBase):
# We're done early as we couldn't load the results # We're done early as we couldn't load the results
return results return results
# Apply our settings now # Get unquoted entries
entries = NotifySlack.split_path(results['fullpath'])
# The first token is stored in the hostname # The first token is stored in the hostname
token_a = results['host'] results['token_a'] = NotifySlack.unquote(results['host'])
# Now fetch the remaining tokens # Now fetch the remaining tokens
try: try:
token_b, token_c = [x for x in filter( results['token_b'] = entries.pop(0)
bool, NotifyBase.split_path(results['fullpath']))][0:2]
except (ValueError, AttributeError, IndexError): except IndexError:
# We're done # We're done
return None results['token_b'] = None
channels = [x for x in filter( try:
bool, NotifyBase.split_path(results['fullpath']))][2:] results['token_c'] = entries.pop(0)
results['token_a'] = token_a except IndexError:
results['token_b'] = token_b # We're done
results['token_c'] = token_c results['token_c'] = None
results['channels'] = channels
# 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 return results

View File

@ -107,7 +107,7 @@ class NotifyTelegram(NotifyBase):
# The maximum allowable characters allowed in the body per message # The maximum allowable characters allowed in the body per message
body_maxlen = 4096 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): include_image=True, **kwargs):
""" """
Initialize Telegram Object Initialize Telegram Object
@ -133,19 +133,19 @@ class NotifyTelegram(NotifyBase):
self.bot_token = result.group('key') self.bot_token = result.group('key')
# Parse our list # Parse our list
self.chat_ids = parse_list(chat_ids) self.targets = parse_list(targets)
if self.user: if self.user:
# Treat this as a channel too # 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() _id = self.detect_bot_owner()
if _id: if _id:
# Store our 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.' err = 'No chat_id(s) were specified.'
self.logger.warning(err) self.logger.warning(err)
raise TypeError(err) raise TypeError(err)
@ -168,14 +168,25 @@ class NotifyTelegram(NotifyBase):
'sendPhoto' '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) path = self.image_path(notify_type)
if not path: if not path:
# No image to send # No image to send
self.logger.debug( self.logger.debug(
'Telegram Image does not exist for %s' % (notify_type)) '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 = { payload = {
'chat_id': chat_id, 'chat_id': chat_id,
@ -196,7 +207,7 @@ class NotifyTelegram(NotifyBase):
if r.status_code != requests.codes.ok: if r.status_code != requests.codes.ok:
# We had a problem # We had a problem
status_str = \ status_str = \
NotifyBase.http_response_code_lookup(r.status_code) NotifyTelegram.http_response_code_lookup(r.status_code)
self.logger.warning( self.logger.warning(
'Failed to send Telegram Image: ' 'Failed to send Telegram Image: '
@ -248,7 +259,7 @@ class NotifyTelegram(NotifyBase):
if r.status_code != requests.codes.ok: if r.status_code != requests.codes.ok:
# We had a problem # We had a problem
status_str = \ status_str = \
NotifyBase.http_response_code_lookup(r.status_code) NotifyTelegram.http_response_code_lookup(r.status_code)
try: try:
# Try to get the error message if we can: # Try to get the error message if we can:
@ -368,10 +379,10 @@ class NotifyTelegram(NotifyBase):
title = re.sub('&emsp;?', ' ', title, re.I) title = re.sub('&emsp;?', ' ', title, re.I)
# HTML # HTML
title = NotifyBase.escape_html(title, whitespace=False) title = NotifyTelegram.escape_html(title, whitespace=False)
# HTML # HTML
body = NotifyBase.escape_html(body, whitespace=False) body = NotifyTelegram.escape_html(body, whitespace=False)
if title and self.notify_format == NotifyFormat.TEXT: if title and self.notify_format == NotifyFormat.TEXT:
# Text HTML Formatting # Text HTML Formatting
@ -393,9 +404,9 @@ class NotifyTelegram(NotifyBase):
payload['text'] = body payload['text'] = body
# Create a copy of the chat_ids list # Create a copy of the chat_ids list
chat_ids = list(self.chat_ids) targets = list(self.targets)
while len(chat_ids): while len(targets):
chat_id = chat_ids.pop(0) chat_id = targets.pop(0)
chat_id = IS_CHAT_ID_RE.match(chat_id) chat_id = IS_CHAT_ID_RE.match(chat_id)
if not chat_id: if not chat_id:
self.logger.warning( self.logger.warning(
@ -441,7 +452,7 @@ class NotifyTelegram(NotifyBase):
if r.status_code != requests.codes.ok: if r.status_code != requests.codes.ok:
# We had a problem # We had a problem
status_str = \ status_str = \
NotifyBase.http_response_code_lookup(r.status_code) NotifyTelegram.http_response_code_lookup(r.status_code)
try: try:
# Try to get the error message if we can: # Try to get the error message if we can:
@ -489,16 +500,17 @@ class NotifyTelegram(NotifyBase):
args = { args = {
'format': self.notify_format, 'format': self.notify_format,
'overflow': self.overflow_mode, 'overflow': self.overflow_mode,
'image': self.include_image,
} }
# No need to check the user token because the user automatically gets # No need to check the user token because the user automatically gets
# appended into the list of chat ids # appended into the list of chat ids
return '{schema}://{bot_token}/{targets}/?{args}'.format( return '{schema}://{bot_token}/{targets}/?{args}'.format(
schema=self.secure_protocol, schema=self.secure_protocol,
bot_token=self.quote(self.bot_token, safe=''), bot_token=NotifyTelegram.quote(self.bot_token, safe=''),
targets='/'.join( targets='/'.join(
[self.quote('@{}'.format(x)) for x in self.chat_ids]), [NotifyTelegram.quote('@{}'.format(x)) for x in self.targets]),
args=self.urlencode(args)) args=NotifyTelegram.urlencode(args))
@staticmethod @staticmethod
def parse_url(url): def parse_url(url):
@ -507,9 +519,9 @@ class NotifyTelegram(NotifyBase):
us to substantiate this object. us to substantiate this object.
""" """
# This is a dirty hack; but it's the only work around to # This is a dirty hack; but it's the only work around to tgram://
# tgram:// messages since the bot_token has a colon in it. # messages since the bot_token has a colon in it. It invalidates a
# It invalidates an normal URL. # normal URL.
# This hack searches for this bogus URL and corrects it so we can # 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 # 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 # 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 # Now fetch the remaining tokens
bot_token_b = [x for x in filter( bot_token_b = entries.pop(0)
bool, NotifyBase.split_path(results['fullpath']))][0]
bot_token = '%s:%s' % (bot_token_a, bot_token_b) bot_token = '%s:%s' % (bot_token_a, bot_token_b)
chat_ids = [x for x in filter( # Store our chat ids (as these are the remaining entries)
bool, NotifyBase.split_path(results['fullpath']))][1:] 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 # Store our bot token
results['bot_token'] = bot_token results['bot_token'] = bot_token
# Store our chat ids
results['chat_ids'] = chat_ids
# Include images with our message # Include images with our message
results['include_image'] = \ results['include_image'] = \
parse_bool(results['qsd'].get('image', False)) parse_bool(results['qsd'].get('image', False))

View File

@ -26,6 +26,7 @@
from . import tweepy from . import tweepy
from ..NotifyBase import NotifyBase from ..NotifyBase import NotifyBase
from ...common import NotifyType from ...common import NotifyType
from ...utils import parse_list
class NotifyTwitter(NotifyBase): class NotifyTwitter(NotifyBase):
@ -54,7 +55,7 @@ class NotifyTwitter(NotifyBase):
# Twitter does have titles when creating a message # Twitter does have titles when creating a message
title_maxlen = 0 title_maxlen = 0
def __init__(self, ckey, csecret, akey, asecret, **kwargs): def __init__(self, ckey, csecret, akey, asecret, targets=None, **kwargs):
""" """
Initialize Twitter Object Initialize Twitter Object
@ -62,29 +63,32 @@ class NotifyTwitter(NotifyBase):
super(NotifyTwitter, self).__init__(**kwargs) super(NotifyTwitter, self).__init__(**kwargs)
if not ckey: if not ckey:
raise TypeError( msg = 'An invalid Consumer API Key was specified.'
'An invalid Consumer API Key was specified.' self.logger.warning(msg)
) raise TypeError(msg)
if not csecret: if not csecret:
raise TypeError( msg = 'An invalid Consumer Secret API Key was specified.'
'An invalid Consumer Secret API Key was specified.' self.logger.warning(msg)
) raise TypeError(msg)
if not akey: if not akey:
raise TypeError( msg = 'An invalid Access Token API Key was specified.'
'An invalid Acess Token API Key was specified.' self.logger.warning(msg)
) raise TypeError(msg)
if not asecret: if not asecret:
raise TypeError( msg = 'An invalid Access Token Secret API Key was specified.'
'An invalid Acess Token Secret API Key was specified.' self.logger.warning(msg)
) raise TypeError(msg)
if not self.user: # Identify our targets
raise TypeError( self.targets = parse_list(targets)
'No user was specified.'
) 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 # Store our data
self.ckey = ckey self.ckey = ckey
@ -113,28 +117,68 @@ class NotifyTwitter(NotifyBase):
) )
return False return False
# Always call throttle before any remote server i/o is made to avoid # Get ourselves a list of targets
# thrashing the remote server and risk being blocked. users = list(self.targets)
self.throttle() if not users:
# notify ourselves
users.append(self.user)
try: # Error Tracking
# Get our API has_error = False
api = tweepy.API(self.auth)
# Send our Direct Message while len(users) > 0:
api.send_direct_message(self.user, text=body) # Get our user
self.logger.info('Sent Twitter DM notification.') user = users.pop(0)
except Exception as e: # Always call throttle before any remote server i/o is made to
self.logger.warning( # avoid thrashing the remote server and risk being blocked.
'A Connection error occured sending Twitter ' self.throttle()
'direct message to %s.' % self.user)
self.logger.debug('Twitter Exception: %s' % str(e))
# Return; we're done try:
return False # 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 @staticmethod
def parse_url(url): def parse_url(url):
@ -152,13 +196,12 @@ class NotifyTwitter(NotifyBase):
# Apply our settings now # Apply our settings now
# The first token is stored in the hostname # The first token is stored in the hostname
consumer_key = results['host'] consumer_key = NotifyTwitter.unquote(results['host'])
# Now fetch the remaining tokens # Now fetch the remaining tokens
try: try:
consumer_secret, access_token_key, access_token_secret = \ consumer_secret, access_token_key, access_token_secret = \
[x for x in filter(bool, NotifyBase.split_path( NotifyTwitter.split_path(results['fullpath'])[0:3]
results['fullpath']))][0:3]
except (ValueError, AttributeError, IndexError): except (ValueError, AttributeError, IndexError):
# Force some bad values that will get caught # Force some bad values that will get caught
@ -172,4 +215,8 @@ class NotifyTwitter(NotifyBase):
results['akey'] = access_token_key results['akey'] = access_token_key
results['asecret'] = access_token_secret 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 return results

View File

@ -31,6 +31,7 @@ from time import sleep
from .NotifyBase import NotifyBase from .NotifyBase import NotifyBase
from ..common import NotifyImageSize from ..common import NotifyImageSize
from ..common import NotifyType from ..common import NotifyType
from ..utils import parse_bool
# Default our global support flag # Default our global support flag
NOTIFY_WINDOWS_SUPPORT_ENABLED = False NOTIFY_WINDOWS_SUPPORT_ENABLED = False
@ -75,6 +76,9 @@ class NotifyWindows(NotifyBase):
# content to display # content to display
body_max_line_count = 2 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 # 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 # in an environment that simply doesn't have the windows packages
# available to us. It also allows us to handle situations where the # available to us. It also allows us to handle situations where the
@ -84,18 +88,23 @@ class NotifyWindows(NotifyBase):
# let me know! :) # let me know! :)
_enabled = NOTIFY_WINDOWS_SUPPORT_ENABLED _enabled = NOTIFY_WINDOWS_SUPPORT_ENABLED
def __init__(self, **kwargs): def __init__(self, include_image=True, duration=None, **kwargs):
""" """
Initialize Windows Object Initialize Windows Object
""" """
super(NotifyWindows, self).__init__(**kwargs)
# Number of seconds to display notification for # 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 # Define our handler
self.hwnd = None 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): def _on_destroy(self, hwnd, msg, wparam, lparam):
""" """
@ -140,20 +149,26 @@ class NotifyWindows(NotifyBase):
self.hinst, None) self.hinst, None)
win32gui.UpdateWindow(self.hwnd) win32gui.UpdateWindow(self.hwnd)
# image path # image path (if configured to acquire)
icon_path = self.image_path(notify_type, extension='.ico') icon_path = None if not self.include_image \
icon_flags = win32con.LR_LOADFROMFILE | win32con.LR_DEFAULTSIZE else self.image_path(notify_type, extension='.ico')
try: if icon_path:
hicon = win32gui.LoadImage( icon_flags = win32con.LR_LOADFROMFILE | win32con.LR_DEFAULTSIZE
self.hinst, icon_path, win32con.IMAGE_ICON, 0, 0,
icon_flags)
except Exception as e: try:
self.logger.warning( hicon = win32gui.LoadImage(
"Could not load windows notification icon ({}): {}" self.hinst, icon_path, win32con.IMAGE_ICON, 0, 0,
.format(icon_path, e)) 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 # disable icon
hicon = win32gui.LoadIcon(0, win32con.IDI_APPLICATION) hicon = win32gui.LoadIcon(0, win32con.IDI_APPLICATION)
@ -185,7 +200,18 @@ class NotifyWindows(NotifyBase):
Returns the URL built dynamically based on specified arguments. 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 @staticmethod
def parse_url(url): def parse_url(url):
@ -196,15 +222,31 @@ class NotifyWindows(NotifyBase):
""" """
# return a very basic set of requirements results = NotifyBase.parse_url(url)
return { if not results:
'schema': NotifyWindows.protocol, results = {
'user': None, 'schema': NotifyWindows.protocol,
'password': None, 'user': None,
'port': None, 'password': None,
'host': 'localhost', 'port': None,
'fullpath': None, 'host': '_',
'path': None, 'fullpath': None,
'url': url, 'path': None,
'qsd': {}, '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

View File

@ -29,6 +29,7 @@ from json import dumps
from .NotifyBase import NotifyBase from .NotifyBase import NotifyBase
from ..common import NotifyType from ..common import NotifyType
from ..common import NotifyImageSize from ..common import NotifyImageSize
from ..utils import parse_bool
class NotifyXBMC(NotifyBase): class NotifyXBMC(NotifyBase):
@ -70,26 +71,27 @@ class NotifyXBMC(NotifyBase):
# Allows the user to specify the NotifyImageSize object # Allows the user to specify the NotifyImageSize object
image_size = NotifyImageSize.XY_128 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 default protocol version (v2)
xbmc_remote_protocol = 2 xbmc_remote_protocol = 2
# KODI default protocol version (v6) # KODI default protocol version (v6)
kodi_remote_protocol = 6 kodi_remote_protocol = 6
def __init__(self, **kwargs): def __init__(self, include_image=True, duration=None, **kwargs):
""" """
Initialize XBMC/KODI Object Initialize XBMC/KODI Object
""" """
super(NotifyXBMC, self).__init__(**kwargs) super(NotifyXBMC, self).__init__(**kwargs)
# Number of micro-seconds to display notification for # Number of seconds to display notification for
self.duration = 12000 self.duration = self.default_popup_duration_sec \
if not (isinstance(duration, int) and duration > 0) else duration
if self.secure: # Build our schema
self.schema = 'https' self.schema = 'https' if self.secure else 'http'
else:
self.schema = 'http'
# Prepare the default header # Prepare the default header
self.headers = { self.headers = {
@ -100,6 +102,10 @@ class NotifyXBMC(NotifyBase):
# Default protocol # Default protocol
self.protocol = kwargs.get('protocol', self.xbmc_remote_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): def _payload_60(self, title, body, notify_type, **kwargs):
""" """
Builds payload for KODI API v6.0 Builds payload for KODI API v6.0
@ -114,13 +120,17 @@ class NotifyXBMC(NotifyBase):
'params': { 'params': {
'title': title, 'title': title,
'message': body, 'message': body,
# displaytime is defined in microseconds # displaytime is defined in microseconds so we need to just
'displaytime': self.duration, # do some simple math
'displaytime': int(self.duration * 1000),
}, },
'id': 1, '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: if image_url:
payload['params']['image'] = image_url payload['params']['image'] = image_url
if notify_type is NotifyType.FAILURE: if notify_type is NotifyType.FAILURE:
@ -148,13 +158,17 @@ class NotifyXBMC(NotifyBase):
'params': { 'params': {
'title': title, 'title': title,
'message': body, 'message': body,
# displaytime is defined in microseconds # displaytime is defined in microseconds so we need to just
'displaytime': self.duration, # do some simple math
'displaytime': int(self.duration * 1000),
}, },
'id': 1, '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: if image_url:
payload['params']['image'] = image_url payload['params']['image'] = image_url
@ -204,7 +218,7 @@ class NotifyXBMC(NotifyBase):
if r.status_code != requests.codes.ok: if r.status_code != requests.codes.ok:
# We had a problem # We had a problem
status_str = \ status_str = \
NotifyBase.http_response_code_lookup(r.status_code) NotifyXBMC.http_response_code_lookup(r.status_code)
self.logger.warning( self.logger.warning(
'Failed to send XBMC/KODI notification: ' 'Failed to send XBMC/KODI notification: '
@ -242,18 +256,20 @@ class NotifyXBMC(NotifyBase):
args = { args = {
'format': self.notify_format, 'format': self.notify_format,
'overflow': self.overflow_mode, 'overflow': self.overflow_mode,
'image': 'yes' if self.include_image else 'no',
'duration': str(self.duration),
} }
# Determine Authentication # Determine Authentication
auth = '' auth = ''
if self.user and self.password: if self.user and self.password:
auth = '{user}:{password}@'.format( auth = '{user}:{password}@'.format(
user=self.quote(self.user, safe=''), user=NotifyXBMC.quote(self.user, safe=''),
password=self.quote(self.password, safe=''), password=NotifyXBMC.quote(self.password, safe=''),
) )
elif self.user: elif self.user:
auth = '{user}@'.format( auth = '{user}@'.format(
user=self.quote(self.user, safe=''), user=NotifyXBMC.quote(self.user, safe=''),
) )
default_schema = self.xbmc_protocol if ( default_schema = self.xbmc_protocol if (
@ -266,10 +282,10 @@ class NotifyXBMC(NotifyBase):
return '{schema}://{auth}{hostname}{port}/?{args}'.format( return '{schema}://{auth}{hostname}{port}/?{args}'.format(
schema=default_schema, schema=default_schema,
auth=auth, auth=auth,
hostname=self.host, hostname=NotifyXBMC.quote(self.host, safe=''),
port='' if not self.port or self.port == default_port port='' if not self.port or self.port == default_port
else ':{}'.format(self.port), else ':{}'.format(self.port),
args=self.urlencode(args), args=NotifyXBMC.urlencode(args),
) )
@staticmethod @staticmethod
@ -298,4 +314,16 @@ class NotifyXBMC(NotifyBase):
# KODI Support # KODI Support
results['protocol'] = NotifyXBMC.kodi_remote_protocol 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 return results

View File

@ -81,12 +81,6 @@ class NotifyXML(NotifyBase):
</soapenv:Body> </soapenv:Body>
</soapenv:Envelope>""" </soapenv:Envelope>"""
if self.secure:
self.schema = 'https'
else:
self.schema = 'http'
self.fullpath = kwargs.get('fullpath') self.fullpath = kwargs.get('fullpath')
if not isinstance(self.fullpath, six.string_types): if not isinstance(self.fullpath, six.string_types):
self.fullpath = '/' self.fullpath = '/'
@ -116,12 +110,12 @@ class NotifyXML(NotifyBase):
auth = '' auth = ''
if self.user and self.password: if self.user and self.password:
auth = '{user}:{password}@'.format( auth = '{user}:{password}@'.format(
user=self.quote(self.user, safe=''), user=NotifyXML.quote(self.user, safe=''),
password=self.quote(self.password, safe=''), password=NotifyXML.quote(self.password, safe=''),
) )
elif self.user: elif self.user:
auth = '{user}@'.format( auth = '{user}@'.format(
user=self.quote(self.user, safe=''), user=NotifyXML.quote(self.user, safe=''),
) )
default_port = 443 if self.secure else 80 default_port = 443 if self.secure else 80
@ -129,10 +123,10 @@ class NotifyXML(NotifyBase):
return '{schema}://{auth}{hostname}{port}/?{args}'.format( return '{schema}://{auth}{hostname}{port}/?{args}'.format(
schema=self.secure_protocol if self.secure else self.protocol, schema=self.secure_protocol if self.secure else self.protocol,
auth=auth, auth=auth,
hostname=self.host, hostname=NotifyXML.quote(self.host, safe=''),
port='' if self.port is None or self.port == default_port port='' if self.port is None or self.port == default_port
else ':{}'.format(self.port), else ':{}'.format(self.port),
args=self.urlencode(args), args=NotifyXML.urlencode(args),
) )
def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs): def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs):
@ -150,9 +144,10 @@ class NotifyXML(NotifyBase):
headers.update(self.headers) headers.update(self.headers)
re_map = { re_map = {
'{MESSAGE_TYPE}': NotifyBase.quote(notify_type), '{MESSAGE_TYPE}': NotifyXML.escape_html(
'{SUBJECT}': NotifyBase.quote(title), notify_type, whitespace=False),
'{MESSAGE}': NotifyBase.quote(body), '{SUBJECT}': NotifyXML.escape_html(title, whitespace=False),
'{MESSAGE}': NotifyXML.escape_html(body, whitespace=False),
} }
# Iterate over above list and store content accordingly # Iterate over above list and store content accordingly
@ -165,7 +160,10 @@ class NotifyXML(NotifyBase):
if self.user: if self.user:
auth = (self.user, self.password) 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): if isinstance(self.port, int):
url += ':%d' % self.port url += ':%d' % self.port
@ -191,7 +189,7 @@ class NotifyXML(NotifyBase):
if r.status_code != requests.codes.ok: if r.status_code != requests.codes.ok:
# We had a problem # We had a problem
status_str = \ status_str = \
NotifyBase.http_response_code_lookup(r.status_code) NotifyXML.http_response_code_lookup(r.status_code)
self.logger.warning( self.logger.warning(
'Failed to send XML notification: ' 'Failed to send XML notification: '
@ -237,4 +235,8 @@ class NotifyXML(NotifyBase):
results['headers'] = results['qsd-'] results['headers'] = results['qsd-']
results['headers'].update(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 return results

View File

@ -24,7 +24,6 @@
# THE SOFTWARE. # THE SOFTWARE.
import re import re
import six
import ssl import ssl
from os.path import isfile from os.path import isfile
@ -99,7 +98,7 @@ class NotifyXMPP(NotifyBase):
# let me know! :) # let me know! :)
_enabled = NOTIFY_XMPP_SUPPORT_ENABLED _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 Initialize XMPP Object
""" """
@ -177,17 +176,6 @@ class NotifyXMPP(NotifyBase):
else: else:
self.targets = list() 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): def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs):
""" """
Perform XMPP Notification Perform XMPP Notification
@ -302,15 +290,16 @@ class NotifyXMPP(NotifyBase):
} }
if self.jid: if self.jid:
args['jid'] = self.quote(self.jid, safe='') args['jid'] = self.jid
if self.xep: if self.xep:
args['xep'] = self.quote( # xep are integers, so we need to just iterate over a list and
','.join([str(xep) for xep in self.xep]), safe='') # 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 # Target JID(s) can clash with our existing paths, so we just use comma
# and/or space as a delimiters # and/or space as a delimiters - %20 = space
jids = self.quote(' '.join(self.targets), safe='') jids = '%20'.join([NotifyXMPP.quote(x, safe='') for x in self.targets])
default_port = self.default_secure_port \ default_port = self.default_secure_port \
if self.secure else self.default_unsecure_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 default_schema = self.secure_protocol if self.secure else self.protocol
if self.user and self.password: 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: else:
auth = self.password if self.password else self.user auth = self.password if self.password else self.user
return '{schema}://{auth}@{hostname}{port}/{jids}?{args}'.format( return '{schema}://{auth}@{hostname}{port}/{jids}?{args}'.format(
auth=self.quote(auth, safe=''), auth=auth,
schema=default_schema, schema=default_schema,
hostname=self.host, hostname=NotifyXMPP.quote(self.host, safe=''),
port='' if not self.port or self.port == default_port port='' if not self.port or self.port == default_port
else ':{}'.format(self.port), else ':{}'.format(self.port),
jids=jids, jids=jids,
args=self.urlencode(args), args=NotifyXMPP.urlencode(args),
) )
@staticmethod @staticmethod
@ -348,18 +339,20 @@ class NotifyXMPP(NotifyBase):
# Get our targets; we ignore path slashes since they identify # Get our targets; we ignore path slashes since they identify
# our resources # our resources
results['targets'] = parse_list(results['fullpath']) results['targets'] = NotifyXMPP.parse_list(results['fullpath'])
# Over-ride the xep plugins # Over-ride the xep plugins
if 'xep' in results['qsd'] and len(results['qsd']['xep']): 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 # Over-ride the default (and detected) jid
if 'jid' in results['qsd'] and len(results['qsd']['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 # Over-ride the default (and detected) jid
if 'to' in results['qsd'] and len(results['qsd']['to']): 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 return results

View File

@ -101,6 +101,7 @@ def test_notify_gitter_plugin_general(mock_post, mock_get):
obj = plugins.NotifyGitter(token=token, targets='apprise') obj = plugins.NotifyGitter(token=token, targets='apprise')
assert isinstance(obj, plugins.NotifyGitter) is True assert isinstance(obj, plugins.NotifyGitter) is True
assert isinstance(obj.url(), six.string_types) is True assert isinstance(obj.url(), six.string_types) is True
# apprise room was found # apprise room was found
assert obj.send(body="test") is True assert obj.send(body="test") is True

View File

@ -176,6 +176,87 @@ def test_dbus_plugin(mock_mainloop, mock_byte, mock_bytearray,
assert(obj.notify(title='', body='body', assert(obj.notify(title='', body='body',
notify_type=apprise.NotifyType.INFO) is True) 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 # If our underlining object throws for whatever reason, we will
# gracefully fail # gracefully fail
mock_notify = mock.Mock() mock_notify = mock.Mock()

View File

@ -116,24 +116,80 @@ def test_gnome_plugin():
obj.duration = 0 obj.duration = 0
# Check that it found our mocked environments # Check that it found our mocked environments
assert(obj._enabled is True) assert obj._enabled is True
# Test url() call # Test url() call
assert(isinstance(obj.url(), six.string_types) is True) assert isinstance(obj.url(), six.string_types) is True
# test notifications # test notifications
assert(obj.notify(title='title', body='body', assert obj.notify(title='title', body='body',
notify_type=apprise.NotifyType.INFO) is True) notify_type=apprise.NotifyType.INFO) is True
# test notification without a title # test notification without a title
assert(obj.notify(title='', body='body', assert obj.notify(title='', body='body',
notify_type=apprise.NotifyType.INFO) is True) 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 # Test our loading of our icon exception; it will still allow the
# notification to be sent # notification to be sent
mock_pixbuf.new_from_file.side_effect = AttributeError() mock_pixbuf.new_from_file.side_effect = AttributeError()
assert(obj.notify(title='title', body='body', assert obj.notify(title='title', body='body',
notify_type=apprise.NotifyType.INFO) is True) notify_type=apprise.NotifyType.INFO) is True
# Undo our change # Undo our change
mock_pixbuf.new_from_file.side_effect = None mock_pixbuf.new_from_file.side_effect = None
@ -142,8 +198,8 @@ def test_gnome_plugin():
.Notification.new.return_value = None .Notification.new.return_value = None
sys.modules['gi.repository.Notify']\ sys.modules['gi.repository.Notify']\
.Notification.new.side_effect = AttributeError() .Notification.new.side_effect = AttributeError()
assert(obj.notify(title='title', body='body', assert obj.notify(title='title', body='body',
notify_type=apprise.NotifyType.INFO) is False) notify_type=apprise.NotifyType.INFO) is False
# Undo our change # Undo our change
sys.modules['gi.repository.Notify']\ 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 # Toggle our testing for when we can't send notifications because the
# package has been made unavailable to us # package has been made unavailable to us
obj._enabled = False obj._enabled = False
assert(obj.notify(title='title', body='body', assert obj.notify(title='title', body='body',
notify_type=apprise.NotifyType.INFO) is False) notify_type=apprise.NotifyType.INFO) is False
# Test the setting of a the urgency # Test the setting of a the urgency (through priority keyword)
apprise.plugins.NotifyGnome(urgency=0) apprise.plugins.NotifyGnome(priority=0)
# Verify this all works in the event a ValueError is also thronw # Verify this all works in the event a ValueError is also thronw
# out of the call to gi.require_version() # out of the call to gi.require_version()
@ -178,10 +234,10 @@ def test_gnome_plugin():
# Create our instance # Create our instance
obj = apprise.Apprise.instantiate('gnome://', suppress_exceptions=False) 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 obj.duration = 0
# Our notifications can not work without our gi library having been # Our notifications can not work without our gi library having been
# loaded. # loaded.
assert(obj.notify(title='title', body='body', assert obj.notify(title='title', body='body',
notify_type=apprise.NotifyType.INFO) is False) notify_type=apprise.NotifyType.INFO) is False

View File

@ -61,25 +61,25 @@ def test_notify_matrix_plugin_general(mock_post, mock_get):
mock_post.return_value = request mock_post.return_value = request
# Variation Initializations # Variation Initializations
obj = plugins.NotifyMatrix(rooms='#abcd') obj = plugins.NotifyMatrix(targets='#abcd')
assert isinstance(obj, plugins.NotifyMatrix) is True assert isinstance(obj, plugins.NotifyMatrix) is True
assert isinstance(obj.url(), six.string_types) is True assert isinstance(obj.url(), six.string_types) is True
# Registration successful # Registration successful
assert obj.send(body="test") is True 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, plugins.NotifyMatrix) is True
assert isinstance(obj.url(), six.string_types) is True assert isinstance(obj.url(), six.string_types) is True
# Registration successful # Registration successful
assert obj.send(body="test") is True 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, plugins.NotifyMatrix) is True
assert isinstance(obj.url(), six.string_types) is True assert isinstance(obj.url(), six.string_types) is True
# A username gets automatically generated in these cases # A username gets automatically generated in these cases
assert obj.send(body="test") is True 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.url(), six.string_types) is True
assert isinstance(obj, plugins.NotifyMatrix) is True assert isinstance(obj, plugins.NotifyMatrix) is True
# Registration Successful # 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 # Fails because we couldn't register because of 404 errors
assert obj.send(body="test") is False 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 assert isinstance(obj, plugins.NotifyMatrix) is True
# Fails because we still couldn't register # Fails because we still couldn't register
assert obj.send(user='test', password='passwd', body="test") is False 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 assert isinstance(obj, plugins.NotifyMatrix) is True
# Fails because we still couldn't register # Fails because we still couldn't register
assert obj.send(body="test") is False 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 # Fails because we still couldn't register
assert isinstance(obj, plugins.NotifyMatrix) is True assert isinstance(obj, plugins.NotifyMatrix) is True
assert obj.send(body="test") is False 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.content = dumps(response_obj)
request.status_code = requests.codes.ok request.status_code = requests.codes.ok
obj = plugins.NotifyMatrix(rooms=None) obj = plugins.NotifyMatrix(targets=None)
assert isinstance(obj, plugins.NotifyMatrix) is True assert isinstance(obj, plugins.NotifyMatrix) is True
# Force a empty joined list response # 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_get.side_effect = fetch_failed
mock_post.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 assert isinstance(obj, plugins.NotifyMatrix) is True
# We would hve failed to send our image notification # We would hve failed to send our image notification
assert obj.send(user='test', password='passwd', body="test") is False 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 request.status_code = 403
obj._room_cache = {} obj._room_cache = {}
assert obj._room_id('#abc123:localhost') is None 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']

View File

@ -194,13 +194,54 @@ def test_notify_base():
"<content>'\t \n</content>", convert_new_lines=True) == \ "<content>'\t \n</content>", convert_new_lines=True) == \
'&lt;content&gt;&apos;&emsp;&nbsp;&lt;br/&gt;&lt;/content&gt;' '&lt;content&gt;&apos;&emsp;&nbsp;&lt;br/&gt;&lt;/content&gt;'
# Test invalid data
assert NotifyBase.split_path(None) == []
assert NotifyBase.split_path(object()) == []
assert NotifyBase.split_path(42) == []
assert NotifyBase.split_path( assert NotifyBase.split_path(
'/path/?name=Dr%20Disrespect', unquote=False) == \ '/path/?name=Dr%20Disrespect', unquote=False) == \
['path', '?name=Dr%20Disrespect'] ['path', '?name=Dr%20Disrespect']
assert NotifyBase.split_path( assert NotifyBase.split_path(
'/path/?name=Dr%20Disrespect', unquote=True) == \ '/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 # Give nothing, get nothing
assert NotifyBase.escape_html("") == "" assert NotifyBase.escape_html("") == ""

View File

@ -45,9 +45,12 @@ TEST_URLS = (
('pjets://', { ('pjets://', {
'instance': None, 'instance': None,
}), }),
('pjet://:@/', {
'instance': None,
}),
# You must specify a username # You must specify a username
('pjet://%s' % ('a' * 32), { ('pjet://%s' % ('a' * 32), {
'instance': None, 'instance': TypeError,
}), }),
# Specify your own server # Specify your own server
('pjet://%s@localhost' % ('a' * 32), { ('pjet://%s@localhost' % ('a' * 32), {
@ -57,9 +60,6 @@ TEST_URLS = (
('pjets://%s@localhost:8080' % ('a' * 32), { ('pjets://%s@localhost:8080' % ('a' * 32), {
'instance': plugins.NotifyPushjet, 'instance': plugins.NotifyPushjet,
}), }),
('pjet://:@/', {
'instance': None,
}),
('pjet://%s@localhost:8081' % ('a' * 32), { ('pjet://%s@localhost:8081' % ('a' * 32), {
'instance': plugins.NotifyPushjet, 'instance': plugins.NotifyPushjet,
# Throws a series of connection and transfer exceptions when this flag # Throws a series of connection and transfer exceptions when this flag

File diff suppressed because it is too large Load Diff

View File

@ -51,7 +51,7 @@ def test_object_initialization():
access_key_id=None, access_key_id=None,
secret_access_key=TEST_ACCESS_KEY_SECRET, secret_access_key=TEST_ACCESS_KEY_SECRET,
region_name=TEST_REGION, region_name=TEST_REGION,
recipients='+1800555999', targets='+1800555999',
) )
# The entries above are invalid, our code should never reach here # The entries above are invalid, our code should never reach here
assert(False) assert(False)
@ -66,7 +66,7 @@ def test_object_initialization():
access_key_id=TEST_ACCESS_KEY_ID, access_key_id=TEST_ACCESS_KEY_ID,
secret_access_key=None, secret_access_key=None,
region_name=TEST_REGION, region_name=TEST_REGION,
recipients='+1800555999', targets='+1800555999',
) )
# The entries above are invalid, our code should never reach here # The entries above are invalid, our code should never reach here
assert(False) assert(False)
@ -81,7 +81,7 @@ def test_object_initialization():
access_key_id=TEST_ACCESS_KEY_ID, access_key_id=TEST_ACCESS_KEY_ID,
secret_access_key=TEST_ACCESS_KEY_SECRET, secret_access_key=TEST_ACCESS_KEY_SECRET,
region_name=None, region_name=None,
recipients='+1800555999', targets='+1800555999',
) )
# The entries above are invalid, our code should never reach here # The entries above are invalid, our code should never reach here
assert(False) assert(False)
@ -96,7 +96,7 @@ def test_object_initialization():
access_key_id=TEST_ACCESS_KEY_ID, access_key_id=TEST_ACCESS_KEY_ID,
secret_access_key=TEST_ACCESS_KEY_SECRET, secret_access_key=TEST_ACCESS_KEY_SECRET,
region_name=TEST_REGION, region_name=TEST_REGION,
recipients=None, targets=None,
) )
# Still valid even without recipients # Still valid even without recipients
assert(True) assert(True)
@ -111,7 +111,7 @@ def test_object_initialization():
access_key_id=TEST_ACCESS_KEY_ID, access_key_id=TEST_ACCESS_KEY_ID,
secret_access_key=TEST_ACCESS_KEY_SECRET, secret_access_key=TEST_ACCESS_KEY_SECRET,
region_name=TEST_REGION, region_name=TEST_REGION,
recipients=object(), targets=object(),
) )
# Still valid even without recipients # Still valid even without recipients
assert(True) assert(True)
@ -127,7 +127,7 @@ def test_object_initialization():
access_key_id=TEST_ACCESS_KEY_ID, access_key_id=TEST_ACCESS_KEY_ID,
secret_access_key=TEST_ACCESS_KEY_SECRET, secret_access_key=TEST_ACCESS_KEY_SECRET,
region_name=TEST_REGION, region_name=TEST_REGION,
recipients='+1809', targets='+1809',
) )
# The recipient is invalid, but it's still okay; this Notification # The recipient is invalid, but it's still okay; this Notification
# still becomes pretty much useless at this point though # still becomes pretty much useless at this point though
@ -144,7 +144,7 @@ def test_object_initialization():
access_key_id=TEST_ACCESS_KEY_ID, access_key_id=TEST_ACCESS_KEY_ID,
secret_access_key=TEST_ACCESS_KEY_SECRET, secret_access_key=TEST_ACCESS_KEY_SECRET,
region_name=TEST_REGION, 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 # The recipient is invalid, but it's still okay; this Notification
# still becomes pretty much useless at this point though # still becomes pretty much useless at this point though
@ -169,7 +169,7 @@ def test_url_parsing():
) )
# Confirm that there were no recipients found # Confirm that there were no recipients found
assert(len(results['recipients']) == 0) assert(len(results['targets']) == 0)
assert('region_name' in results) assert('region_name' in results)
assert(TEST_REGION == results['region_name']) assert(TEST_REGION == results['region_name'])
assert('access_key_id' in results) assert('access_key_id' in results)
@ -188,9 +188,9 @@ def test_url_parsing():
) )
# Confirm that our recipients were found # Confirm that our recipients were found
assert(len(results['recipients']) == 2) assert(len(results['targets']) == 2)
assert('+18001234567' in results['recipients']) assert('+18001234567' in results['targets'])
assert('MyTopic' in results['recipients']) assert('MyTopic' in results['targets'])
assert('region_name' in results) assert('region_name' in results)
assert(TEST_REGION == results['region_name']) assert(TEST_REGION == results['region_name'])
assert('access_key_id' in results) assert('access_key_id' in results)

View File

@ -23,10 +23,16 @@
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE. # 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 plugins
from apprise import NotifyType from apprise import NotifyType
from apprise import Apprise from apprise import Apprise
import mock from apprise import OverflowMode
# Disable logging for a cleaner testing output # Disable logging for a cleaner testing output
import logging import logging
@ -40,6 +46,9 @@ TEST_URLS = (
('tweet://', { ('tweet://', {
'instance': None, 'instance': None,
}), }),
('tweet://:@/', {
'instance': None,
}),
('tweet://consumer_key', { ('tweet://consumer_key', {
# Missing Keys # Missing Keys
'instance': TypeError, 'instance': TypeError,
@ -60,9 +69,11 @@ TEST_URLS = (
# We're good! # We're good!
'instance': plugins.NotifyTwitter, 'instance': plugins.NotifyTwitter,
}), }),
('tweet://:@/', { ('tweet://usera@consumer_key/consumer_key/access_token/'
'instance': None, '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) 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 # iterate over our dictionary and test it out
for (url, meta) in TEST_URLS: 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) # Our expected Query response (True, False, or exception type)
response = meta.get('response', True) 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 # Allow us to force the server response code to be something other then
# the defaults # the defaults
response = meta.get( response = meta.get(
@ -94,25 +124,69 @@ def test_plugin(mock_oauth, mock_api):
try: try:
obj = Apprise.instantiate(url, suppress_exceptions=False) obj = Apprise.instantiate(url, suppress_exceptions=False)
if instance is None: if obj is None:
# Check that we got what we came for if instance is not None:
assert obj is instance # We're done (assuming this is what we were expecting)
print("{} didn't instantiate itself "
"(we expected it to)".format(url))
assert False
continue 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: if self:
# Iterate over our expected entries inside of our object # Iterate over our expected entries inside of our object
for key, val in self.items(): for key, val in self.items():
# Test that our object has the desired key # Test that our object has the desired key
assert(hasattr(key, obj)) assert hasattr(key, obj) is True
assert(getattr(key, obj) == val) assert getattr(key, obj) == val
obj.request_rate_per_sec = 0
# check that we're as expected # check that we're as expected
assert obj.notify( assert obj.notify(
title='test', body='body', title='test', body='body',
notify_type=NotifyType.INFO) == response 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: except AssertionError:
# Don't mess with these entries # Don't mess with these entries
raise raise

View File

@ -125,6 +125,43 @@ def test_windows_plugin():
assert(obj.notify(title='title', body='body', assert(obj.notify(title='title', body='body',
notify_type=apprise.NotifyType.INFO) is True) 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 # Test our loading of our icon exception; it will still allow the
# notification to be sent # notification to be sent
win32gui.LoadImage.side_effect = AttributeError win32gui.LoadImage.side_effect = AttributeError

View File

@ -26,7 +26,7 @@
import six import six
import mock import mock
import sys import sys
# import types import ssl
import apprise import apprise
@ -129,6 +129,39 @@ def test_xmpp_plugin(tmpdir):
# Not possible because no password was specified # Not possible because no password was specified
assert obj is None 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 # Try Different Variations of our URL
for url in ( for url in (
'xmpps://user:pass@example.com', 'xmpps://user:pass@example.com',