normalize plugin classes mass code cleanup

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

View File

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

View File

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

View File

@ -30,6 +30,7 @@ from .NotifyBase import NotifyBase
from ..common import NotifyImageSize
from ..common import NotifyType
from ..utils import GET_SCHEMA_RE
from ..utils import parse_bool
# Default our global support flag
NOTIFY_DBUS_SUPPORT_ENABLED = False
@ -170,7 +171,8 @@ class NotifyDBus(NotifyBase):
# let me know! :)
_enabled = NOTIFY_DBUS_SUPPORT_ENABLED
def __init__(self, urgency=None, x_axis=None, y_axis=None, **kwargs):
def __init__(self, urgency=None, x_axis=None, y_axis=None,
include_image=True, **kwargs):
"""
Initialize DBus Object
"""
@ -184,13 +186,10 @@ class NotifyDBus(NotifyBase):
self.schema = kwargs.get('schema', 'dbus')
if self.schema not in MAINLOOP_MAP:
# Unsupported Schema
self.logger.warning(
'The schema specified ({}) is not supported.'
.format(self.schema))
raise TypeError(
'The schema specified ({}) is not supported.'
.format(self.schema))
msg = 'The schema specified ({}) is not supported.' \
.format(self.schema)
self.logger.warning(msg)
raise TypeError(msg)
# The urgency of the message
if urgency not in DBUS_URGENCIES:
@ -200,8 +199,12 @@ class NotifyDBus(NotifyBase):
self.urgency = urgency
# Our x/y axis settings
self.x_axis = x_axis
self.y_axis = y_axis
self.x_axis = x_axis if isinstance(x_axis, int) else None
self.y_axis = y_axis if isinstance(y_axis, int) else None
# Track whether or not we want to send an image with our notification
# or not.
self.include_image = include_image
def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs):
"""
@ -229,7 +232,8 @@ class NotifyDBus(NotifyBase):
)
# image path
icon_path = self.image_path(notify_type, extension='.ico')
icon_path = None if not self.include_image \
else self.image_path(notify_type, extension='.ico')
# Our meta payload
meta_payload = {
@ -241,7 +245,7 @@ class NotifyDBus(NotifyBase):
meta_payload['x'] = self.x_axis
meta_payload['y'] = self.y_axis
if NOTIFY_DBUS_IMAGE_SUPPORT is True:
if NOTIFY_DBUS_IMAGE_SUPPORT and icon_path:
try:
# Use Pixbuf to create the proper image type
image = GdkPixbuf.Pixbuf.new_from_file(icon_path)
@ -299,7 +303,33 @@ class NotifyDBus(NotifyBase):
Returns the URL built dynamically based on specified arguments.
"""
return '{schema}://'.format(schema=self.schema)
_map = {
DBusUrgency.LOW: 'low',
DBusUrgency.NORMAL: 'normal',
DBusUrgency.HIGH: 'high',
}
# Define any arguments set
args = {
'format': self.notify_format,
'overflow': self.overflow_mode,
'image': 'yes' if self.include_image else 'no',
'urgency': 'normal' if self.urgency not in _map
else _map[self.urgency]
}
# x in (x,y) screen coordinates
if self.x_axis:
args['x'] = str(self.x_axis)
# y in (x,y) screen coordinates
if self.y_axis:
args['y'] = str(self.y_axis)
return '{schema}://_/?{args}'.format(
schema=self.protocol,
args=NotifyDBus.urlencode(args),
)
@staticmethod
def parse_url(url):
@ -314,23 +344,58 @@ class NotifyDBus(NotifyBase):
# Content is simply not parseable
return None
# return a very basic set of requirements
return {
results = NotifyBase.parse_url(url)
if not results:
results = {
'schema': schema.group('schema').lower(),
'user': None,
'password': None,
'port': None,
'host': 'localhost',
'host': '_',
'fullpath': None,
'path': None,
'url': url,
'qsd': {},
# screen lat/lon (in pixels) where x=0 and y=0 if you want to put
# the notification in the top left hand side. Accept defaults if
# set to None
'x_axis': None,
'y_axis': None,
# Set the urgency to None so that we fall back to the default
# value.
'urgency': None,
}
# Include images with our message
results['include_image'] = \
parse_bool(results['qsd'].get('image', True))
# DBus supports urgency, but we we also support the keyword priority
# so that it is consistent with some of the other plugins
urgency = results['qsd'].get('urgency', results['qsd'].get('priority'))
if urgency and len(urgency):
_map = {
'0': DBusUrgency.LOW,
'l': DBusUrgency.LOW,
'n': DBusUrgency.NORMAL,
'1': DBusUrgency.NORMAL,
'h': DBusUrgency.HIGH,
'2': DBusUrgency.HIGH,
}
try:
# Attempt to index/retrieve our urgency
results['urgency'] = _map[urgency[0].lower()]
except KeyError:
# No priority was set
pass
# handle x,y coordinates
try:
results['x_axis'] = int(results['qsd'].get('x'))
except (TypeError, ValueError):
# No x was set
pass
try:
results['y_axis'] = int(results['qsd'].get('y'))
except (TypeError, ValueError):
# No y was set
pass
return results

View File

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

View File

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

View File

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

View File

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

View File

@ -25,15 +25,17 @@
# To use this plugin, you need to first access https://dev.flock.com/webhooks
# Specifically https://dev.flock.com/webhooks/incoming
# to create a new incoming webhook for your account. You'll need to
#
# To create a new incoming webhook for your account. You'll need to
# follow the wizard to pre-determine the channel(s) you want your
# message to broadcast to, and when you're complete, you will
# message to broadcast to. When you've completed this, you will
# recieve a URL that looks something like this:
# https://api.flock.com/hooks/sendMessage/134b8gh0-eba0-4fa9-ab9c-257ced0e8221
# ^
# |
# This is important <----------------------------------------^
#
# It becomes your 'token' that you will pass into this class
#
import re
import requests
@ -44,6 +46,7 @@ from ..common import NotifyType
from ..common import NotifyFormat
from ..common import NotifyImageSize
from ..utils import parse_list
from ..utils import parse_bool
# Extend HTTP Error Messages
@ -89,7 +92,7 @@ class NotifyFlock(NotifyBase):
# Allows the user to specify the NotifyImageSize object
image_size = NotifyImageSize.XY_72
def __init__(self, token, targets=None, **kwargs):
def __init__(self, token, targets=None, include_image=True, **kwargs):
"""
Initialize Flock Object
"""
@ -134,6 +137,10 @@ class NotifyFlock(NotifyBase):
self.logger.warning(msg)
raise TypeError(msg)
# Track whether or not we want to send an image with our notification
# or not.
self.include_image = include_image
def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs):
"""
Perform Flock Notification
@ -151,8 +158,8 @@ class NotifyFlock(NotifyBase):
body = '<flockml>{}</flockml>'.format(body)
else:
title = NotifyBase.escape_html(title, whitespace=False)
body = NotifyBase.escape_html(body, whitespace=False)
title = NotifyFlock.escape_html(title, whitespace=False)
body = NotifyFlock.escape_html(body, whitespace=False)
body = '<flockml>{}{}</flockml>'.format(
'' if not title else '<b>{}</b><br/>'.format(title), body)
@ -162,7 +169,10 @@ class NotifyFlock(NotifyBase):
'flockml': body,
'sendAs': {
'name': FLOCK_DEFAULT_USER if not self.user else self.user,
'profileImage': self.image_url(notify_type),
# A Profile Image is only configured if we're configured to
# allow it
'profileImage': None if not self.include_image
else self.image_url(notify_type),
}
}
@ -213,7 +223,7 @@ class NotifyFlock(NotifyBase):
if r.status_code != requests.codes.ok:
# We had a problem
status_str = \
NotifyBase.http_response_code_lookup(
NotifyFlock.http_response_code_lookup(
r.status_code, FLOCK_HTTP_ERROR_MAP)
self.logger.warning(
@ -251,15 +261,17 @@ class NotifyFlock(NotifyBase):
args = {
'format': self.notify_format,
'overflow': self.overflow_mode,
'image': 'yes' if self.include_image else 'no',
}
return '{schema}://{token}/{targets}?{args}'\
.format(
schema=self.secure_protocol,
token=self.quote(self.token, safe=''),
token=NotifyFlock.quote(self.token, safe=''),
targets='/'.join(
[self.quote(target, safe='') for target in self.targets]),
args=self.urlencode(args),
[NotifyFlock.quote(target, safe='')
for target in self.targets]),
args=NotifyFlock.urlencode(args),
)
@staticmethod
@ -274,12 +286,19 @@ class NotifyFlock(NotifyBase):
# We're done early as we couldn't load the results
return results
# Apply our settings now
# Get our entries; split_path() looks after unquoting content for us
# by default
results['targets'] = NotifyFlock.split_path(results['fullpath'])
results['targets'] = [x for x in filter(
bool, NotifyBase.split_path(results['fullpath']))]
# The 'to' makes it easier to use yaml configuration
if 'to' in results['qsd'] and len(results['qsd']['to']):
results['targets'] += NotifyFlock.parse_list(results['qsd']['to'])
# The first token is stored in the hostname
results['token'] = results['host']
results['token'] = NotifyFlock.unquote(results['host'])
# Include images with our message
results['include_image'] = \
parse_bool(results['qsd'].get('image', True))
return results

View File

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

View File

@ -29,6 +29,7 @@ from __future__ import print_function
from .NotifyBase import NotifyBase
from ..common import NotifyImageSize
from ..common import NotifyType
from ..utils import parse_bool
# Default our global support flag
NOTIFY_GNOME_SUPPORT_ENABLED = False
@ -109,7 +110,7 @@ class NotifyGnome(NotifyBase):
# let me know! :)
_enabled = NOTIFY_GNOME_SUPPORT_ENABLED
def __init__(self, urgency=None, **kwargs):
def __init__(self, urgency=None, include_image=True, **kwargs):
"""
Initialize Gnome Object
"""
@ -123,6 +124,10 @@ class NotifyGnome(NotifyBase):
else:
self.urgency = urgency
# Track whether or not we want to send an image with our notification
# or not.
self.include_image = include_image
def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs):
"""
Perform Gnome Notification
@ -138,7 +143,8 @@ class NotifyGnome(NotifyBase):
Notify.init(self.app_id)
# image path
icon_path = self.image_path(notify_type, extension='.ico')
icon_path = None if not self.include_image \
else self.image_path(notify_type, extension='.ico')
# Build message body
notification = Notify.Notification.new(body)
@ -149,6 +155,7 @@ class NotifyGnome(NotifyBase):
# Always call throttle before any remote server i/o is made
self.throttle()
if icon_path:
try:
# Use Pixbuf to create the proper image type
image = GdkPixbuf.Pixbuf.new_from_file(icon_path)
@ -177,7 +184,25 @@ class NotifyGnome(NotifyBase):
Returns the URL built dynamically based on specified arguments.
"""
return '{schema}://'.format(schema=self.protocol)
_map = {
GnomeUrgency.LOW: 'low',
GnomeUrgency.NORMAL: 'normal',
GnomeUrgency.HIGH: 'high',
}
# Define any arguments set
args = {
'format': self.notify_format,
'overflow': self.overflow_mode,
'image': 'yes' if self.include_image else 'no',
'urgency': 'normal' if self.urgency not in _map
else _map[self.urgency]
}
return '{schema}://_/?{args}'.format(
schema=self.protocol,
args=NotifyGnome.urlencode(args),
)
@staticmethod
def parse_url(url):
@ -188,18 +213,43 @@ class NotifyGnome(NotifyBase):
"""
# return a very basic set of requirements
return {
results = NotifyBase.parse_url(url)
if not results:
results = {
'schema': NotifyGnome.protocol,
'user': None,
'password': None,
'port': None,
'host': 'localhost',
'host': '_',
'fullpath': None,
'path': None,
'url': url,
'qsd': {},
# Set the urgency to None so that we fall back to the default
# 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
# authenticate with our Gotify server. The same goes for the
# user id below.
self.access_token = token
self.token = token
return
@ -121,12 +121,12 @@ class NotifyGotify(NotifyBase):
# Define our parameteers
params = {
'token': self.access_token,
'token': self.token,
}
# Prepare Gotify Object
payload = {
'priority': 2,
'priority': self.priority,
'title': title,
'message': body,
}
@ -156,7 +156,7 @@ class NotifyGotify(NotifyBase):
if r.status_code != requests.codes.ok:
# We had a problem
status_str = \
NotifyBase.http_response_code_lookup(r.status_code)
NotifyGotify.http_response_code_lookup(r.status_code)
self.logger.warning(
'Failed to send Gotify notification: '
@ -201,11 +201,11 @@ class NotifyGotify(NotifyBase):
return '{schema}://{hostname}{port}/{token}/?{args}'.format(
schema=self.secure_protocol if self.secure else self.protocol,
hostname=self.host,
hostname=NotifyGotify.quote(self.host, safe=''),
port='' if self.port is None or self.port == default_port
else ':{}'.format(self.port),
token=self.access_token,
args=self.urlencode(args),
token=NotifyGotify.quote(self.token, safe=''),
args=NotifyGotify.urlencode(args),
)
@staticmethod
@ -220,13 +220,17 @@ class NotifyGotify(NotifyBase):
# We're done early
return results
# Retrieve our escaped entries found on the fullpath
entries = NotifyBase.split_path(results['fullpath'])
# optionally find the provider key
try:
token = [x for x in filter(
bool, NotifyBase.split_path(results['fullpath']))][0]
# The first entry is our token
results['token'] = entries.pop(0)
except (AttributeError, IndexError):
token = None
except IndexError:
# No token was set
results['token'] = None
if 'priority' in results['qsd'] and len(results['qsd']['priority']):
_map = {
@ -244,7 +248,4 @@ class NotifyGotify(NotifyBase):
# No priority was set
pass
# Set our token
results['token'] = token
return results

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -30,6 +30,8 @@ from json import dumps
from .NotifyBase import NotifyBase
from ..common import NotifyImageSize
from ..common import NotifyType
from ..utils import parse_bool
from ..utils import parse_list
# Some Reference Locations:
# - https://docs.mattermost.com/developer/webhooks-incoming.html
@ -71,7 +73,8 @@ class NotifyMatterMost(NotifyBase):
# Mattermost does not have a title
title_maxlen = 0
def __init__(self, authtoken, channel=None, **kwargs):
def __init__(self, authtoken, channels=None, include_image=True,
**kwargs):
"""
Initialize MatterMost Object
"""
@ -88,27 +91,24 @@ class NotifyMatterMost(NotifyBase):
# Validate authtoken
if not authtoken:
self.logger.warning(
'Missing MatterMost Authorization Token.'
)
raise TypeError(
'Missing MatterMost Authorization Token.'
)
msg = 'Missing MatterMost Authorization Token.'
self.logger.warning(msg)
raise TypeError(msg)
if not VALIDATE_AUTHTOKEN.match(authtoken):
self.logger.warning(
'Invalid MatterMost Authorization Token Specified.'
)
raise TypeError(
'Invalid MatterMost Authorization Token Specified.'
)
msg = 'Invalid MatterMost Authorization Token Specified.'
self.logger.warning(msg)
raise TypeError(msg)
# A Channel (optional)
self.channel = channel
# Optional Channels
self.channels = parse_list(channels)
if not self.port:
self.port = self.default_port
# Place a thumbnail image inline with the message body
self.include_image = include_image
return
def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs):
@ -116,6 +116,9 @@ class NotifyMatterMost(NotifyBase):
Perform MatterMost Notification
"""
# Create a copy of our channels, otherwise place a dummy entry
channels = list(self.channels) if self.channels else [None, ]
headers = {
'User-Agent': self.app_id,
'Content-Type': 'application/json'
@ -124,17 +127,29 @@ class NotifyMatterMost(NotifyBase):
# prepare JSON Object
payload = {
'text': body,
'icon_url': self.image_url(notify_type),
'icon_url': None,
}
if self.user:
payload['username'] = self.user
# Acquire our image url if configured to do so
image_url = None if not self.include_image \
else self.image_url(notify_type)
else:
payload['username'] = self.app_id
if image_url:
# Set our image configuration if told to do so
payload['icon_url'] = image_url
if self.channel:
payload['channel'] = self.channel
# Set our user
payload['username'] = self.user if self.user else self.app_id
# For error tracking
has_error = False
while len(channels):
# Pop a channel off of the list
channel = channels.pop(0)
if channel:
payload['channel'] = channel
url = '%s://%s:%d' % (self.schema, self.host, self.port)
url += '/hooks/%s' % self.authtoken
@ -154,37 +169,49 @@ class NotifyMatterMost(NotifyBase):
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)
NotifyMatterMost.http_response_code_lookup(
r.status_code)
self.logger.warning(
'Failed to send MatterMost notification: '
'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))
self.logger.debug(
'Response Details:\r\n{}'.format(r.content))
# Return; we're done
return False
# Flag our error
has_error = True
continue
else:
self.logger.info('Sent MatterMost notification.')
self.logger.info(
'Sent MatterMost notification{}.'.format(
'' if not channel
else ' to channel {}'.format(channel)))
except requests.RequestException as e:
self.logger.warning(
'A Connection error occured sending MatterMost '
'notification.'
)
'notification{}.'.format(
'' if not channel
else ' to channel {}'.format(channel)))
self.logger.debug('Socket Exception: %s' % str(e))
# Return; we're done
return False
# Flag our error
has_error = True
continue
return True
# Return our overall status
return not has_error
def url(self):
"""
@ -195,18 +222,25 @@ class NotifyMatterMost(NotifyBase):
args = {
'format': self.notify_format,
'overflow': self.overflow_mode,
'image': 'yes' if self.include_image else 'no',
}
if self.channels:
# historically the value only accepted one channel and is
# therefore identified as 'channel'. Channels have always been
# optional, so that is why this setting is nested in an if block
args['channel'] = ','.join(self.channels)
default_port = 443 if self.secure else self.default_port
default_schema = self.secure_protocol if self.secure else self.protocol
return '{schema}://{hostname}{port}/{authtoken}/?{args}'.format(
schema=default_schema,
hostname=self.host,
hostname=NotifyMatterMost.quote(self.host, safe=''),
port='' if not self.port or self.port == default_port
else ':{}'.format(self.port),
authtoken=self.quote(self.authtoken, safe=''),
args=self.urlencode(args),
authtoken=NotifyMatterMost.quote(self.authtoken, safe=''),
args=NotifyMatterMost.urlencode(args),
)
@staticmethod
@ -222,15 +256,31 @@ class NotifyMatterMost(NotifyBase):
# We're done early as we couldn't load the results
return results
try:
# Apply our settings now
authtoken = NotifyBase.split_path(results['fullpath'])[0]
results['authtoken'] = \
NotifyMatterMost.split_path(results['fullpath'])[0]
except IndexError:
# There was no Authorization Token specified
results['authtoken'] = None
# Define our optional list of channels to notify
results['channels'] = list()
# Support both 'to' (for yaml configuration) and channel=
if 'to' in results['qsd'] and len(results['qsd']['to']):
# Allow the user to specify the channel to post to
results['channels'].append(
NotifyMatterMost.parse_list(results['qsd']['to']))
channel = None
if 'channel' in results['qsd'] and len(results['qsd']['channel']):
# Allow the user to specify the channel to post to
channel = NotifyBase.unquote(results['qsd']['channel']).strip()
results['channels'].append(
NotifyMatterMost.parse_list(results['qsd']['channel']))
results['authtoken'] = authtoken
results['channel'] = channel
# Image manipulation
results['include_image'] = \
parse_bool(results['qsd'].get('image', False))
return results

View File

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

View File

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

View File

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

View File

@ -62,6 +62,12 @@ class NotifyPushjet(NotifyBase):
"""
super(NotifyPushjet, self).__init__(**kwargs)
if not secret_key:
# You must provide a Pushjet key to work with
msg = 'You must specify a Pushjet Secret Key.'
self.logger.warning(msg)
raise TypeError(msg)
# store our key
self.secret_key = secret_key
@ -107,11 +113,11 @@ class NotifyPushjet(NotifyBase):
return '{schema}://{secret_key}@{hostname}{port}/?{args}'.format(
schema=self.secure_protocol if self.secure else self.protocol,
secret_key=self.quote(self.secret_key, safe=''),
hostname=self.host,
secret_key=NotifyPushjet.quote(self.secret_key, safe=''),
hostname=NotifyPushjet.quote(self.host, safe=''),
port='' if self.port is None or self.port == default_port
else ':{}'.format(self.port),
args=self.urlencode(args),
args=NotifyPushjet.urlencode(args),
)
@staticmethod
@ -133,11 +139,8 @@ class NotifyPushjet(NotifyBase):
# We're done early as we couldn't load the results
return results
if not results.get('user'):
# a username is required
return None
# Store it as it's value
results['secret_key'] = results.get('user')
results['secret_key'] = \
NotifyPushjet.unquote(results.get('user'))
return results

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -107,7 +107,7 @@ class NotifyTelegram(NotifyBase):
# The maximum allowable characters allowed in the body per message
body_maxlen = 4096
def __init__(self, bot_token, chat_ids, detect_bot_owner=True,
def __init__(self, bot_token, targets, detect_bot_owner=True,
include_image=True, **kwargs):
"""
Initialize Telegram Object
@ -133,19 +133,19 @@ class NotifyTelegram(NotifyBase):
self.bot_token = result.group('key')
# Parse our list
self.chat_ids = parse_list(chat_ids)
self.targets = parse_list(targets)
if self.user:
# Treat this as a channel too
self.chat_ids.append(self.user)
self.targets.append(self.user)
if len(self.chat_ids) == 0 and detect_bot_owner:
if len(self.targets) == 0 and detect_bot_owner:
_id = self.detect_bot_owner()
if _id:
# Store our id
self.chat_ids.append(str(_id))
self.targets.append(str(_id))
if len(self.chat_ids) == 0:
if len(self.targets) == 0:
err = 'No chat_id(s) were specified.'
self.logger.warning(err)
raise TypeError(err)
@ -168,14 +168,25 @@ class NotifyTelegram(NotifyBase):
'sendPhoto'
)
# Acquire our image path if configured to do so; we don't bother
# checking to see if selfinclude_image is set here because the
# send_image() function itself (this function) checks this flag
# already
path = self.image_path(notify_type)
if not path:
# No image to send
self.logger.debug(
'Telegram Image does not exist for %s' % (notify_type))
return None
files = {'photo': (basename(path), open(path), 'rb')}
# No need to fail; we may have been configured this way through
# the apprise.AssetObject()
return True
# Configure file payload (for upload)
files = {
'photo': (basename(path), open(path), 'rb'),
}
payload = {
'chat_id': chat_id,
@ -196,7 +207,7 @@ class NotifyTelegram(NotifyBase):
if r.status_code != requests.codes.ok:
# We had a problem
status_str = \
NotifyBase.http_response_code_lookup(r.status_code)
NotifyTelegram.http_response_code_lookup(r.status_code)
self.logger.warning(
'Failed to send Telegram Image: '
@ -248,7 +259,7 @@ class NotifyTelegram(NotifyBase):
if r.status_code != requests.codes.ok:
# We had a problem
status_str = \
NotifyBase.http_response_code_lookup(r.status_code)
NotifyTelegram.http_response_code_lookup(r.status_code)
try:
# Try to get the error message if we can:
@ -368,10 +379,10 @@ class NotifyTelegram(NotifyBase):
title = re.sub('&emsp;?', ' ', title, re.I)
# HTML
title = NotifyBase.escape_html(title, whitespace=False)
title = NotifyTelegram.escape_html(title, whitespace=False)
# HTML
body = NotifyBase.escape_html(body, whitespace=False)
body = NotifyTelegram.escape_html(body, whitespace=False)
if title and self.notify_format == NotifyFormat.TEXT:
# Text HTML Formatting
@ -393,9 +404,9 @@ class NotifyTelegram(NotifyBase):
payload['text'] = body
# Create a copy of the chat_ids list
chat_ids = list(self.chat_ids)
while len(chat_ids):
chat_id = chat_ids.pop(0)
targets = list(self.targets)
while len(targets):
chat_id = targets.pop(0)
chat_id = IS_CHAT_ID_RE.match(chat_id)
if not chat_id:
self.logger.warning(
@ -441,7 +452,7 @@ class NotifyTelegram(NotifyBase):
if r.status_code != requests.codes.ok:
# We had a problem
status_str = \
NotifyBase.http_response_code_lookup(r.status_code)
NotifyTelegram.http_response_code_lookup(r.status_code)
try:
# Try to get the error message if we can:
@ -489,16 +500,17 @@ class NotifyTelegram(NotifyBase):
args = {
'format': self.notify_format,
'overflow': self.overflow_mode,
'image': self.include_image,
}
# No need to check the user token because the user automatically gets
# appended into the list of chat ids
return '{schema}://{bot_token}/{targets}/?{args}'.format(
schema=self.secure_protocol,
bot_token=self.quote(self.bot_token, safe=''),
bot_token=NotifyTelegram.quote(self.bot_token, safe=''),
targets='/'.join(
[self.quote('@{}'.format(x)) for x in self.chat_ids]),
args=self.urlencode(args))
[NotifyTelegram.quote('@{}'.format(x)) for x in self.targets]),
args=NotifyTelegram.urlencode(args))
@staticmethod
def parse_url(url):
@ -507,9 +519,9 @@ class NotifyTelegram(NotifyBase):
us to substantiate this object.
"""
# This is a dirty hack; but it's the only work around to
# tgram:// messages since the bot_token has a colon in it.
# It invalidates an normal URL.
# This is a dirty hack; but it's the only work around to tgram://
# messages since the bot_token has a colon in it. It invalidates a
# normal URL.
# This hack searches for this bogus URL and corrects it so we can
# properly load it further down. The other alternative is to ask users
@ -550,23 +562,28 @@ class NotifyTelegram(NotifyBase):
)
# The first token is stored in the hostname
bot_token_a = results['host']
bot_token_a = NotifyTelegram.unquote(results['host'])
# Get a nice unquoted list of path entries
entries = NotifyTelegram.split_path(results['fullpath'])
# Now fetch the remaining tokens
bot_token_b = [x for x in filter(
bool, NotifyBase.split_path(results['fullpath']))][0]
bot_token_b = entries.pop(0)
bot_token = '%s:%s' % (bot_token_a, bot_token_b)
chat_ids = [x for x in filter(
bool, NotifyBase.split_path(results['fullpath']))][1:]
# Store our chat ids (as these are the remaining entries)
results['targets'] = entries
# Support the 'to' variable so that we can support rooms this way too
# The 'to' makes it easier to use yaml configuration
if 'to' in results['qsd'] and len(results['qsd']['to']):
results['targets'] += \
NotifyTelegram.parse_list(results['qsd']['to'])
# Store our bot token
results['bot_token'] = bot_token
# Store our chat ids
results['chat_ids'] = chat_ids
# Include images with our message
results['include_image'] = \
parse_bool(results['qsd'].get('image', False))

View File

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

View File

@ -31,6 +31,7 @@ from time import sleep
from .NotifyBase import NotifyBase
from ..common import NotifyImageSize
from ..common import NotifyType
from ..utils import parse_bool
# Default our global support flag
NOTIFY_WINDOWS_SUPPORT_ENABLED = False
@ -75,6 +76,9 @@ class NotifyWindows(NotifyBase):
# content to display
body_max_line_count = 2
# The number of seconds to display the popup for
default_popup_duration_sec = 12
# This entry is a bit hacky, but it allows us to unit-test this library
# in an environment that simply doesn't have the windows packages
# available to us. It also allows us to handle situations where the
@ -84,18 +88,23 @@ class NotifyWindows(NotifyBase):
# let me know! :)
_enabled = NOTIFY_WINDOWS_SUPPORT_ENABLED
def __init__(self, **kwargs):
def __init__(self, include_image=True, duration=None, **kwargs):
"""
Initialize Windows Object
"""
super(NotifyWindows, self).__init__(**kwargs)
# Number of seconds to display notification for
self.duration = 12
self.duration = self.default_popup_duration_sec \
if not (isinstance(duration, int) and duration > 0) else duration
# Define our handler
self.hwnd = None
super(NotifyWindows, self).__init__(**kwargs)
# Track whether or not we want to send an image with our notification
# or not.
self.include_image = include_image
def _on_destroy(self, hwnd, msg, wparam, lparam):
"""
@ -140,8 +149,11 @@ class NotifyWindows(NotifyBase):
self.hinst, None)
win32gui.UpdateWindow(self.hwnd)
# image path
icon_path = self.image_path(notify_type, extension='.ico')
# image path (if configured to acquire)
icon_path = None if not self.include_image \
else self.image_path(notify_type, extension='.ico')
if icon_path:
icon_flags = win32con.LR_LOADFROMFILE | win32con.LR_DEFAULTSIZE
try:
@ -156,6 +168,9 @@ class NotifyWindows(NotifyBase):
# disable icon
hicon = win32gui.LoadIcon(0, win32con.IDI_APPLICATION)
else:
# disable icon
hicon = win32gui.LoadIcon(0, win32con.IDI_APPLICATION)
# Taskbar icon
flags = win32gui.NIF_ICON | win32gui.NIF_MESSAGE | win32gui.NIF_TIP
@ -185,7 +200,18 @@ class NotifyWindows(NotifyBase):
Returns the URL built dynamically based on specified arguments.
"""
return '{schema}://'.format(schema=self.protocol)
# Define any arguments set
args = {
'format': self.notify_format,
'overflow': self.overflow_mode,
'image': 'yes' if self.include_image else 'no',
'duration': str(self.duration),
}
return '{schema}://_/?{args}'.format(
schema=self.protocol,
args=NotifyWindows.urlencode(args),
)
@staticmethod
def parse_url(url):
@ -196,15 +222,31 @@ class NotifyWindows(NotifyBase):
"""
# return a very basic set of requirements
return {
results = NotifyBase.parse_url(url)
if not results:
results = {
'schema': NotifyWindows.protocol,
'user': None,
'password': None,
'port': None,
'host': 'localhost',
'host': '_',
'fullpath': None,
'path': None,
'url': url,
'qsd': {},
}
# Include images with our message
results['include_image'] = \
parse_bool(results['qsd'].get('image', True))
# Set duration
try:
results['duration'] = int(results['qsd'].get('duration'))
except (TypeError, ValueError):
# Not a valid integer; ignore entry
pass
# return results
return results

View File

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

View File

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

View File

@ -24,7 +24,6 @@
# THE SOFTWARE.
import re
import six
import ssl
from os.path import isfile
@ -99,7 +98,7 @@ class NotifyXMPP(NotifyBase):
# let me know! :)
_enabled = NOTIFY_XMPP_SUPPORT_ENABLED
def __init__(self, targets=None, jid=None, xep=None, to=None, **kwargs):
def __init__(self, targets=None, jid=None, xep=None, **kwargs):
"""
Initialize XMPP Object
"""
@ -177,17 +176,6 @@ class NotifyXMPP(NotifyBase):
else:
self.targets = list()
if isinstance(to, six.string_types):
# supporting to= makes yaml configuration easier since the user
# just has to identify each user one after another. This is just
# an optional extension to also make the url easier to read if
# some wish to use it.
# the to is presumed to be the targets JID
self.targets.append(to)
return
def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs):
"""
Perform XMPP Notification
@ -302,15 +290,16 @@ class NotifyXMPP(NotifyBase):
}
if self.jid:
args['jid'] = self.quote(self.jid, safe='')
args['jid'] = self.jid
if self.xep:
args['xep'] = self.quote(
','.join([str(xep) for xep in self.xep]), safe='')
# xep are integers, so we need to just iterate over a list and
# switch them to a string
args['xep'] = ','.join([str(xep) for xep in self.xep])
# Target JID(s) can clash with our existing paths, so we just use comma
# and/or space as a delimiters
jids = self.quote(' '.join(self.targets), safe='')
# and/or space as a delimiters - %20 = space
jids = '%20'.join([NotifyXMPP.quote(x, safe='') for x in self.targets])
default_port = self.default_secure_port \
if self.secure else self.default_unsecure_port
@ -318,19 +307,21 @@ class NotifyXMPP(NotifyBase):
default_schema = self.secure_protocol if self.secure else self.protocol
if self.user and self.password:
auth = '{}:{}'.format(self.user, self.password)
auth = '{}:{}'.format(
NotifyXMPP.quote(self.user, safe=''),
NotifyXMPP.quote(self.password, safe=''))
else:
auth = self.password if self.password else self.user
return '{schema}://{auth}@{hostname}{port}/{jids}?{args}'.format(
auth=self.quote(auth, safe=''),
auth=auth,
schema=default_schema,
hostname=self.host,
hostname=NotifyXMPP.quote(self.host, safe=''),
port='' if not self.port or self.port == default_port
else ':{}'.format(self.port),
jids=jids,
args=self.urlencode(args),
args=NotifyXMPP.urlencode(args),
)
@staticmethod
@ -348,18 +339,20 @@ class NotifyXMPP(NotifyBase):
# Get our targets; we ignore path slashes since they identify
# our resources
results['targets'] = parse_list(results['fullpath'])
results['targets'] = NotifyXMPP.parse_list(results['fullpath'])
# Over-ride the xep plugins
if 'xep' in results['qsd'] and len(results['qsd']['xep']):
results['xep'] = parse_list(results['qsd']['xep'])
results['xep'] = \
NotifyXMPP.parse_list(results['qsd']['xep'])
# Over-ride the default (and detected) jid
if 'jid' in results['qsd'] and len(results['qsd']['jid']):
results['jid'] = results['qsd']['jid']
results['jid'] = NotifyXMPP.unquote(results['qsd']['jid'])
# Over-ride the default (and detected) jid
if 'to' in results['qsd'] and len(results['qsd']['to']):
results['to'] = results['qsd']['to']
results['targets'] += \
NotifyXMPP.parse_list(results['qsd']['to'])
return results

View File

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

View File

@ -176,6 +176,87 @@ def test_dbus_plugin(mock_mainloop, mock_byte, mock_bytearray,
assert(obj.notify(title='', body='body',
notify_type=apprise.NotifyType.INFO) is True)
# Test our arguments through the instantiate call
obj = apprise.Apprise.instantiate(
'dbus://_/?image=True', suppress_exceptions=False)
assert(isinstance(obj, apprise.plugins.NotifyDBus) is True)
assert(isinstance(obj.url(), six.string_types) is True)
assert(obj.notify(title='title', body='body',
notify_type=apprise.NotifyType.INFO) is True)
obj = apprise.Apprise.instantiate(
'dbus://_/?image=False', suppress_exceptions=False)
assert(isinstance(obj, apprise.plugins.NotifyDBus) is True)
assert(isinstance(obj.url(), six.string_types) is True)
assert(obj.notify(title='title', body='body',
notify_type=apprise.NotifyType.INFO) is True)
# Test priority (alias to urgency) handling
obj = apprise.Apprise.instantiate(
'dbus://_/?priority=invalid', suppress_exceptions=False)
assert(isinstance(obj, apprise.plugins.NotifyDBus) is True)
assert(isinstance(obj.url(), six.string_types) is True)
assert(obj.notify(title='title', body='body',
notify_type=apprise.NotifyType.INFO) is True)
obj = apprise.Apprise.instantiate(
'dbus://_/?priority=high', suppress_exceptions=False)
assert(isinstance(obj, apprise.plugins.NotifyDBus) is True)
assert(isinstance(obj.url(), six.string_types) is True)
assert(obj.notify(title='title', body='body',
notify_type=apprise.NotifyType.INFO) is True)
obj = apprise.Apprise.instantiate(
'dbus://_/?priority=2', suppress_exceptions=False)
assert(isinstance(obj, apprise.plugins.NotifyDBus) is True)
assert(isinstance(obj.url(), six.string_types) is True)
assert(obj.notify(title='title', body='body',
notify_type=apprise.NotifyType.INFO) is True)
# Test urgency handling
obj = apprise.Apprise.instantiate(
'dbus://_/?urgency=invalid', suppress_exceptions=False)
assert(isinstance(obj, apprise.plugins.NotifyDBus) is True)
assert(isinstance(obj.url(), six.string_types) is True)
assert(obj.notify(title='title', body='body',
notify_type=apprise.NotifyType.INFO) is True)
obj = apprise.Apprise.instantiate(
'dbus://_/?urgency=high', suppress_exceptions=False)
assert(isinstance(obj, apprise.plugins.NotifyDBus) is True)
assert(isinstance(obj.url(), six.string_types) is True)
assert(obj.notify(title='title', body='body',
notify_type=apprise.NotifyType.INFO) is True)
obj = apprise.Apprise.instantiate(
'dbus://_/?urgency=2', suppress_exceptions=False)
assert(isinstance(obj, apprise.plugins.NotifyDBus) is True)
assert(isinstance(obj.url(), six.string_types) is True)
assert(obj.notify(title='title', body='body',
notify_type=apprise.NotifyType.INFO) is True)
obj = apprise.Apprise.instantiate(
'dbus://_/?urgency=', suppress_exceptions=False)
assert(isinstance(obj, apprise.plugins.NotifyDBus) is True)
assert(isinstance(obj.url(), six.string_types) is True)
assert(obj.notify(title='title', body='body',
notify_type=apprise.NotifyType.INFO) is True)
# Test x/y
obj = apprise.Apprise.instantiate(
'dbus://_/?x=5&y=5', suppress_exceptions=False)
assert(isinstance(obj, apprise.plugins.NotifyDBus) is True)
assert(isinstance(obj.url(), six.string_types) is True)
assert(obj.notify(title='title', body='body',
notify_type=apprise.NotifyType.INFO) is True)
obj = apprise.Apprise.instantiate(
'dbus://_/?x=invalid&y=invalid', suppress_exceptions=False)
assert(isinstance(obj, apprise.plugins.NotifyDBus) is True)
assert(isinstance(obj.url(), six.string_types) is True)
assert(obj.notify(title='title', body='body',
notify_type=apprise.NotifyType.INFO) is True)
# If our underlining object throws for whatever reason, we will
# gracefully fail
mock_notify = mock.Mock()

View File

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

View File

@ -61,25 +61,25 @@ def test_notify_matrix_plugin_general(mock_post, mock_get):
mock_post.return_value = request
# Variation Initializations
obj = plugins.NotifyMatrix(rooms='#abcd')
obj = plugins.NotifyMatrix(targets='#abcd')
assert isinstance(obj, plugins.NotifyMatrix) is True
assert isinstance(obj.url(), six.string_types) is True
# Registration successful
assert obj.send(body="test") is True
obj = plugins.NotifyMatrix(user='user', rooms='#abcd')
obj = plugins.NotifyMatrix(user='user', targets='#abcd')
assert isinstance(obj, plugins.NotifyMatrix) is True
assert isinstance(obj.url(), six.string_types) is True
# Registration successful
assert obj.send(body="test") is True
obj = plugins.NotifyMatrix(password='passwd', rooms='#abcd')
obj = plugins.NotifyMatrix(password='passwd', targets='#abcd')
assert isinstance(obj, plugins.NotifyMatrix) is True
assert isinstance(obj.url(), six.string_types) is True
# A username gets automatically generated in these cases
assert obj.send(body="test") is True
obj = plugins.NotifyMatrix(user='user', password='passwd', rooms='#abcd')
obj = plugins.NotifyMatrix(user='user', password='passwd', targets='#abcd')
assert isinstance(obj.url(), six.string_types) is True
assert isinstance(obj, plugins.NotifyMatrix) is True
# Registration Successful
@ -94,17 +94,17 @@ def test_notify_matrix_plugin_general(mock_post, mock_get):
# Fails because we couldn't register because of 404 errors
assert obj.send(body="test") is False
obj = plugins.NotifyMatrix(user='test', rooms='#abcd')
obj = plugins.NotifyMatrix(user='test', targets='#abcd')
assert isinstance(obj, plugins.NotifyMatrix) is True
# Fails because we still couldn't register
assert obj.send(user='test', password='passwd', body="test") is False
obj = plugins.NotifyMatrix(user='test', password='passwd', rooms='#abcd')
obj = plugins.NotifyMatrix(user='test', password='passwd', targets='#abcd')
assert isinstance(obj, plugins.NotifyMatrix) is True
# Fails because we still couldn't register
assert obj.send(body="test") is False
obj = plugins.NotifyMatrix(password='passwd', rooms='#abcd')
obj = plugins.NotifyMatrix(password='passwd', targets='#abcd')
# Fails because we still couldn't register
assert isinstance(obj, plugins.NotifyMatrix) is True
assert obj.send(body="test") is False
@ -132,7 +132,7 @@ def test_notify_matrix_plugin_general(mock_post, mock_get):
request.content = dumps(response_obj)
request.status_code = requests.codes.ok
obj = plugins.NotifyMatrix(rooms=None)
obj = plugins.NotifyMatrix(targets=None)
assert isinstance(obj, plugins.NotifyMatrix) is True
# Force a empty joined list response
@ -191,7 +191,8 @@ def test_notify_matrix_plugin_fetch(mock_post, mock_get):
mock_get.side_effect = fetch_failed
mock_post.side_effect = fetch_failed
obj = plugins.NotifyMatrix(user='user', password='passwd', thumbnail=True)
obj = plugins.NotifyMatrix(
user='user', password='passwd', include_image=True)
assert isinstance(obj, plugins.NotifyMatrix) is True
# We would hve failed to send our image notification
assert obj.send(user='test', password='passwd', body="test") is False
@ -518,3 +519,23 @@ def test_notify_matrix_plugin_rooms(mock_post, mock_get):
request.status_code = 403
obj._room_cache = {}
assert obj._room_id('#abc123:localhost') is None
def test_notify_matrix_url_parsing():
"""
API: NotifyMatrix() URL Testing
"""
result = plugins.NotifyMatrix.parse_url(
'matrix://user:token@localhost?to=#room')
assert isinstance(result, dict) is True
assert len(result['targets']) == 1
assert '#room' in result['targets']
result = plugins.NotifyMatrix.parse_url(
'matrix://user:token@localhost?to=#room1,#room2,#room3')
assert isinstance(result, dict) is True
assert len(result['targets']) == 3
assert '#room1' in result['targets']
assert '#room2' in result['targets']
assert '#room3' in result['targets']

View File

@ -194,13 +194,54 @@ def test_notify_base():
"<content>'\t \n</content>", convert_new_lines=True) == \
'&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(
'/path/?name=Dr%20Disrespect', unquote=False) == \
['path', '?name=Dr%20Disrespect']
assert NotifyBase.split_path(
'/path/?name=Dr%20Disrespect', unquote=True) == \
['path', '?name=Dr', 'Disrespect']
['path', '?name=Dr Disrespect']
# a slash found inside the path, if escaped properly will not be broken
# by split_path while additional concatinated slashes are ignored
# FYI: %2F = /
assert NotifyBase.split_path(
'/%2F///%2F%2F////%2F%2F%2F////', unquote=True) == \
['/', '//', '///']
# Test invalid data
assert NotifyBase.parse_list(None) == []
assert NotifyBase.parse_list(42) == ['42', ]
result = NotifyBase.parse_list(
',path,?name=Dr%20Disrespect', unquote=False)
assert isinstance(result, list) is True
assert len(result) == 2
assert 'path' in result
assert '?name=Dr%20Disrespect' in result
result = NotifyBase.parse_list(',path,?name=Dr%20Disrespect', unquote=True)
assert isinstance(result, list) is True
assert len(result) == 2
assert 'path' in result
assert '?name=Dr Disrespect' in result
# by parse_list while additional concatinated slashes are ignored
# FYI: %2F = /
# In this lit there are actually 4 entries, however parse_list
# eliminates duplicates in addition to unquoting content by default
result = NotifyBase.parse_list(
',%2F,%2F%2F, , , ,%2F%2F%2F, %2F', unquote=True)
assert isinstance(result, list) is True
assert len(result) == 3
assert '/' in result
assert '//' in result
assert '///' in result
# Give nothing, get nothing
assert NotifyBase.escape_html("") == ""

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

@ -23,10 +23,16 @@
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
import six
import mock
from random import choice
from string import ascii_uppercase as str_alpha
from string import digits as str_num
from apprise import plugins
from apprise import NotifyType
from apprise import Apprise
import mock
from apprise import OverflowMode
# Disable logging for a cleaner testing output
import logging
@ -40,6 +46,9 @@ TEST_URLS = (
('tweet://', {
'instance': None,
}),
('tweet://:@/', {
'instance': None,
}),
('tweet://consumer_key', {
# Missing Keys
'instance': TypeError,
@ -60,8 +69,10 @@ TEST_URLS = (
# We're good!
'instance': plugins.NotifyTwitter,
}),
('tweet://:@/', {
'instance': None,
('tweet://usera@consumer_key/consumer_key/access_token/'
'access_secret/?to=userb', {
# We're good!
'instance': plugins.NotifyTwitter,
}),
)
@ -73,6 +84,22 @@ def test_plugin(mock_oauth, mock_api):
API: NotifyTwitter Plugin() (pt1)
"""
# Disable Throttling to speed testing
plugins.NotifyBase.NotifyBase.request_rate_per_sec = 0
# Define how many characters exist per line
row = 80
# Some variables we use to control the data we work with
body_len = 1024
title_len = 1024
# Create a large body and title with random data
body = ''.join(choice(str_alpha + str_num + ' ') for _ in range(body_len))
body = '\r\n'.join([body[i: i + row] for i in range(0, len(body), row)])
# Create our title using random data
title = ''.join(choice(str_alpha + str_num) for _ in range(title_len))
# iterate over our dictionary and test it out
for (url, meta) in TEST_URLS:
@ -86,6 +113,9 @@ def test_plugin(mock_oauth, mock_api):
# Our expected Query response (True, False, or exception type)
response = meta.get('response', True)
# Allow notification type override, otherwise default to INFO
notify_type = meta.get('notify_type', NotifyType.INFO)
# Allow us to force the server response code to be something other then
# the defaults
response = meta.get(
@ -94,25 +124,69 @@ def test_plugin(mock_oauth, mock_api):
try:
obj = Apprise.instantiate(url, suppress_exceptions=False)
if instance is None:
# Check that we got what we came for
assert obj is instance
if obj is None:
if instance is not None:
# We're done (assuming this is what we were expecting)
print("{} didn't instantiate itself "
"(we expected it to)".format(url))
assert False
continue
assert(isinstance(obj, instance))
if instance is None:
# Expected None but didn't get it
print('%s instantiated %s (but expected None)' % (
url, str(obj)))
assert False
assert isinstance(obj, instance) is True
if isinstance(obj, plugins.NotifyBase.NotifyBase):
# We loaded okay; now lets make sure we can reverse this url
assert isinstance(obj.url(), six.string_types) is True
# Instantiate the exact same object again using the URL from
# the one that was already created properly
obj_cmp = Apprise.instantiate(obj.url())
# Our object should be the same instance as what we had
# originally expected above.
if not isinstance(obj_cmp, plugins.NotifyBase.NotifyBase):
# Assert messages are hard to trace back with the way
# these tests work. Just printing before throwing our
# assertion failure makes things easier to debug later on
print('TEST FAIL: {} regenerated as {}'.format(
url, obj.url()))
assert False
if self:
# Iterate over our expected entries inside of our object
for key, val in self.items():
# Test that our object has the desired key
assert(hasattr(key, obj))
assert(getattr(key, obj) == val)
assert hasattr(key, obj) is True
assert getattr(key, obj) == val
obj.request_rate_per_sec = 0
# check that we're as expected
assert obj.notify(
title='test', body='body',
notify_type=NotifyType.INFO) == response
# check that this doesn't change using different overflow
# methods
assert obj.notify(
body=body, title=title,
notify_type=notify_type,
overflow=OverflowMode.UPSTREAM) == response
assert obj.notify(
body=body, title=title,
notify_type=notify_type,
overflow=OverflowMode.TRUNCATE) == response
assert obj.notify(
body=body, title=title,
notify_type=notify_type,
overflow=OverflowMode.SPLIT) == response
except AssertionError:
# Don't mess with these entries
raise

View File

@ -125,6 +125,43 @@ def test_windows_plugin():
assert(obj.notify(title='title', body='body',
notify_type=apprise.NotifyType.INFO) is True)
obj = apprise.Apprise.instantiate(
'windows://_/?image=True', suppress_exceptions=False)
obj.duration = 0
assert(isinstance(obj.url(), six.string_types) is True)
assert(obj.notify(title='title', body='body',
notify_type=apprise.NotifyType.INFO) is True)
obj = apprise.Apprise.instantiate(
'windows://_/?image=False', suppress_exceptions=False)
obj.duration = 0
assert(isinstance(obj.url(), six.string_types) is True)
assert(obj.notify(title='title', body='body',
notify_type=apprise.NotifyType.INFO) is True)
obj = apprise.Apprise.instantiate(
'windows://_/?duration=1', suppress_exceptions=False)
assert(isinstance(obj.url(), six.string_types) is True)
assert(obj.notify(title='title', body='body',
notify_type=apprise.NotifyType.INFO) is True)
# loads okay
assert obj.duration == 1
obj = apprise.Apprise.instantiate(
'windows://_/?duration=invalid', suppress_exceptions=False)
# Falls back to default
assert obj.duration == obj.default_popup_duration_sec
obj = apprise.Apprise.instantiate(
'windows://_/?duration=-1', suppress_exceptions=False)
# Falls back to default
assert obj.duration == obj.default_popup_duration_sec
obj = apprise.Apprise.instantiate(
'windows://_/?duration=0', suppress_exceptions=False)
# Falls back to default
assert obj.duration == obj.default_popup_duration_sec
# Test our loading of our icon exception; it will still allow the
# notification to be sent
win32gui.LoadImage.side_effect = AttributeError

View File

@ -26,7 +26,7 @@
import six
import mock
import sys
# import types
import ssl
import apprise
@ -129,6 +129,39 @@ def test_xmpp_plugin(tmpdir):
# Not possible because no password was specified
assert obj is None
# SSL Flags
if hasattr(ssl, "PROTOCOL_TLS"):
# Test cases where PROTOCOL_TLS simply isn't available
ssl_temp_swap = ssl.PROTOCOL_TLS
del ssl.PROTOCOL_TLS
# Test our URL
url = 'xmpps://user:pass@example.com'
obj = apprise.Apprise.instantiate(url, suppress_exceptions=False)
# Test we loaded
assert isinstance(obj, apprise.plugins.NotifyXMPP) is True
assert obj.notify(
title='title', body='body',
notify_type=apprise.NotifyType.INFO) is True
# Restore the variable for remaining tests
setattr(ssl, 'PROTOCOL_TLS', ssl_temp_swap)
else:
# Handle case where it is not missing
setattr(ssl, 'PROTOCOL_TLS', ssl.PROTOCOL_TLSv1)
# Test our URL
url = 'xmpps://user:pass@example.com'
obj = apprise.Apprise.instantiate(url, suppress_exceptions=False)
# Test we loaded
assert isinstance(obj, apprise.plugins.NotifyXMPP) is True
assert obj.notify(
title='title', body='body',
notify_type=apprise.NotifyType.INFO) is True
# Restore settings as they were
del ssl.PROTOCOL_TLS
# Try Different Variations of our URL
for url in (
'xmpps://user:pass@example.com',