mirror of
https://github.com/caronc/apprise.git
synced 2025-02-01 02:49:19 +01:00
more unittesting + bugfixes
This commit is contained in:
parent
6cca5946e1
commit
82c5a11e5b
@ -21,7 +21,7 @@ The table below identifies the services this tool supports and some example serv
|
|||||||
|
|
||||||
| Notification Service | Service ID | Default Port | Example Syntax |
|
| Notification Service | Service ID | Default Port | Example Syntax |
|
||||||
| -------------------- | ---------- | ------------ | -------------- |
|
| -------------------- | ---------- | ------------ | -------------- |
|
||||||
| [Boxcar](https://github.com/caronc/apprise/wiki/Notify_boxcar) | boxcar:// | (TCP) 443 | boxcar://hostname<br />boxcar://hostname/@tag<br/>boxcar://hostname/device_token<br />boxcar://hostname/device_token1/device_token2/device_tokenN<br />boxcar://hostname/alias<br />boxcar://hostname/@tag/@tag2/alias/device_token
|
| [Boxcar](https://github.com/caronc/apprise/wiki/Notify_boxcar) | boxcar:// | (TCP) 443 | boxcar://hostname<br />boxcar://hostname/@tag<br/>boxcar://hostname/device_token<br />boxcar://hostname/device_token1/device_token2/device_tokenN<br />boxcar://hostname/@tag/@tag2/device_token
|
||||||
| [Faast](https://github.com/caronc/apprise/wiki/Notify_faast) | faast:// | (TCP) 443 | faast://authorizationtoken
|
| [Faast](https://github.com/caronc/apprise/wiki/Notify_faast) | faast:// | (TCP) 443 | faast://authorizationtoken
|
||||||
| [Growl](https://github.com/caronc/apprise/wiki/Notify_growl) | growl:// | (UDP) 23053 | growl://hostname<br />growl://hostname:portno<br />growl://password@hostname<br />growl://password@hostname:port</br>_Note: you can also use the get parameter _version_ which can allow the growl request to behave using the older v1.x protocol. An example would look like: growl://hostname?version=1
|
| [Growl](https://github.com/caronc/apprise/wiki/Notify_growl) | growl:// | (UDP) 23053 | growl://hostname<br />growl://hostname:portno<br />growl://password@hostname<br />growl://password@hostname:port</br>_Note: you can also use the get parameter _version_ which can allow the growl request to behave using the older v1.x protocol. An example would look like: growl://hostname?version=1
|
||||||
| [Join](https://github.com/caronc/apprise/wiki/Notify_join) | join:// | (TCP) 443 | join://apikey/device<br />join://apikey/device1/device2/deviceN/<br />join://apikey/group<br />join://apikey/groupA/groupB/groupN<br />join://apikey/DeviceA/groupA/groupN/DeviceN/
|
| [Join](https://github.com/caronc/apprise/wiki/Notify_join) | join:// | (TCP) 443 | join://apikey/device<br />join://apikey/device1/device2/deviceN/<br />join://apikey/group<br />join://apikey/groupA/groupB/groupN<br />join://apikey/DeviceA/groupA/groupN/DeviceN/
|
||||||
|
@ -238,10 +238,10 @@ class Apprise(object):
|
|||||||
# Toggle our return status flag
|
# Toggle our return status flag
|
||||||
status = False
|
status = False
|
||||||
|
|
||||||
except:
|
except Exception:
|
||||||
# A catch all so we don't have to abort early
|
# A catch all so we don't have to abort early
|
||||||
# just because one of our plugins has a bug in it.
|
# just because one of our plugins has a bug in it.
|
||||||
# TODO: print backtrace
|
logging.exception("notification exception")
|
||||||
status = False
|
status = False
|
||||||
|
|
||||||
return status
|
return status
|
||||||
|
@ -70,10 +70,10 @@ class AppriseAsset(object):
|
|||||||
if theme:
|
if theme:
|
||||||
self.theme = theme
|
self.theme = theme
|
||||||
|
|
||||||
if image_path_mask:
|
if image_path_mask is not None:
|
||||||
self.image_path_mask = image_path_mask
|
self.image_path_mask = image_path_mask
|
||||||
|
|
||||||
if image_url_mask:
|
if image_url_mask is not None:
|
||||||
self.image_url_mask = image_url_mask
|
self.image_url_mask = image_url_mask
|
||||||
|
|
||||||
def html_color(self, notify_type):
|
def html_color(self, notify_type):
|
||||||
@ -89,6 +89,10 @@ class AppriseAsset(object):
|
|||||||
Apply our mask to our image URL
|
Apply our mask to our image URL
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
if not self.image_url_mask:
|
||||||
|
# No image to return
|
||||||
|
return None
|
||||||
|
|
||||||
re_map = {
|
re_map = {
|
||||||
'{THEME}': self.theme if self.theme else '',
|
'{THEME}': self.theme if self.theme else '',
|
||||||
'{TYPE}': notify_type,
|
'{TYPE}': notify_type,
|
||||||
@ -108,6 +112,11 @@ class AppriseAsset(object):
|
|||||||
Apply our mask to our image file path
|
Apply our mask to our image file path
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
if not self.image_path_mask:
|
||||||
|
# No image to return
|
||||||
|
return None
|
||||||
|
|
||||||
re_map = {
|
re_map = {
|
||||||
'{THEME}': self.theme if self.theme else '',
|
'{THEME}': self.theme if self.theme else '',
|
||||||
'{TYPE}': notify_type,
|
'{TYPE}': notify_type,
|
||||||
|
@ -121,7 +121,7 @@ class NotifyBase(object):
|
|||||||
|
|
||||||
def __init__(self, title_maxlen=100, body_maxlen=512,
|
def __init__(self, title_maxlen=100, body_maxlen=512,
|
||||||
notify_format=NotifyFormat.TEXT, image_size=None,
|
notify_format=NotifyFormat.TEXT, image_size=None,
|
||||||
include_image=False, secure=False, throttle=None, **kwargs):
|
secure=False, throttle=None, **kwargs):
|
||||||
"""
|
"""
|
||||||
Initialize some general logging and common server arguments that will
|
Initialize some general logging and common server arguments that will
|
||||||
keep things consistent when working with the notifiers that will
|
keep things consistent when working with the notifiers that will
|
||||||
@ -152,7 +152,6 @@ class NotifyBase(object):
|
|||||||
self.title_maxlen = title_maxlen
|
self.title_maxlen = title_maxlen
|
||||||
self.body_maxlen = body_maxlen
|
self.body_maxlen = body_maxlen
|
||||||
self.image_size = image_size
|
self.image_size = image_size
|
||||||
self.include_image = include_image
|
|
||||||
self.secure = secure
|
self.secure = secure
|
||||||
|
|
||||||
if isinstance(throttle, (float, int)):
|
if isinstance(throttle, (float, int)):
|
||||||
@ -256,6 +255,9 @@ class NotifyBase(object):
|
|||||||
common unquote function
|
common unquote function
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
if not content:
|
||||||
|
return ''
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Python v3.x
|
# Python v3.x
|
||||||
return _unquote(content, encoding=encoding, errors=errors)
|
return _unquote(content, encoding=encoding, errors=errors)
|
||||||
@ -270,6 +272,9 @@ class NotifyBase(object):
|
|||||||
common quote function
|
common quote function
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
if not content:
|
||||||
|
return ''
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Python v3.x
|
# Python v3.x
|
||||||
return _quote(content, safe=safe, encoding=encoding, errors=errors)
|
return _quote(content, safe=safe, encoding=encoding, errors=errors)
|
||||||
@ -292,7 +297,7 @@ class NotifyBase(object):
|
|||||||
|
|
||||||
except TypeError:
|
except TypeError:
|
||||||
# Python v2.7
|
# Python v2.7
|
||||||
return _urlencode(query, oseq=doseq)
|
return _urlencode(query)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def split_path(path, unquote=True):
|
def split_path(path, unquote=True):
|
||||||
@ -322,12 +327,13 @@ class NotifyBase(object):
|
|||||||
return is_hostname(hostname)
|
return is_hostname(hostname)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def parse_url(url):
|
def parse_url(url, verify_host=True):
|
||||||
"""
|
"""
|
||||||
Parses the URL and returns it broken apart into a dictionary.
|
Parses the URL and returns it broken apart into a dictionary.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
results = parse_url(url, default_schema='unknown')
|
results = parse_url(
|
||||||
|
url, default_schema='unknown', verify_host=verify_host)
|
||||||
|
|
||||||
if not results:
|
if not results:
|
||||||
# We're done; we failed to parse our url
|
# We're done; we failed to parse our url
|
||||||
|
@ -19,86 +19,137 @@
|
|||||||
from json import dumps
|
from json import dumps
|
||||||
import requests
|
import requests
|
||||||
import re
|
import re
|
||||||
|
from time import time
|
||||||
|
import hmac
|
||||||
|
from hashlib import sha1
|
||||||
|
try:
|
||||||
|
from urlparse import urlparse
|
||||||
|
|
||||||
|
except ImportError:
|
||||||
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
from .NotifyBase import NotifyBase
|
from .NotifyBase import NotifyBase
|
||||||
from .NotifyBase import HTTP_ERROR_MAP
|
from .NotifyBase import HTTP_ERROR_MAP
|
||||||
|
|
||||||
|
from ..common import NotifyImageSize
|
||||||
from ..utils import compat_is_basestring
|
from ..utils import compat_is_basestring
|
||||||
|
|
||||||
# Used to validate Tags, Aliases and Devices
|
# Default to sending to all devices if nothing is specified
|
||||||
IS_TAG = re.compile(r'^[A-Za-z0-9]{1,63}$')
|
DEFAULT_TAG = '@all'
|
||||||
IS_ALIAS = re.compile(r'^[@]?[A-Za-z0-9]+$')
|
|
||||||
IS_DEVICETOKEN = re.compile(r'^[A-Za-z0-9]{64}$')
|
# The tags value is an structure containing an array of strings defining the
|
||||||
|
# list of tagged devices that the notification need to be send to, and a
|
||||||
|
# boolean operator (‘and’ / ‘or’) that defines the criteria to match devices
|
||||||
|
# against those tags.
|
||||||
|
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.
|
||||||
|
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
|
||||||
|
# you create on the boxcar website
|
||||||
|
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
|
# Used to break apart list of potential tags by their delimiter
|
||||||
# into a usable list.
|
# into a usable list.
|
||||||
TAGS_LIST_DELIM = re.compile(r'[ \t\r\n,\\/]+')
|
TAGS_LIST_DELIM = re.compile(r'[ \t\r\n,\\/]+')
|
||||||
|
|
||||||
|
# Image Support (72x72)
|
||||||
|
BOXCAR_IMAGE_XY = NotifyImageSize.XY_72
|
||||||
|
|
||||||
|
|
||||||
class NotifyBoxcar(NotifyBase):
|
class NotifyBoxcar(NotifyBase):
|
||||||
"""
|
"""
|
||||||
A wrapper for Boxcar Notifications
|
A wrapper for Boxcar Notifications
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# The default simple (insecure) protocol
|
# All boxcar notifications are secure
|
||||||
protocol = 'boxcar'
|
secure_protocol = 'boxcar'
|
||||||
|
|
||||||
# The default secure protocol
|
# Boxcar URL
|
||||||
secure_protocol = 'boxcars'
|
notify_url = 'https://boxcar-api.io/api/push/'
|
||||||
|
|
||||||
def __init__(self, recipients=None, **kwargs):
|
def __init__(self, access, secret, recipients=None, **kwargs):
|
||||||
"""
|
"""
|
||||||
Initialize Boxcar Object
|
Initialize Boxcar Object
|
||||||
"""
|
"""
|
||||||
super(NotifyBoxcar, self).__init__(
|
super(NotifyBoxcar, self).__init__(
|
||||||
title_maxlen=250, body_maxlen=10000, **kwargs)
|
title_maxlen=250, body_maxlen=10000,
|
||||||
|
image_size=BOXCAR_IMAGE_XY, **kwargs)
|
||||||
if self.secure:
|
|
||||||
self.schema = 'https'
|
|
||||||
|
|
||||||
else:
|
|
||||||
self.schema = 'http'
|
|
||||||
|
|
||||||
# Initialize tag list
|
# Initialize tag list
|
||||||
self.tags = list()
|
self.tags = list()
|
||||||
|
|
||||||
# Initialize alias list
|
|
||||||
self.aliases = list()
|
|
||||||
|
|
||||||
# Initialize device_token list
|
# Initialize device_token list
|
||||||
self.device_tokens = list()
|
self.device_tokens = list()
|
||||||
|
|
||||||
if recipients is None:
|
try:
|
||||||
|
# Access Key (associated with project)
|
||||||
|
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.',
|
||||||
|
)
|
||||||
|
|
||||||
|
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.',
|
||||||
|
)
|
||||||
|
|
||||||
|
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,
|
||||||
|
)
|
||||||
|
|
||||||
|
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,
|
||||||
|
)
|
||||||
|
|
||||||
|
if not recipients:
|
||||||
|
self.tags.append(DEFAULT_TAG)
|
||||||
recipients = []
|
recipients = []
|
||||||
|
|
||||||
elif compat_is_basestring(recipients):
|
elif compat_is_basestring(recipients):
|
||||||
recipients = filter(bool, TAGS_LIST_DELIM.split(
|
recipients = [x for x in filter(bool, TAGS_LIST_DELIM.split(
|
||||||
recipients,
|
recipients,
|
||||||
))
|
))]
|
||||||
|
|
||||||
elif not isinstance(recipients, (set, tuple, list)):
|
|
||||||
recipients = []
|
|
||||||
|
|
||||||
# Validate recipients and drop bad ones:
|
# Validate recipients and drop bad ones:
|
||||||
for recipient in recipients:
|
for recipient in recipients:
|
||||||
if IS_DEVICETOKEN.match(recipient):
|
if IS_TAG.match(recipient):
|
||||||
|
# store valid tag/alias
|
||||||
|
self.tags.append(IS_TAG.match(recipient).group('name'))
|
||||||
|
|
||||||
|
elif IS_DEVICETOKEN.match(recipient):
|
||||||
# store valid device
|
# store valid device
|
||||||
self.device_tokens.append(recipient)
|
self.device_tokens.append(recipient)
|
||||||
|
|
||||||
elif IS_TAG.match(recipient):
|
|
||||||
# store valid tag
|
|
||||||
self.tags.append(recipient)
|
|
||||||
|
|
||||||
elif IS_ALIAS.match(recipient):
|
|
||||||
# store valid tag/alias
|
|
||||||
self.aliases.append(recipient)
|
|
||||||
|
|
||||||
else:
|
else:
|
||||||
self.logger.warning(
|
self.logger.warning(
|
||||||
'Dropped invalid tag/alias/device_token '
|
'Dropped invalid tag/alias/device_token '
|
||||||
'(%s) specified.' % recipient,
|
'(%s) specified.' % recipient,
|
||||||
)
|
)
|
||||||
continue
|
|
||||||
|
|
||||||
def notify(self, title, body, notify_type, **kwargs):
|
def notify(self, title, body, notify_type, **kwargs):
|
||||||
"""
|
"""
|
||||||
@ -112,62 +163,87 @@ class NotifyBoxcar(NotifyBase):
|
|||||||
|
|
||||||
# prepare Boxcar Object
|
# prepare Boxcar Object
|
||||||
payload = {
|
payload = {
|
||||||
'badge': 'auto',
|
'aps': {
|
||||||
'alert': '%s:\r\n%s' % (title, body),
|
'badge': 'auto',
|
||||||
|
'alert': '',
|
||||||
|
},
|
||||||
|
'expires': str(int(time() + 30)),
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.tags:
|
if title:
|
||||||
payload['tags'] = self.tags
|
payload['aps']['@title'] = title
|
||||||
|
|
||||||
if self.aliases:
|
if body:
|
||||||
payload['aliases'] = self.aliases
|
payload['aps']['alert'] = body
|
||||||
|
|
||||||
|
if self.tags:
|
||||||
|
payload['tags'] = {'or': self.tags}
|
||||||
|
|
||||||
if self.device_tokens:
|
if self.device_tokens:
|
||||||
payload['device_tokens'] = self.device_tokens
|
payload['device_tokens'] = self.device_tokens
|
||||||
|
|
||||||
auth = None
|
# Source picture should be <= 450 DP wide, ~2:1 aspect.
|
||||||
if self.user:
|
image_url = self.image_url(notify_type)
|
||||||
auth = (self.user, self.password)
|
if image_url:
|
||||||
|
# Set our image
|
||||||
|
payload['@img'] = image_url
|
||||||
|
|
||||||
url = '%s://%s' % (self.schema, self.host)
|
# Acquire our hostname
|
||||||
if isinstance(self.port, int):
|
host = urlparse(self.notify_url).hostname
|
||||||
url += ':%d' % self.port
|
|
||||||
|
|
||||||
url += '/api/push'
|
# Calculate signature.
|
||||||
|
str_to_sign = "%s\n%s\n%s\n%s" % (
|
||||||
|
"POST", host, "/api/push", dumps(payload))
|
||||||
|
|
||||||
|
h = hmac.new(
|
||||||
|
bytearray(self.secret, 'utf-8'),
|
||||||
|
bytearray(str_to_sign, 'utf-8'),
|
||||||
|
sha1,
|
||||||
|
)
|
||||||
|
|
||||||
|
params = self.urlencode({
|
||||||
|
"publishkey": self.access,
|
||||||
|
"signature": h.hexdigest(),
|
||||||
|
})
|
||||||
|
|
||||||
|
notify_url = '%s?%s' % (self.notify_url, params)
|
||||||
self.logger.debug('Boxcar POST URL: %s (cert_verify=%r)' % (
|
self.logger.debug('Boxcar POST URL: %s (cert_verify=%r)' % (
|
||||||
url, self.verify_certificate,
|
notify_url, self.verify_certificate,
|
||||||
))
|
))
|
||||||
self.logger.debug('Boxcar Payload: %s' % str(payload))
|
self.logger.debug('Boxcar Payload: %s' % str(payload))
|
||||||
|
|
||||||
try:
|
try:
|
||||||
r = requests.post(
|
r = requests.post(
|
||||||
url,
|
notify_url,
|
||||||
data=dumps(payload),
|
data=dumps(payload),
|
||||||
headers=headers,
|
headers=headers,
|
||||||
auth=auth,
|
|
||||||
verify=self.verify_certificate,
|
verify=self.verify_certificate,
|
||||||
)
|
)
|
||||||
if r.status_code != requests.codes.ok:
|
|
||||||
|
# Boxcar returns 201 (Created) when successful
|
||||||
|
if r.status_code != requests.codes.created:
|
||||||
try:
|
try:
|
||||||
self.logger.warning(
|
self.logger.warning(
|
||||||
'Failed to send Boxcar notification: '
|
'Failed to send Boxcar notification: '
|
||||||
'%s (error=%s).' % (
|
'%s (error=%s).' % (
|
||||||
HTTP_ERROR_MAP[r.status_code],
|
HTTP_ERROR_MAP[r.status_code],
|
||||||
r.status_code))
|
r.status_code))
|
||||||
|
|
||||||
except KeyError:
|
except KeyError:
|
||||||
self.logger.warning(
|
self.logger.warning(
|
||||||
'Failed to send Boxcar notification '
|
'Failed to send Boxcar notification '
|
||||||
'(error=%s).' % (
|
'(error=%s).' % (
|
||||||
r.status_code))
|
r.status_code))
|
||||||
|
|
||||||
|
# self.logger.debug('Response Details: %s' % r.raw.read())
|
||||||
|
|
||||||
# Return; we're done
|
# Return; we're done
|
||||||
return False
|
return False
|
||||||
|
|
||||||
except requests.ConnectionError as e:
|
except requests.RequestException as e:
|
||||||
self.logger.warning(
|
self.logger.warning(
|
||||||
'A Connection error occured sending Boxcar '
|
'A Connection error occured sending Boxcar '
|
||||||
'notification to %s.' % (
|
'notification to %s.' % (host))
|
||||||
self.host))
|
|
||||||
|
|
||||||
self.logger.debug('Socket Exception: %s' % str(e))
|
self.logger.debug('Socket Exception: %s' % str(e))
|
||||||
|
|
||||||
@ -182,21 +258,39 @@ class NotifyBoxcar(NotifyBase):
|
|||||||
Parses the URL and returns it broken apart into a dictionary.
|
Parses the URL and returns it broken apart into a dictionary.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
results = NotifyBase.parse_url(url)
|
results = NotifyBase.parse_url(url, verify_host=False)
|
||||||
|
|
||||||
if not results:
|
if not results:
|
||||||
# We're done early
|
# We're done early
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# Acquire our recipients and include them in the response
|
# The first token is stored in the hostnamee
|
||||||
|
access = results['host']
|
||||||
|
|
||||||
|
# Now fetch the remaining tokens
|
||||||
try:
|
try:
|
||||||
recipients = NotifyBase.unquote(results['fullpath'])
|
secret = NotifyBase.split_path(results['fullpath'])[0]
|
||||||
|
|
||||||
except (AttributeError, KeyError):
|
except (AttributeError, IndexError):
|
||||||
# no recipients detected
|
# Force a bad value that will get caught in parsing later
|
||||||
recipients = ''
|
secret = None
|
||||||
|
|
||||||
# Store our recipients
|
try:
|
||||||
results['recipients'] = recipients
|
recipients = ','.join(
|
||||||
|
NotifyBase.split_path(results['fullpath'])[1:])
|
||||||
|
|
||||||
|
except (AttributeError, IndexError):
|
||||||
|
# Default to not having any recipients
|
||||||
|
recipients = None
|
||||||
|
|
||||||
|
if not (access and secret):
|
||||||
|
# If we did not recive an access and/or secret code
|
||||||
|
# then we're done
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Store our required content
|
||||||
|
results['recipients'] = recipients if recipients else None
|
||||||
|
results['access'] = access
|
||||||
|
results['secret'] = secret
|
||||||
|
|
||||||
return results
|
return results
|
||||||
|
@ -189,7 +189,7 @@ class NotifyEmail(NotifyBase):
|
|||||||
# over-riding any smarts to be applied
|
# over-riding any smarts to be applied
|
||||||
return
|
return
|
||||||
|
|
||||||
for i in range(len(WEBBASE_LOOKUP_TABLE)):
|
for i in range(len(WEBBASE_LOOKUP_TABLE)): # pragma: no branch
|
||||||
self.logger.debug('Scanning %s against %s' % (
|
self.logger.debug('Scanning %s against %s' % (
|
||||||
self.to_addr, WEBBASE_LOOKUP_TABLE[i][0]
|
self.to_addr, WEBBASE_LOOKUP_TABLE[i][0]
|
||||||
))
|
))
|
||||||
@ -290,13 +290,10 @@ class NotifyEmail(NotifyBase):
|
|||||||
# Return; we're done
|
# Return; we're done
|
||||||
return False
|
return False
|
||||||
|
|
||||||
try:
|
finally:
|
||||||
|
# Gracefully terminate the connection with the server
|
||||||
socket.quit()
|
socket.quit()
|
||||||
|
|
||||||
except:
|
|
||||||
# no problem
|
|
||||||
pass
|
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@ -332,17 +329,11 @@ class NotifyEmail(NotifyBase):
|
|||||||
|
|
||||||
else:
|
else:
|
||||||
# get 'To' email address
|
# get 'To' email address
|
||||||
try:
|
to_addr = '%s@%s' % (
|
||||||
to_addr = '%s@%s' % (
|
re.split(
|
||||||
re.split(
|
'[\s@]+', NotifyBase.unquote(results['user']))[0],
|
||||||
'[\s@]+', NotifyBase.unquote(results['user']))[0],
|
results.get('host', '')
|
||||||
results.get('host', '')
|
)
|
||||||
)
|
|
||||||
|
|
||||||
except (AttributeError, IndexError):
|
|
||||||
# No problem, we have other ways of getting
|
|
||||||
# the To address
|
|
||||||
pass
|
|
||||||
|
|
||||||
# Attempt to detect 'from' email address
|
# Attempt to detect 'from' email address
|
||||||
from_addr = to_addr
|
from_addr = to_addr
|
||||||
|
@ -64,12 +64,9 @@ class NotifyFaast(NotifyBase):
|
|||||||
'message': body,
|
'message': body,
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.include_image:
|
image_url = self.image_url(notify_type)
|
||||||
image_url = self.image_url(
|
if image_url:
|
||||||
notify_type,
|
payload['icon_url'] = image_url
|
||||||
)
|
|
||||||
if image_url:
|
|
||||||
payload['icon_url'] = image_url
|
|
||||||
|
|
||||||
self.logger.debug('Faast POST URL: %s (cert_verify=%r)' % (
|
self.logger.debug('Faast POST URL: %s (cert_verify=%r)' % (
|
||||||
self.notify_url, self.verify_certificate,
|
self.notify_url, self.verify_certificate,
|
||||||
|
@ -140,13 +140,13 @@ class NotifyGrowl(NotifyBase):
|
|||||||
body = '\r\n'.join(body[0:2])
|
body = '\r\n'.join(body[0:2])
|
||||||
|
|
||||||
icon = None
|
icon = None
|
||||||
if self.include_image:
|
if self.version >= 2:
|
||||||
if self.version >= 2:
|
# URL Based
|
||||||
# URL Based
|
icon = self.image_url(notify_type)
|
||||||
icon = self.image_url(notify_type)
|
|
||||||
else:
|
else:
|
||||||
# Raw
|
# Raw
|
||||||
icon = self.image_raw(notify_type)
|
icon = self.image_raw(notify_type)
|
||||||
|
|
||||||
payload = {
|
payload = {
|
||||||
'noteType': GROWL_NOTIFICATION_TYPE,
|
'noteType': GROWL_NOTIFICATION_TYPE,
|
||||||
|
@ -113,8 +113,7 @@ class NotifyJSON(NotifyBase):
|
|||||||
except KeyError:
|
except KeyError:
|
||||||
self.logger.warning(
|
self.logger.warning(
|
||||||
'Failed to send JSON notification '
|
'Failed to send JSON notification '
|
||||||
'(error=%s).' % (
|
'(error=%s).' % (r.status_code))
|
||||||
r.status_code))
|
|
||||||
|
|
||||||
# Return; we're done
|
# Return; we're done
|
||||||
return False
|
return False
|
||||||
|
@ -148,12 +148,9 @@ class NotifyJoin(NotifyBase):
|
|||||||
'text': body,
|
'text': body,
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.include_image:
|
image_url = self.image_url(notify_type)
|
||||||
image_url = self.image_url(
|
if image_url:
|
||||||
notify_type,
|
url_args['icon'] = image_url
|
||||||
)
|
|
||||||
if image_url:
|
|
||||||
url_args['icon'] = image_url
|
|
||||||
|
|
||||||
# prepare payload
|
# prepare payload
|
||||||
payload = {}
|
payload = {}
|
||||||
|
@ -45,9 +45,6 @@ class NotifyPushBullet(NotifyBase):
|
|||||||
A wrapper for PushBullet Notifications
|
A wrapper for PushBullet Notifications
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# The default protocol
|
|
||||||
protocol = 'pbul'
|
|
||||||
|
|
||||||
# The default secure protocol
|
# The default secure protocol
|
||||||
secure_protocol = 'pbul'
|
secure_protocol = 'pbul'
|
||||||
|
|
||||||
|
@ -92,12 +92,9 @@ class NotifyPushalot(NotifyBase):
|
|||||||
'Source': self.app_id,
|
'Source': self.app_id,
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.include_image:
|
image_url = self.image_url(notify_type)
|
||||||
image_url = self.image_url(
|
if image_url:
|
||||||
notify_type,
|
payload['Image'] = image_url
|
||||||
)
|
|
||||||
if image_url:
|
|
||||||
payload['Image'] = image_url
|
|
||||||
|
|
||||||
self.logger.debug('Pushalot POST URL: %s (cert_verify=%r)' % (
|
self.logger.debug('Pushalot POST URL: %s (cert_verify=%r)' % (
|
||||||
self.notify_url, self.verify_certificate,
|
self.notify_url, self.verify_certificate,
|
||||||
|
@ -22,7 +22,7 @@
|
|||||||
# follow the wizard to pre-determine the channel(s) you want your
|
# follow the wizard to pre-determine the channel(s) you want your
|
||||||
# message to broadcast to, and when you're complete, you will
|
# message to broadcast to, and when you're complete, you will
|
||||||
# recieve a URL that looks something like this:
|
# recieve a URL that looks something like this:
|
||||||
# https://hooks.slack.com/services/T1JJ3T3L2/A1BRTD4JD/TIiajkdnlazkcOXrIdevi7F
|
# https://hooks.slack.com/services/T1JJ3T3L2/A1BRTD4JD/TIiajkdnlazkcOXrIdevi7FQ
|
||||||
# ^ ^ ^
|
# ^ ^ ^
|
||||||
# | | |
|
# | | |
|
||||||
# These are important <--------------^---------^---------------^
|
# These are important <--------------^---------^---------------^
|
||||||
@ -59,11 +59,11 @@ SLACK_HTTP_ERROR_MAP.update({
|
|||||||
401: 'Unauthorized - Invalid Token.',
|
401: 'Unauthorized - Invalid Token.',
|
||||||
})
|
})
|
||||||
|
|
||||||
# Used to break path apart into list of devices
|
# Used to break path apart into list of channels
|
||||||
CHANNEL_LIST_DELIM = re.compile(r'[ \t\r\n,#\\/]+')
|
CHANNEL_LIST_DELIM = re.compile(r'[ \t\r\n,#\\/]+')
|
||||||
|
|
||||||
# Used to detect a device
|
# Used to detect a channel
|
||||||
IS_CHANNEL_RE = re.compile(r'#?([A-Za-z0-9_]{1,32})')
|
IS_CHANNEL_RE = re.compile(r'[+#@]?([A-Z0-9_]{1,32})', re.I)
|
||||||
|
|
||||||
# Image Support (72x72)
|
# Image Support (72x72)
|
||||||
SLACK_IMAGE_XY = NotifyImageSize.XY_72
|
SLACK_IMAGE_XY = NotifyImageSize.XY_72
|
||||||
@ -127,11 +127,13 @@ class NotifySlack(NotifyBase):
|
|||||||
self.user = SLACK_DEFAULT_USER
|
self.user = SLACK_DEFAULT_USER
|
||||||
|
|
||||||
if compat_is_basestring(channels):
|
if compat_is_basestring(channels):
|
||||||
self.channels = filter(bool, CHANNEL_LIST_DELIM.split(
|
self.channels = [x for x in filter(bool, CHANNEL_LIST_DELIM.split(
|
||||||
channels,
|
channels,
|
||||||
))
|
))]
|
||||||
|
|
||||||
elif isinstance(channels, (set, tuple, list)):
|
elif isinstance(channels, (set, tuple, list)):
|
||||||
self.channels = channels
|
self.channels = channels
|
||||||
|
|
||||||
else:
|
else:
|
||||||
self.channels = list()
|
self.channels = list()
|
||||||
|
|
||||||
@ -167,13 +169,13 @@ class NotifySlack(NotifyBase):
|
|||||||
}
|
}
|
||||||
|
|
||||||
# error tracking (used for function return)
|
# error tracking (used for function return)
|
||||||
has_error = False
|
notify_okay = True
|
||||||
|
|
||||||
# Perform Formatting
|
# Perform Formatting
|
||||||
title = self._re_formatting_rules.sub(
|
title = self._re_formatting_rules.sub( # pragma: no branch
|
||||||
lambda x: self._re_formatting_map[x.group()], title,
|
lambda x: self._re_formatting_map[x.group()], title,
|
||||||
)
|
)
|
||||||
body = self._re_formatting_rules.sub(
|
body = self._re_formatting_rules.sub( # pragma: no branch
|
||||||
lambda x: self._re_formatting_map[x.group()], body,
|
lambda x: self._re_formatting_map[x.group()], body,
|
||||||
)
|
)
|
||||||
url = '%s/%s/%s/%s' % (
|
url = '%s/%s/%s/%s' % (
|
||||||
@ -183,11 +185,7 @@ class NotifySlack(NotifyBase):
|
|||||||
self.token_c,
|
self.token_c,
|
||||||
)
|
)
|
||||||
|
|
||||||
image_url = None
|
image_url = self.image_url(notify_type)
|
||||||
if self.include_image:
|
|
||||||
image_url = self.image_url(
|
|
||||||
notify_type,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Create a copy of the channel list
|
# Create a copy of the channel list
|
||||||
channels = list(self.channels)
|
channels = list(self.channels)
|
||||||
@ -204,9 +202,11 @@ class NotifySlack(NotifyBase):
|
|||||||
if len(channel) > 1 and channel[0] == '+':
|
if len(channel) > 1 and channel[0] == '+':
|
||||||
# Treat as encoded id if prefixed with a +
|
# Treat as encoded id if prefixed with a +
|
||||||
_channel = channel[1:]
|
_channel = channel[1:]
|
||||||
|
|
||||||
elif len(channel) > 1 and channel[0] == '@':
|
elif len(channel) > 1 and channel[0] == '@':
|
||||||
# Treat @ value 'as is'
|
# Treat @ value 'as is'
|
||||||
_channel = channel
|
_channel = channel
|
||||||
|
|
||||||
else:
|
else:
|
||||||
# Prefix with channel hash tag
|
# Prefix with channel hash tag
|
||||||
_channel = '#%s' % channel
|
_channel = '#%s' % channel
|
||||||
@ -220,7 +220,7 @@ class NotifySlack(NotifyBase):
|
|||||||
'attachments': [{
|
'attachments': [{
|
||||||
'title': title,
|
'title': title,
|
||||||
'text': body,
|
'text': body,
|
||||||
'color': self.asset.html_color[notify_type],
|
'color': self.asset.html_color(notify_type),
|
||||||
# Time
|
# Time
|
||||||
'ts': time(),
|
'ts': time(),
|
||||||
'footer': self.app_id,
|
'footer': self.app_id,
|
||||||
@ -251,7 +251,7 @@ class NotifySlack(NotifyBase):
|
|||||||
SLACK_HTTP_ERROR_MAP[r.status_code],
|
SLACK_HTTP_ERROR_MAP[r.status_code],
|
||||||
r.status_code))
|
r.status_code))
|
||||||
|
|
||||||
except IndexError:
|
except KeyError:
|
||||||
self.logger.warning(
|
self.logger.warning(
|
||||||
'Failed to send Slack:%s '
|
'Failed to send Slack:%s '
|
||||||
'notification (error=%s).' % (
|
'notification (error=%s).' % (
|
||||||
@ -261,21 +261,21 @@ class NotifySlack(NotifyBase):
|
|||||||
# self.logger.debug('Response Details: %s' % r.raw.read())
|
# self.logger.debug('Response Details: %s' % r.raw.read())
|
||||||
|
|
||||||
# Return; we're done
|
# Return; we're done
|
||||||
has_error = True
|
notify_okay = False
|
||||||
|
|
||||||
except requests.ConnectionError as e:
|
except requests.RequestException as e:
|
||||||
self.logger.warning(
|
self.logger.warning(
|
||||||
'A Connection error occured sending Slack:%s ' % (
|
'A Connection error occured sending Slack:%s ' % (
|
||||||
channel) + 'notification.'
|
channel) + 'notification.'
|
||||||
)
|
)
|
||||||
self.logger.debug('Socket Exception: %s' % str(e))
|
self.logger.debug('Socket Exception: %s' % str(e))
|
||||||
has_error = True
|
notify_okay = False
|
||||||
|
|
||||||
if len(channels):
|
if len(channels):
|
||||||
# Prevent thrashing requests
|
# Prevent thrashing requests
|
||||||
self.throttle()
|
self.throttle()
|
||||||
|
|
||||||
return has_error
|
return notify_okay
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def parse_url(url):
|
def parse_url(url):
|
||||||
@ -297,23 +297,15 @@ class NotifySlack(NotifyBase):
|
|||||||
|
|
||||||
# Now fetch the remaining tokens
|
# Now fetch the remaining tokens
|
||||||
try:
|
try:
|
||||||
token_b, token_c = filter(
|
token_b, token_c = [x for x in filter(
|
||||||
bool, NotifyBase.split_path(results['fullpath']))[0:2]
|
bool, NotifyBase.split_path(results['fullpath']))][0:2]
|
||||||
|
|
||||||
except (AttributeError, IndexError):
|
except (ValueError, AttributeError, IndexError):
|
||||||
# Force some bad values that will get caught
|
# We're done
|
||||||
# in parsing later
|
return None
|
||||||
token_b = None
|
|
||||||
token_c = None
|
|
||||||
|
|
||||||
try:
|
channels = [x for x in filter(
|
||||||
channels = '#'.join(filter(
|
bool, NotifyBase.split_path(results['fullpath']))][2:]
|
||||||
bool, NotifyBase.split_path(results['fullpath']))[2:])
|
|
||||||
|
|
||||||
except (AttributeError, IndexError):
|
|
||||||
# Force some bad values that will get caught
|
|
||||||
# in parsing later
|
|
||||||
channels = None
|
|
||||||
|
|
||||||
results['token_a'] = token_a
|
results['token_a'] = token_a
|
||||||
results['token_b'] = token_b
|
results['token_b'] = token_b
|
||||||
|
@ -218,18 +218,17 @@ class NotifyTelegram(NotifyBase):
|
|||||||
has_error = False
|
has_error = False
|
||||||
|
|
||||||
image_url = None
|
image_url = None
|
||||||
if self.include_image:
|
image_content = self.image_raw(notify_type)
|
||||||
image_content = self.image_raw(notify_type)
|
if image_content is not None:
|
||||||
if image_content is not None:
|
# prepare our image URL
|
||||||
# prepare our image URL
|
image_url = '%s%s/%s' % (
|
||||||
image_url = '%s%s/%s' % (
|
self.notify_url,
|
||||||
self.notify_url,
|
self.bot_token,
|
||||||
self.bot_token,
|
'sendPhoto'
|
||||||
'sendPhoto'
|
)
|
||||||
)
|
|
||||||
|
|
||||||
# Set up our upload
|
# Set up our upload
|
||||||
files = {'photo': ('%s.png' % notify_type, image_content)}
|
files = {'photo': ('%s.png' % notify_type, image_content)}
|
||||||
|
|
||||||
url = '%s%s/%s' % (
|
url = '%s%s/%s' % (
|
||||||
self.notify_url,
|
self.notify_url,
|
||||||
|
@ -90,12 +90,9 @@ class NotifyToasty(NotifyBase):
|
|||||||
'text': NotifyBase.quote(body),
|
'text': NotifyBase.quote(body),
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.include_image:
|
image_url = self.image_url(notify_type)
|
||||||
image_url = self.image_url(
|
if image_url:
|
||||||
notify_type,
|
payload['image'] = image_url
|
||||||
)
|
|
||||||
if image_url:
|
|
||||||
payload['image'] = image_url
|
|
||||||
|
|
||||||
# URL to transmit content via
|
# URL to transmit content via
|
||||||
url = '%s%s' % (self.notify_url, device)
|
url = '%s%s' % (self.notify_url, device)
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
#
|
#
|
||||||
# XBMC Notify Wrapper
|
# XBMC/KODI Notify Wrapper
|
||||||
#
|
#
|
||||||
# Copyright (C) 2017 Chris Caron <lead2gold@gmail.com>
|
# Copyright (C) 2017 Chris Caron <lead2gold@gmail.com>
|
||||||
#
|
#
|
||||||
@ -28,17 +28,6 @@ from ..common import NotifyImageSize
|
|||||||
# Image Support (128x128)
|
# Image Support (128x128)
|
||||||
XBMC_IMAGE_XY = NotifyImageSize.XY_128
|
XBMC_IMAGE_XY = NotifyImageSize.XY_128
|
||||||
|
|
||||||
# XBMC uses v2
|
|
||||||
XBMC_PROTOCOL_V2 = 2
|
|
||||||
|
|
||||||
# Kodi uses v6
|
|
||||||
XBMC_PROTOCOL_V6 = 6
|
|
||||||
|
|
||||||
SUPPORTED_XBMC_PROTOCOLS = (
|
|
||||||
XBMC_PROTOCOL_V2,
|
|
||||||
XBMC_PROTOCOL_V6,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class NotifyXBMC(NotifyBase):
|
class NotifyXBMC(NotifyBase):
|
||||||
"""
|
"""
|
||||||
@ -52,7 +41,13 @@ class NotifyXBMC(NotifyBase):
|
|||||||
secure_protocol = ('xbmc', 'kodis')
|
secure_protocol = ('xbmc', 'kodis')
|
||||||
|
|
||||||
# XBMC uses the http protocol with JSON requests
|
# XBMC uses the http protocol with JSON requests
|
||||||
default_port = 8080
|
xbmc_default_port = 8080
|
||||||
|
|
||||||
|
# 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, **kwargs):
|
||||||
"""
|
"""
|
||||||
@ -68,14 +63,8 @@ class NotifyXBMC(NotifyBase):
|
|||||||
else:
|
else:
|
||||||
self.schema = 'http'
|
self.schema = 'http'
|
||||||
|
|
||||||
if not self.port:
|
# Default protocol
|
||||||
self.port = self.default_port
|
self.protocol = kwargs.get('protocol', self.xbmc_remote_protocol)
|
||||||
|
|
||||||
self.protocol = kwargs.get('protocol', XBMC_PROTOCOL_V2)
|
|
||||||
if self.protocol not in SUPPORTED_XBMC_PROTOCOLS:
|
|
||||||
raise TypeError("Invalid protocol specified.")
|
|
||||||
|
|
||||||
return
|
|
||||||
|
|
||||||
def _payload_60(self, title, body, notify_type, **kwargs):
|
def _payload_60(self, title, body, notify_type, **kwargs):
|
||||||
"""
|
"""
|
||||||
@ -102,18 +91,17 @@ class NotifyXBMC(NotifyBase):
|
|||||||
'id': 1,
|
'id': 1,
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.include_image:
|
image_url = self.image_url(notify_type)
|
||||||
image_url = self.image_url(
|
if image_url:
|
||||||
notify_type,
|
payload['image'] = image_url
|
||||||
)
|
if notify_type is NotifyType.FAILURE:
|
||||||
if image_url:
|
payload['type'] = 'error'
|
||||||
payload['image'] = image_url
|
|
||||||
if notify_type is NotifyType.Error:
|
elif notify_type is NotifyType.WARNING:
|
||||||
payload['type'] = 'error'
|
payload['type'] = 'warning'
|
||||||
elif notify_type is NotifyType.Warning:
|
|
||||||
payload['type'] = 'warning'
|
else:
|
||||||
else:
|
payload['type'] = 'info'
|
||||||
payload['type'] = 'info'
|
|
||||||
|
|
||||||
return (headers, dumps(payload))
|
return (headers, dumps(payload))
|
||||||
|
|
||||||
@ -142,18 +130,15 @@ class NotifyXBMC(NotifyBase):
|
|||||||
'id': 1,
|
'id': 1,
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.include_image:
|
image_url = self.image_url(notify_type)
|
||||||
image_url = self.image_url(
|
if image_url:
|
||||||
notify_type,
|
payload['image'] = image_url
|
||||||
)
|
|
||||||
if image_url:
|
|
||||||
payload['image'] = image_url
|
|
||||||
|
|
||||||
return (headers, dumps(payload))
|
return (headers, dumps(payload))
|
||||||
|
|
||||||
def notify(self, title, body, notify_type, **kwargs):
|
def notify(self, title, body, notify_type, **kwargs):
|
||||||
"""
|
"""
|
||||||
Perform XBMC Notification
|
Perform XBMC/KODI Notification
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# Limit results to just the first 2 line otherwise
|
# Limit results to just the first 2 line otherwise
|
||||||
@ -162,13 +147,13 @@ class NotifyXBMC(NotifyBase):
|
|||||||
body[0] = body[0].strip('#').strip()
|
body[0] = body[0].strip('#').strip()
|
||||||
body = '\r\n'.join(body[0:2])
|
body = '\r\n'.join(body[0:2])
|
||||||
|
|
||||||
if self.protocol == XBMC_PROTOCOL_V2:
|
if self.protocol == self.xbmc_remote_protocol:
|
||||||
# XBMC v2.0
|
# XBMC v2.0
|
||||||
(headers, payload) = self._payload_20(
|
(headers, payload) = self._payload_20(
|
||||||
title, body, notify_type, **kwargs)
|
title, body, notify_type, **kwargs)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
# XBMC v6.0
|
# KODI v6.0
|
||||||
(headers, payload) = self._payload_60(
|
(headers, payload) = self._payload_60(
|
||||||
title, body, notify_type, **kwargs)
|
title, body, notify_type, **kwargs)
|
||||||
|
|
||||||
@ -177,7 +162,7 @@ class NotifyXBMC(NotifyBase):
|
|||||||
auth = (self.user, self.password)
|
auth = (self.user, self.password)
|
||||||
|
|
||||||
url = '%s://%s' % (self.schema, self.host)
|
url = '%s://%s' % (self.schema, self.host)
|
||||||
if isinstance(self.port, int):
|
if self.port:
|
||||||
url += ':%d' % self.port
|
url += ':%d' % self.port
|
||||||
|
|
||||||
url += '/jsonrpc'
|
url += '/jsonrpc'
|
||||||
@ -214,7 +199,7 @@ class NotifyXBMC(NotifyBase):
|
|||||||
else:
|
else:
|
||||||
self.logger.info('Sent XBMC/KODI notification.')
|
self.logger.info('Sent XBMC/KODI notification.')
|
||||||
|
|
||||||
except requests.ConnectionError as e:
|
except requests.RequestException as e:
|
||||||
self.logger.warning(
|
self.logger.warning(
|
||||||
'A Connection error occured sending XBMC/KODI '
|
'A Connection error occured sending XBMC/KODI '
|
||||||
'notification.'
|
'notification.'
|
||||||
@ -225,3 +210,31 @@ class NotifyXBMC(NotifyBase):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def parse_url(url):
|
||||||
|
"""
|
||||||
|
Parses the URL and returns enough arguments that can allow
|
||||||
|
us to substantiate this object.
|
||||||
|
|
||||||
|
"""
|
||||||
|
results = NotifyBase.parse_url(url)
|
||||||
|
if not results:
|
||||||
|
# We're done early
|
||||||
|
return results
|
||||||
|
|
||||||
|
# We want to set our protocol depending on whether we're using XBMC
|
||||||
|
# or KODI
|
||||||
|
if results.get('schema', '').startswith('xbmc'):
|
||||||
|
# XBMC Support
|
||||||
|
results['protocol'] = NotifyXBMC.xbmc_remote_protocol
|
||||||
|
|
||||||
|
# Assign Default XBMC Port
|
||||||
|
if not results['port']:
|
||||||
|
results['port'] = NotifyXBMC.xbmc_default_port
|
||||||
|
|
||||||
|
else:
|
||||||
|
# KODI Support
|
||||||
|
results['protocol'] = NotifyXBMC.kodi_remote_protocol
|
||||||
|
|
||||||
|
return results
|
||||||
|
@ -136,7 +136,7 @@ class NotifyXML(NotifyBase):
|
|||||||
# Return; we're done
|
# Return; we're done
|
||||||
return False
|
return False
|
||||||
|
|
||||||
except requests.ConnectionError as e:
|
except requests.RequestException as e:
|
||||||
self.logger.warning(
|
self.logger.warning(
|
||||||
'A Connection error occured sending XML '
|
'A Connection error occured sending XML '
|
||||||
'notification to %s.' % self.host)
|
'notification to %s.' % self.host)
|
||||||
|
@ -133,7 +133,7 @@ def tidy_path(path):
|
|||||||
return path
|
return path
|
||||||
|
|
||||||
|
|
||||||
def parse_url(url, default_schema='http'):
|
def parse_url(url, default_schema='http', verify_host=True):
|
||||||
"""A function that greatly simplifies the parsing of a url
|
"""A function that greatly simplifies the parsing of a url
|
||||||
specified by the end user.
|
specified by the end user.
|
||||||
|
|
||||||
@ -293,7 +293,7 @@ def parse_url(url, default_schema='http'):
|
|||||||
if result['port'] == 0:
|
if result['port'] == 0:
|
||||||
result['port'] = None
|
result['port'] = None
|
||||||
|
|
||||||
if not is_hostname(result['host']):
|
if verify_host and not is_hostname(result['host']):
|
||||||
# Nothing more we can do without a hostname
|
# Nothing more we can do without a hostname
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
@ -27,19 +27,11 @@ import click
|
|||||||
import logging
|
import logging
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
from apprise import Apprise
|
|
||||||
from apprise import AppriseAsset
|
|
||||||
from apprise import NotifyType
|
from apprise import NotifyType
|
||||||
|
import apprise
|
||||||
|
|
||||||
# Logging
|
# Logging
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger('apprise.plugins.NotifyBase')
|
||||||
logger.setLevel(logging.INFO)
|
|
||||||
|
|
||||||
ch = logging.StreamHandler(sys.stdout)
|
|
||||||
ch.setLevel(logging.INFO)
|
|
||||||
formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')
|
|
||||||
ch.setFormatter(formatter)
|
|
||||||
logger.addHandler(ch)
|
|
||||||
|
|
||||||
# Defines our click context settings adding -h to the additional options that
|
# Defines our click context settings adding -h to the additional options that
|
||||||
# can be specified to get the help menu to come up
|
# can be specified to get the help menu to come up
|
||||||
@ -60,41 +52,58 @@ def print_help_msg(command):
|
|||||||
help='Specify the message title.')
|
help='Specify the message title.')
|
||||||
@click.option('--body', '-b', default=None, type=str,
|
@click.option('--body', '-b', default=None, type=str,
|
||||||
help='Specify the message body.')
|
help='Specify the message body.')
|
||||||
@click.option('--notification-type', '-t', default=NotifyType.INFO, type=str,
|
@click.option('--notification-type', '-n', default=NotifyType.INFO, type=str,
|
||||||
metavar='TYPE', help='Specify the message type (default=info).')
|
metavar='TYPE', help='Specify the message type (default=info).')
|
||||||
@click.option('--theme', '-T', default='default', type=str,
|
@click.option('--theme', '-T', default='default', type=str,
|
||||||
help='Specify the default theme.')
|
help='Specify the default theme.')
|
||||||
|
@click.option('-v', '--verbose', count=True)
|
||||||
@click.argument('urls', nargs=-1,
|
@click.argument('urls', nargs=-1,
|
||||||
metavar='SERVER_URL [SERVER_URL2 [SERVER_URL3]]',)
|
metavar='SERVER_URL [SERVER_URL2 [SERVER_URL3]]',)
|
||||||
def _main(title, body, urls, notification_type, theme):
|
def _main(title, body, urls, notification_type, theme, verbose):
|
||||||
"""
|
"""
|
||||||
Send a notification to all of the specified servers identified by their
|
Send a notification to all of the specified servers identified by their
|
||||||
URLs the content provided within the title, body and notification-type.
|
URLs the content provided within the title, body and notification-type.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
# Logging
|
||||||
|
ch = logging.StreamHandler(sys.stdout)
|
||||||
|
if verbose > 2:
|
||||||
|
logger.setLevel(logging.DEBUG)
|
||||||
|
|
||||||
|
elif verbose == 1:
|
||||||
|
logger.setLevel(logging.INFO)
|
||||||
|
|
||||||
|
else:
|
||||||
|
logger.setLevel(logging.NONE)
|
||||||
|
|
||||||
|
formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')
|
||||||
|
ch.setFormatter(formatter)
|
||||||
|
logger.addHandler(ch)
|
||||||
|
|
||||||
if not urls:
|
if not urls:
|
||||||
logger.error('You must specify at least one server URL.')
|
logger.error('You must specify at least one server URL.')
|
||||||
print_help_msg(_main)
|
print_help_msg(_main)
|
||||||
return 1
|
return 1
|
||||||
|
|
||||||
# Prepare our asset
|
# Prepare our asset
|
||||||
asset = AppriseAsset(theme=theme)
|
asset = apprise.AppriseAsset(theme=theme)
|
||||||
|
|
||||||
# Create our object
|
# Create our object
|
||||||
apprise = Apprise(asset=asset)
|
a = apprise.Apprise(asset=asset)
|
||||||
|
|
||||||
# Load our inventory up
|
# Load our inventory up
|
||||||
for url in urls:
|
for url in urls:
|
||||||
apprise.add(url)
|
a.add(url)
|
||||||
|
|
||||||
if body is None:
|
if body is None:
|
||||||
# if no body was specified, then read from STDIN
|
# if no body was specified, then read from STDIN
|
||||||
body = click.get_text_stream('stdin').read()
|
body = click.get_text_stream('stdin').read()
|
||||||
|
|
||||||
# now print it out
|
# now print it out
|
||||||
apprise.notify(title=title, body=body, notify_type=notification_type)
|
if a.notify(title=title, body=body, notify_type=notification_type):
|
||||||
|
return 0
|
||||||
return 0
|
return 1
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
|
@ -316,3 +316,13 @@ def test_apprise_asset(tmpdir):
|
|||||||
|
|
||||||
# Restore our permissions
|
# Restore our permissions
|
||||||
chmod(a.image_path(NotifyType.INFO, NotifyImageSize.XY_256), 0o640)
|
chmod(a.image_path(NotifyType.INFO, NotifyImageSize.XY_256), 0o640)
|
||||||
|
|
||||||
|
# Disable all image references
|
||||||
|
a = AppriseAsset(image_path_mask=False, image_url_mask=False)
|
||||||
|
# We always return none in these calls now
|
||||||
|
assert(a.image_raw(NotifyType.INFO, NotifyImageSize.XY_256) is None)
|
||||||
|
assert(a.image_url(NotifyType.INFO, NotifyImageSize.XY_256) is None)
|
||||||
|
assert(a.image_path(NotifyType.INFO, NotifyImageSize.XY_256,
|
||||||
|
must_exist=False) is None)
|
||||||
|
assert(a.image_path(NotifyType.INFO, NotifyImageSize.XY_256,
|
||||||
|
must_exist=True) is None)
|
||||||
|
@ -78,7 +78,7 @@ def test_notify_base():
|
|||||||
# Create an object with an ImageSize loaded into it
|
# Create an object with an ImageSize loaded into it
|
||||||
nb = NotifyBase(image_size=NotifyImageSize.XY_256)
|
nb = NotifyBase(image_size=NotifyImageSize.XY_256)
|
||||||
|
|
||||||
# We'll get an object thi time around
|
# We'll get an object this time around
|
||||||
assert nb.image_url(notify_type=NotifyType.INFO) is not None
|
assert nb.image_url(notify_type=NotifyType.INFO) is not None
|
||||||
assert nb.image_path(notify_type=NotifyType.INFO) is not None
|
assert nb.image_path(notify_type=NotifyType.INFO) is not None
|
||||||
assert nb.image_raw(notify_type=NotifyType.INFO) is not None
|
assert nb.image_raw(notify_type=NotifyType.INFO) is not None
|
||||||
@ -104,9 +104,13 @@ def test_notify_base():
|
|||||||
'/path/?name=Dr%20Disrespect', unquote=True) == \
|
'/path/?name=Dr%20Disrespect', unquote=True) == \
|
||||||
['path', '?name=Dr', 'Disrespect']
|
['path', '?name=Dr', 'Disrespect']
|
||||||
|
|
||||||
|
# Test is_email
|
||||||
assert NotifyBase.is_email('test@gmail.com') is True
|
assert NotifyBase.is_email('test@gmail.com') is True
|
||||||
assert NotifyBase.is_email('invalid.com') is False
|
assert NotifyBase.is_email('invalid.com') is False
|
||||||
|
|
||||||
|
# Test is_hostname
|
||||||
|
assert NotifyBase.is_hostname('example.com') is True
|
||||||
|
|
||||||
|
|
||||||
def test_notify_base_urls():
|
def test_notify_base_urls():
|
||||||
"""
|
"""
|
||||||
|
@ -19,11 +19,74 @@
|
|||||||
from apprise import plugins
|
from apprise import plugins
|
||||||
from apprise import NotifyType
|
from apprise import NotifyType
|
||||||
from apprise import Apprise
|
from apprise import Apprise
|
||||||
|
from apprise import AppriseAsset
|
||||||
import requests
|
import requests
|
||||||
import mock
|
import mock
|
||||||
|
|
||||||
|
|
||||||
VALID_URLS = (
|
TEST_URLS = (
|
||||||
|
##################################
|
||||||
|
# NotifyBoxcar
|
||||||
|
##################################
|
||||||
|
('boxcar://', {
|
||||||
|
'instance': None,
|
||||||
|
}),
|
||||||
|
# No secret specified
|
||||||
|
('boxcar://%s' % ('a' * 64), {
|
||||||
|
'instance': None,
|
||||||
|
}),
|
||||||
|
# An invalid access and secret key specified
|
||||||
|
('boxcar://access.key/secret.key/', {
|
||||||
|
'instance': plugins.NotifyBoxcar,
|
||||||
|
# Thrown because there were no recipients specified
|
||||||
|
'exception': TypeError,
|
||||||
|
}),
|
||||||
|
# Provide both an access and a secret
|
||||||
|
('boxcar://%s/%s' % ('a' * 64, 'b' * 64), {
|
||||||
|
'instance': plugins.NotifyBoxcar,
|
||||||
|
'requests_response_code': requests.codes.created,
|
||||||
|
}),
|
||||||
|
# Test without image set
|
||||||
|
('boxcar://%s/%s' % ('a' * 64, 'b' * 64), {
|
||||||
|
'instance': plugins.NotifyBoxcar,
|
||||||
|
'requests_response_code': requests.codes.created,
|
||||||
|
# don't include an image by default
|
||||||
|
'include_image': False,
|
||||||
|
}),
|
||||||
|
# our access, secret and device are all 64 characters
|
||||||
|
# which is what we're doing here
|
||||||
|
('boxcar://%s/%s/@tag1/tag2///%s/' % (
|
||||||
|
'a' * 64, 'b' * 64, 'd' * 64), {
|
||||||
|
'instance': plugins.NotifyBoxcar,
|
||||||
|
'requests_response_code': requests.codes.created,
|
||||||
|
}),
|
||||||
|
# An invalid tag
|
||||||
|
('boxcar://%s/%s/@%s' % ('a' * 64, 'b' * 64, 't' * 64), {
|
||||||
|
'instance': plugins.NotifyBoxcar,
|
||||||
|
'requests_response_code': requests.codes.created,
|
||||||
|
}),
|
||||||
|
('boxcar://:@/', {
|
||||||
|
'instance': None,
|
||||||
|
}),
|
||||||
|
('boxcar://%s/%s/' % ('a' * 64, 'b' * 64), {
|
||||||
|
'instance': plugins.NotifyBoxcar,
|
||||||
|
# force a failure
|
||||||
|
'response': False,
|
||||||
|
'requests_response_code': requests.codes.internal_server_error,
|
||||||
|
}),
|
||||||
|
('boxcar://%s/%s/' % ('a' * 64, 'b' * 64), {
|
||||||
|
'instance': plugins.NotifyBoxcar,
|
||||||
|
# throw a bizzare code forcing us to fail to look it up
|
||||||
|
'response': False,
|
||||||
|
'requests_response_code': 999,
|
||||||
|
}),
|
||||||
|
('boxcar://%s/%s/' % ('a' * 64, 'b' * 64), {
|
||||||
|
'instance': plugins.NotifyBoxcar,
|
||||||
|
# Throws a series of connection and transfer exceptions when this flag
|
||||||
|
# is set and tests that we gracfully handle them
|
||||||
|
'test_requests_exceptions': True,
|
||||||
|
}),
|
||||||
|
|
||||||
##################################
|
##################################
|
||||||
# NotifyJSON
|
# NotifyJSON
|
||||||
##################################
|
##################################
|
||||||
@ -64,7 +127,7 @@ VALID_URLS = (
|
|||||||
'instance': plugins.NotifyJSON,
|
'instance': plugins.NotifyJSON,
|
||||||
# force a failure
|
# force a failure
|
||||||
'response': False,
|
'response': False,
|
||||||
'requests_response_code': 500,
|
'requests_response_code': requests.codes.internal_server_error,
|
||||||
}),
|
}),
|
||||||
('json://user:pass@localhost:8082', {
|
('json://user:pass@localhost:8082', {
|
||||||
'instance': plugins.NotifyJSON,
|
'instance': plugins.NotifyJSON,
|
||||||
@ -79,6 +142,76 @@ VALID_URLS = (
|
|||||||
'test_requests_exceptions': True,
|
'test_requests_exceptions': True,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
##################################
|
||||||
|
# NotifyKODI
|
||||||
|
##################################
|
||||||
|
('kodi://', {
|
||||||
|
'instance': None,
|
||||||
|
}),
|
||||||
|
('kodis://', {
|
||||||
|
'instance': None,
|
||||||
|
}),
|
||||||
|
('kodi://localhost', {
|
||||||
|
'instance': plugins.NotifyXBMC,
|
||||||
|
}),
|
||||||
|
('kodi://user:pass@localhost', {
|
||||||
|
'instance': plugins.NotifyXBMC,
|
||||||
|
}),
|
||||||
|
('kodi://localhost:8080', {
|
||||||
|
'instance': plugins.NotifyXBMC,
|
||||||
|
}),
|
||||||
|
('kodi://user:pass@localhost:8080', {
|
||||||
|
'instance': plugins.NotifyXBMC,
|
||||||
|
}),
|
||||||
|
('kodis://localhost', {
|
||||||
|
'instance': plugins.NotifyXBMC,
|
||||||
|
}),
|
||||||
|
('kodis://user:pass@localhost', {
|
||||||
|
'instance': plugins.NotifyXBMC,
|
||||||
|
}),
|
||||||
|
('kodis://localhost:8080/path/', {
|
||||||
|
'instance': plugins.NotifyXBMC,
|
||||||
|
}),
|
||||||
|
('kodis://user:pass@localhost:8080', {
|
||||||
|
'instance': plugins.NotifyXBMC,
|
||||||
|
}),
|
||||||
|
('kodi://localhost', {
|
||||||
|
'instance': plugins.NotifyXBMC,
|
||||||
|
# Experement with different notification types
|
||||||
|
'notify_type': NotifyType.WARNING,
|
||||||
|
}),
|
||||||
|
('kodi://localhost', {
|
||||||
|
'instance': plugins.NotifyXBMC,
|
||||||
|
# Experement with different notification types
|
||||||
|
'notify_type': NotifyType.FAILURE,
|
||||||
|
}),
|
||||||
|
('kodis://localhost:443', {
|
||||||
|
'instance': plugins.NotifyXBMC,
|
||||||
|
# don't include an image by default
|
||||||
|
'include_image': False,
|
||||||
|
}),
|
||||||
|
('kodi://:@/', {
|
||||||
|
'instance': None,
|
||||||
|
}),
|
||||||
|
('kodi://user:pass@localhost:8081', {
|
||||||
|
'instance': plugins.NotifyXBMC,
|
||||||
|
# force a failure
|
||||||
|
'response': False,
|
||||||
|
'requests_response_code': requests.codes.internal_server_error,
|
||||||
|
}),
|
||||||
|
('kodi://user:pass@localhost:8082', {
|
||||||
|
'instance': plugins.NotifyXBMC,
|
||||||
|
# throw a bizzare code forcing us to fail to look it up
|
||||||
|
'response': False,
|
||||||
|
'requests_response_code': 999,
|
||||||
|
}),
|
||||||
|
('kodi://user:pass@localhost:8083', {
|
||||||
|
'instance': plugins.NotifyXBMC,
|
||||||
|
# Throws a series of connection and transfer exceptions when this flag
|
||||||
|
# is set and tests that we gracfully handle them
|
||||||
|
'test_requests_exceptions': True,
|
||||||
|
}),
|
||||||
|
|
||||||
##################################
|
##################################
|
||||||
# NotifyMatterMost
|
# NotifyMatterMost
|
||||||
##################################
|
##################################
|
||||||
@ -123,7 +256,7 @@ VALID_URLS = (
|
|||||||
'instance': plugins.NotifyMatterMost,
|
'instance': plugins.NotifyMatterMost,
|
||||||
# force a failure
|
# force a failure
|
||||||
'response': False,
|
'response': False,
|
||||||
'requests_response_code': 500,
|
'requests_response_code': requests.codes.internal_server_error,
|
||||||
}),
|
}),
|
||||||
('mmost://localhost/3ccdd113474722377935511fc85d3dd4', {
|
('mmost://localhost/3ccdd113474722377935511fc85d3dd4', {
|
||||||
'instance': plugins.NotifyMatterMost,
|
'instance': plugins.NotifyMatterMost,
|
||||||
@ -137,6 +270,184 @@ VALID_URLS = (
|
|||||||
# is set and tests that we gracfully handle them
|
# is set and tests that we gracfully handle them
|
||||||
'test_requests_exceptions': True,
|
'test_requests_exceptions': True,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
##################################
|
||||||
|
# NotifySlack
|
||||||
|
##################################
|
||||||
|
('slack://', {
|
||||||
|
'instance': None,
|
||||||
|
}),
|
||||||
|
('slack://:@/', {
|
||||||
|
'instance': None,
|
||||||
|
}),
|
||||||
|
('slack://T1JJ3T3L2', {
|
||||||
|
# Just Token 1 provided
|
||||||
|
'instance': None,
|
||||||
|
}),
|
||||||
|
('slack://T1JJ3T3L2/A1BRTD4JD/TIiajkdnlazkcOXrIdevi7FQ/#hmm/#-invalid-', {
|
||||||
|
# No username specified; this is still okay as we sub in
|
||||||
|
# default; The one invalid channel is skipped when sending a message
|
||||||
|
'instance': plugins.NotifySlack,
|
||||||
|
}),
|
||||||
|
('slack://T1JJ3T3L2/A1BRTD4JD/TIiajkdnlazkcOXrIdevi7FQ/#channel', {
|
||||||
|
# No username specified; this is still okay as we sub in
|
||||||
|
# default; The one invalid channel is skipped when sending a message
|
||||||
|
'instance': plugins.NotifySlack,
|
||||||
|
# don't include an image by default
|
||||||
|
'include_image': False,
|
||||||
|
}),
|
||||||
|
('slack://T1JJ3T3L2/A1BRTD4JD/TIiajkdnlazkcOXrIdevi7FQ/+id/%20/@id/', {
|
||||||
|
# + encoded id,
|
||||||
|
# @ userid
|
||||||
|
'instance': plugins.NotifySlack,
|
||||||
|
}),
|
||||||
|
('slack://username@T1JJ3T3L2/A1BRTD4JD/TIiajkdnlazkcOXrIdevi7FQ/#nuxref', {
|
||||||
|
'instance': plugins.NotifySlack,
|
||||||
|
}),
|
||||||
|
('slack://username@T1JJ3T3L2/A1BRTD4JD/TIiajkdnlazkcOXrIdevi7FQ', {
|
||||||
|
# Missing a channel
|
||||||
|
'exception': TypeError,
|
||||||
|
}),
|
||||||
|
('slack://username@INVALID/A1BRTD4JD/TIiajkdnlazkcOXrIdevi7FQ/#cool', {
|
||||||
|
# invalid 1st Token
|
||||||
|
'exception': TypeError,
|
||||||
|
}),
|
||||||
|
('slack://username@T1JJ3T3L2/INVALID/TIiajkdnlazkcOXrIdevi7FQ/#great', {
|
||||||
|
# invalid 2rd Token
|
||||||
|
'exception': TypeError,
|
||||||
|
}),
|
||||||
|
('slack://username@T1JJ3T3L2/A1BRTD4JD/INVALID/#channel', {
|
||||||
|
# invalid 3rd Token
|
||||||
|
'exception': TypeError,
|
||||||
|
}),
|
||||||
|
('slack://l2g@T1JJ3T3L2/A1BRTD4JD/TIiajkdnlazkcOXrIdevi7FQ/#usenet', {
|
||||||
|
'instance': plugins.NotifySlack,
|
||||||
|
# force a failure
|
||||||
|
'response': False,
|
||||||
|
'requests_response_code': requests.codes.internal_server_error,
|
||||||
|
}),
|
||||||
|
('slack://respect@T1JJ3T3L2/A1BRTD4JD/TIiajkdnlazkcOXrIdevi7FQ/#a', {
|
||||||
|
'instance': plugins.NotifySlack,
|
||||||
|
# throw a bizzare code forcing us to fail to look it up
|
||||||
|
'response': False,
|
||||||
|
'requests_response_code': 999,
|
||||||
|
}),
|
||||||
|
('slack://notify@T1JJ3T3L2/A1BRTD4JD/TIiajkdnlazkcOXrIdevi7FQ/#b', {
|
||||||
|
'instance': plugins.NotifySlack,
|
||||||
|
# Throws a series of connection and transfer exceptions when this flag
|
||||||
|
# is set and tests that we gracfully handle them
|
||||||
|
'test_requests_exceptions': True,
|
||||||
|
}),
|
||||||
|
|
||||||
|
##################################
|
||||||
|
# NotifyKODI
|
||||||
|
##################################
|
||||||
|
('xbmc://', {
|
||||||
|
'instance': None,
|
||||||
|
}),
|
||||||
|
('xbmc://localhost', {
|
||||||
|
'instance': plugins.NotifyXBMC,
|
||||||
|
}),
|
||||||
|
('xbmc://user:pass@localhost', {
|
||||||
|
'instance': plugins.NotifyXBMC,
|
||||||
|
}),
|
||||||
|
('xbmc://localhost:8080', {
|
||||||
|
'instance': plugins.NotifyXBMC,
|
||||||
|
}),
|
||||||
|
('xbmc://user:pass@localhost:8080', {
|
||||||
|
'instance': plugins.NotifyXBMC,
|
||||||
|
}),
|
||||||
|
('xbmc://localhost', {
|
||||||
|
'instance': plugins.NotifyXBMC,
|
||||||
|
# don't include an image by default
|
||||||
|
'include_image': False,
|
||||||
|
}),
|
||||||
|
('xbmc://localhost', {
|
||||||
|
'instance': plugins.NotifyXBMC,
|
||||||
|
# Experement with different notification types
|
||||||
|
'notify_type': NotifyType.WARNING,
|
||||||
|
}),
|
||||||
|
('xbmc://localhost', {
|
||||||
|
'instance': plugins.NotifyXBMC,
|
||||||
|
# Experement with different notification types
|
||||||
|
'notify_type': NotifyType.FAILURE,
|
||||||
|
}),
|
||||||
|
('xbmc://:@/', {
|
||||||
|
'instance': None,
|
||||||
|
}),
|
||||||
|
('xbmc://user:pass@localhost:8081', {
|
||||||
|
'instance': plugins.NotifyXBMC,
|
||||||
|
# force a failure
|
||||||
|
'response': False,
|
||||||
|
'requests_response_code': requests.codes.internal_server_error,
|
||||||
|
}),
|
||||||
|
('xbmc://user:pass@localhost:8082', {
|
||||||
|
'instance': plugins.NotifyXBMC,
|
||||||
|
# throw a bizzare code forcing us to fail to look it up
|
||||||
|
'response': False,
|
||||||
|
'requests_response_code': 999,
|
||||||
|
}),
|
||||||
|
('xbmc://user:pass@localhost:8083', {
|
||||||
|
'instance': plugins.NotifyXBMC,
|
||||||
|
# Throws a series of connection and transfer exceptions when this flag
|
||||||
|
# is set and tests that we gracfully handle them
|
||||||
|
'test_requests_exceptions': True,
|
||||||
|
}),
|
||||||
|
|
||||||
|
##################################
|
||||||
|
# NotifyXML
|
||||||
|
##################################
|
||||||
|
('xml://', {
|
||||||
|
'instance': None,
|
||||||
|
}),
|
||||||
|
('xmls://', {
|
||||||
|
'instance': None,
|
||||||
|
}),
|
||||||
|
('xml://localhost', {
|
||||||
|
'instance': plugins.NotifyXML,
|
||||||
|
}),
|
||||||
|
('xml://user:pass@localhost', {
|
||||||
|
'instance': plugins.NotifyXML,
|
||||||
|
}),
|
||||||
|
('xml://localhost:8080', {
|
||||||
|
'instance': plugins.NotifyXML,
|
||||||
|
}),
|
||||||
|
('xml://user:pass@localhost:8080', {
|
||||||
|
'instance': plugins.NotifyXML,
|
||||||
|
}),
|
||||||
|
('xmls://localhost', {
|
||||||
|
'instance': plugins.NotifyXML,
|
||||||
|
}),
|
||||||
|
('xmls://user:pass@localhost', {
|
||||||
|
'instance': plugins.NotifyXML,
|
||||||
|
}),
|
||||||
|
('xmls://localhost:8080/path/', {
|
||||||
|
'instance': plugins.NotifyXML,
|
||||||
|
}),
|
||||||
|
('xmls://user:pass@localhost:8080', {
|
||||||
|
'instance': plugins.NotifyXML,
|
||||||
|
}),
|
||||||
|
('xml://:@/', {
|
||||||
|
'instance': None,
|
||||||
|
}),
|
||||||
|
('xml://user:pass@localhost:8081', {
|
||||||
|
'instance': plugins.NotifyXML,
|
||||||
|
# force a failure
|
||||||
|
'response': False,
|
||||||
|
'requests_response_code': requests.codes.internal_server_error,
|
||||||
|
}),
|
||||||
|
('xml://user:pass@localhost:8082', {
|
||||||
|
'instance': plugins.NotifyXML,
|
||||||
|
# throw a bizzare code forcing us to fail to look it up
|
||||||
|
'response': False,
|
||||||
|
'requests_response_code': 999,
|
||||||
|
}),
|
||||||
|
('xml://user:pass@localhost:8083', {
|
||||||
|
'instance': plugins.NotifyXML,
|
||||||
|
# Throws a series of connection and transfer exceptions when this flag
|
||||||
|
# is set and tests that we gracfully handle them
|
||||||
|
'test_requests_exceptions': True,
|
||||||
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -149,7 +460,7 @@ def test_rest_plugins(mock_post, mock_get):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
# iterate over our dictionary and test it out
|
# iterate over our dictionary and test it out
|
||||||
for (url, meta) in VALID_URLS:
|
for (url, meta) in TEST_URLS:
|
||||||
|
|
||||||
# Our expected instance
|
# Our expected instance
|
||||||
instance = meta.get('instance', None)
|
instance = meta.get('instance', None)
|
||||||
@ -166,7 +477,23 @@ def test_rest_plugins(mock_post, mock_get):
|
|||||||
# Allow us to force the server response code to be something other then
|
# Allow us to force the server response code to be something other then
|
||||||
# the defaults
|
# the defaults
|
||||||
requests_response_code = meta.get(
|
requests_response_code = meta.get(
|
||||||
'requests_response_code', 200 if response else 404)
|
'requests_response_code',
|
||||||
|
requests.codes.ok if response else requests.codes.not_found,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Allow notification type override, otherwise default to INFO
|
||||||
|
notify_type = meta.get('notify_type', NotifyType.INFO)
|
||||||
|
|
||||||
|
# Whether or not we should include an image with our request; unless
|
||||||
|
# otherwise specified, we assume that images are to be included
|
||||||
|
include_image = meta.get('include_image', True)
|
||||||
|
if include_image:
|
||||||
|
# a default asset
|
||||||
|
asset = AppriseAsset()
|
||||||
|
|
||||||
|
else:
|
||||||
|
# Disable images
|
||||||
|
asset = AppriseAsset(image_path_mask=False, image_url_mask=False)
|
||||||
|
|
||||||
test_requests_exceptions = meta.get(
|
test_requests_exceptions = meta.get(
|
||||||
'test_requests_exceptions', False)
|
'test_requests_exceptions', False)
|
||||||
@ -197,7 +524,8 @@ def test_rest_plugins(mock_post, mock_get):
|
|||||||
)
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
obj = Apprise.instantiate(url, suppress_exceptions=False)
|
obj = Apprise.instantiate(
|
||||||
|
url, asset=asset, suppress_exceptions=False)
|
||||||
|
|
||||||
assert(exception is None)
|
assert(exception is None)
|
||||||
|
|
||||||
@ -224,7 +552,7 @@ def test_rest_plugins(mock_post, mock_get):
|
|||||||
# check that we're as expected
|
# check that we're as expected
|
||||||
assert obj.notify(
|
assert obj.notify(
|
||||||
title='test', body='body',
|
title='test', body='body',
|
||||||
notify_type=NotifyType.INFO) == response
|
notify_type=notify_type) == response
|
||||||
|
|
||||||
else:
|
else:
|
||||||
for exception in test_requests_exceptions:
|
for exception in test_requests_exceptions:
|
||||||
@ -255,7 +583,7 @@ def test_rest_plugins(mock_post, mock_get):
|
|||||||
|
|
||||||
except AssertionError:
|
except AssertionError:
|
||||||
# Don't mess with these entries
|
# Don't mess with these entries
|
||||||
print('%s / %s' % (url, str(e)))
|
print('%s AssertionError' % url)
|
||||||
raise
|
raise
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@ -263,3 +591,106 @@ def test_rest_plugins(mock_post, mock_get):
|
|||||||
print('%s / %s' % (url, str(e)))
|
print('%s / %s' % (url, str(e)))
|
||||||
assert(exception is not None)
|
assert(exception is not None)
|
||||||
assert(isinstance(e, exception))
|
assert(isinstance(e, exception))
|
||||||
|
|
||||||
|
|
||||||
|
@mock.patch('requests.get')
|
||||||
|
@mock.patch('requests.post')
|
||||||
|
def test_notify_boxcar_plugin(mock_post, mock_get):
|
||||||
|
"""
|
||||||
|
API: NotifyBoxcar() Extra Checks
|
||||||
|
|
||||||
|
"""
|
||||||
|
# Generate some generic message types
|
||||||
|
device = 'A' * 64
|
||||||
|
tag = '@B' * 63
|
||||||
|
|
||||||
|
access = '-' * 64
|
||||||
|
secret = '_' * 64
|
||||||
|
|
||||||
|
# Initializes the plugin with recipients set to None
|
||||||
|
plugins.NotifyBoxcar(access=access, secret=secret, recipients=None)
|
||||||
|
|
||||||
|
# Initializes the plugin with a valid access, but invalid access key
|
||||||
|
try:
|
||||||
|
plugins.NotifyBoxcar(access=None, secret=secret, recipients=None)
|
||||||
|
assert(False)
|
||||||
|
|
||||||
|
except TypeError:
|
||||||
|
# We should throw an exception for knowingly having an invalid
|
||||||
|
assert(True)
|
||||||
|
|
||||||
|
# Initializes the plugin with a valid access, but invalid secret key
|
||||||
|
try:
|
||||||
|
plugins.NotifyBoxcar(access=access, secret='invalid', recipients=None)
|
||||||
|
assert(False)
|
||||||
|
|
||||||
|
except TypeError:
|
||||||
|
# We should throw an exception for knowingly having an invalid key
|
||||||
|
assert(True)
|
||||||
|
|
||||||
|
# Initializes the plugin with a valid access, but invalid secret
|
||||||
|
try:
|
||||||
|
plugins.NotifyBoxcar(access=access, secret=None, recipients=None)
|
||||||
|
assert(False)
|
||||||
|
|
||||||
|
except TypeError:
|
||||||
|
# We should throw an exception for knowingly having an invalid
|
||||||
|
assert(True)
|
||||||
|
|
||||||
|
# Initializes the plugin with recipients list
|
||||||
|
# the below also tests our the variation of recipient types
|
||||||
|
plugins.NotifyBoxcar(
|
||||||
|
access=access, secret=secret, recipients=[device, tag])
|
||||||
|
|
||||||
|
mock_get.return_value = requests.Request()
|
||||||
|
mock_post.return_value = requests.Request()
|
||||||
|
mock_post.return_value.status_code = requests.codes.created
|
||||||
|
mock_get.return_value.status_code = requests.codes.created
|
||||||
|
# Test notifications without a body or a title
|
||||||
|
p = plugins.NotifyBoxcar(access=access, secret=secret, recipients=None)
|
||||||
|
p.notify(body=None, title=None, notify_type=NotifyType.INFO) is True
|
||||||
|
|
||||||
|
|
||||||
|
@mock.patch('requests.get')
|
||||||
|
@mock.patch('requests.post')
|
||||||
|
def test_notify_slack_plugin(mock_post, mock_get):
|
||||||
|
"""
|
||||||
|
API: NotifySlack() Extra Checks
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Initialize some generic (but valid) tokens
|
||||||
|
token_a = 'A' * 9
|
||||||
|
token_b = 'B' * 9
|
||||||
|
token_c = 'c' * 24
|
||||||
|
|
||||||
|
# Support strings
|
||||||
|
channels = 'chan1,#chan2,+id,@user,,,'
|
||||||
|
|
||||||
|
obj = plugins.NotifySlack(
|
||||||
|
token_a=token_a, token_b=token_b, token_c=token_c, channels=channels)
|
||||||
|
assert(len(obj.channels) == 4)
|
||||||
|
mock_get.return_value = requests.Request()
|
||||||
|
mock_post.return_value = requests.Request()
|
||||||
|
mock_post.return_value.status_code = requests.codes.ok
|
||||||
|
mock_get.return_value.status_code = requests.codes.ok
|
||||||
|
|
||||||
|
# Empty Channel list
|
||||||
|
try:
|
||||||
|
plugins.NotifySlack(
|
||||||
|
token_a=token_a, token_b=token_b, token_c=token_c,
|
||||||
|
channels=None)
|
||||||
|
assert(False)
|
||||||
|
|
||||||
|
except TypeError:
|
||||||
|
# we'll thrown because an empty list of channels was provided
|
||||||
|
assert(True)
|
||||||
|
|
||||||
|
# Test include_image
|
||||||
|
obj = plugins.NotifySlack(
|
||||||
|
token_a=token_a, token_b=token_b, token_c=token_c, channels=channels,
|
||||||
|
include_image=True)
|
||||||
|
|
||||||
|
# This call includes an image with it's payload:
|
||||||
|
assert obj.notify(title='title', body='body',
|
||||||
|
notify_type=NotifyType.INFO) is True
|
||||||
|
Loading…
Reference in New Issue
Block a user