mirror of
https://github.com/caronc/apprise.git
synced 2025-03-06 11:01:41 +01:00
more unit tests; 90% coverage
This commit is contained in:
parent
f8c3d35f8c
commit
cc79763b3f
BIN
apprise/assets/themes/default/apprise-failure-32x32.png
Normal file
BIN
apprise/assets/themes/default/apprise-failure-32x32.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.4 KiB |
BIN
apprise/assets/themes/default/apprise-info-32x32.png
Normal file
BIN
apprise/assets/themes/default/apprise-info-32x32.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.5 KiB |
BIN
apprise/assets/themes/default/apprise-success-32x32.png
Normal file
BIN
apprise/assets/themes/default/apprise-success-32x32.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.5 KiB |
BIN
apprise/assets/themes/default/apprise-warning-32x32.png
Normal file
BIN
apprise/assets/themes/default/apprise-warning-32x32.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.4 KiB |
@ -41,12 +41,14 @@ class NotifyImageSize(object):
|
|||||||
A list of pre-defined image sizes to make it easier to work with defined
|
A list of pre-defined image sizes to make it easier to work with defined
|
||||||
plugins.
|
plugins.
|
||||||
"""
|
"""
|
||||||
|
XY_32 = '32x32'
|
||||||
XY_72 = '72x72'
|
XY_72 = '72x72'
|
||||||
XY_128 = '128x128'
|
XY_128 = '128x128'
|
||||||
XY_256 = '256x256'
|
XY_256 = '256x256'
|
||||||
|
|
||||||
|
|
||||||
NOTIFY_IMAGE_SIZES = (
|
NOTIFY_IMAGE_SIZES = (
|
||||||
|
NotifyImageSize.XY_32,
|
||||||
NotifyImageSize.XY_72,
|
NotifyImageSize.XY_72,
|
||||||
NotifyImageSize.XY_128,
|
NotifyImageSize.XY_128,
|
||||||
NotifyImageSize.XY_256,
|
NotifyImageSize.XY_256,
|
||||||
|
@ -183,13 +183,9 @@ class NotifyMyAndroid(NotifyBase):
|
|||||||
|
|
||||||
if 'format' in results['qsd'] and len(results['qsd']['format']):
|
if 'format' in results['qsd'] and len(results['qsd']['format']):
|
||||||
# Extract email format (Text/Html)
|
# Extract email format (Text/Html)
|
||||||
try:
|
format = NotifyBase.unquote(results['qsd']['format']).lower()
|
||||||
format = NotifyBase.unquote(results['qsd']['format']).lower()
|
if len(format) > 0 and format[0] == 't':
|
||||||
if len(format) > 0 and format[0] == 't':
|
results['notify_format'] = NotifyFormat.TEXT
|
||||||
results['notify_format'] = NotifyFormat.TEXT
|
|
||||||
|
|
||||||
except AttributeError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
if 'priority' in results['qsd'] and len(results['qsd']['priority']):
|
if 'priority' in results['qsd'] and len(results['qsd']['priority']):
|
||||||
_map = {
|
_map = {
|
||||||
|
@ -31,7 +31,7 @@ VALIDATE_PROVIDERKEY = re.compile(r'[A-Za-z0-9]{40}')
|
|||||||
|
|
||||||
# Priorities
|
# Priorities
|
||||||
class ProwlPriority(object):
|
class ProwlPriority(object):
|
||||||
VERY_LOW = -2
|
LOW = -2
|
||||||
MODERATE = -1
|
MODERATE = -1
|
||||||
NORMAL = 0
|
NORMAL = 0
|
||||||
HIGH = 1
|
HIGH = 1
|
||||||
@ -39,7 +39,7 @@ class ProwlPriority(object):
|
|||||||
|
|
||||||
|
|
||||||
PROWL_PRIORITIES = (
|
PROWL_PRIORITIES = (
|
||||||
ProwlPriority.VERY_LOW,
|
ProwlPriority.LOW,
|
||||||
ProwlPriority.MODERATE,
|
ProwlPriority.MODERATE,
|
||||||
ProwlPriority.NORMAL,
|
ProwlPriority.NORMAL,
|
||||||
ProwlPriority.HIGH,
|
ProwlPriority.HIGH,
|
||||||
@ -65,8 +65,7 @@ class NotifyProwl(NotifyBase):
|
|||||||
# Prowl uses the http protocol with JSON requests
|
# Prowl uses the http protocol with JSON requests
|
||||||
notify_url = 'https://api.prowlapp.com/publicapi/add'
|
notify_url = 'https://api.prowlapp.com/publicapi/add'
|
||||||
|
|
||||||
def __init__(self, apikey, providerkey=None, priority=ProwlPriority.NORMAL,
|
def __init__(self, apikey, providerkey=None, priority=None, **kwargs):
|
||||||
**kwargs):
|
|
||||||
"""
|
"""
|
||||||
Initialize Prowl Object
|
Initialize Prowl Object
|
||||||
"""
|
"""
|
||||||
@ -146,7 +145,7 @@ class NotifyProwl(NotifyBase):
|
|||||||
PROWL_HTTP_ERROR_MAP[r.status_code],
|
PROWL_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 Prowl notification '
|
'Failed to send Prowl notification '
|
||||||
'(error=%s).' % (
|
'(error=%s).' % (
|
||||||
@ -159,7 +158,7 @@ class NotifyProwl(NotifyBase):
|
|||||||
else:
|
else:
|
||||||
self.logger.info('Sent Prowl notification.')
|
self.logger.info('Sent Prowl notification.')
|
||||||
|
|
||||||
except requests.ConnectionError as e:
|
except requests.RequestException as e:
|
||||||
self.logger.warning(
|
self.logger.warning(
|
||||||
'A Connection error occured sending Prowl notification.')
|
'A Connection error occured sending Prowl notification.')
|
||||||
self.logger.debug('Socket Exception: %s' % str(e))
|
self.logger.debug('Socket Exception: %s' % str(e))
|
||||||
@ -186,15 +185,33 @@ class NotifyProwl(NotifyBase):
|
|||||||
|
|
||||||
# optionally find the provider key
|
# optionally find the provider key
|
||||||
try:
|
try:
|
||||||
providerkey = filter(
|
providerkey = [x for x in filter(
|
||||||
bool, NotifyBase.split_path(results['fullpath']))[0]
|
bool, NotifyBase.split_path(results['fullpath']))][0]
|
||||||
|
|
||||||
if not providerkey:
|
|
||||||
providerkey = None
|
|
||||||
|
|
||||||
except (AttributeError, IndexError):
|
except (AttributeError, IndexError):
|
||||||
providerkey = None
|
providerkey = None
|
||||||
|
|
||||||
|
if 'priority' in results['qsd'] and len(results['qsd']['priority']):
|
||||||
|
_map = {
|
||||||
|
'l': ProwlPriority.LOW,
|
||||||
|
'-2': ProwlPriority.LOW,
|
||||||
|
'm': ProwlPriority.MODERATE,
|
||||||
|
'-1': ProwlPriority.MODERATE,
|
||||||
|
'n': ProwlPriority.NORMAL,
|
||||||
|
'0': ProwlPriority.NORMAL,
|
||||||
|
'h': ProwlPriority.HIGH,
|
||||||
|
'1': ProwlPriority.HIGH,
|
||||||
|
'e': ProwlPriority.EMERGENCY,
|
||||||
|
'2': ProwlPriority.EMERGENCY,
|
||||||
|
}
|
||||||
|
try:
|
||||||
|
results['priority'] = \
|
||||||
|
_map[results['qsd']['priority'][0].lower()]
|
||||||
|
|
||||||
|
except KeyError:
|
||||||
|
# No priority was set
|
||||||
|
pass
|
||||||
|
|
||||||
results['apikey'] = results['host']
|
results['apikey'] = results['host']
|
||||||
results['providerkey'] = providerkey
|
results['providerkey'] = providerkey
|
||||||
|
|
||||||
|
@ -60,11 +60,10 @@ class NotifyPushBullet(NotifyBase):
|
|||||||
|
|
||||||
self.accesstoken = accesstoken
|
self.accesstoken = accesstoken
|
||||||
if compat_is_basestring(recipients):
|
if compat_is_basestring(recipients):
|
||||||
self.recipients = filter(bool, RECIPIENTS_LIST_DELIM.split(
|
self.recipients = [x for x in filter(
|
||||||
recipients,
|
bool, RECIPIENTS_LIST_DELIM.split(recipients))]
|
||||||
))
|
|
||||||
|
|
||||||
elif isinstance(recipients, (tuple, list)):
|
elif isinstance(recipients, (set, tuple, list)):
|
||||||
self.recipients = recipients
|
self.recipients = recipients
|
||||||
|
|
||||||
else:
|
else:
|
||||||
@ -138,7 +137,7 @@ class NotifyPushBullet(NotifyBase):
|
|||||||
PUSHBULLET_HTTP_ERROR_MAP[r.status_code],
|
PUSHBULLET_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 PushBullet notification '
|
'Failed to send PushBullet notification '
|
||||||
'(error=%s).' % r.status_code)
|
'(error=%s).' % r.status_code)
|
||||||
@ -148,7 +147,7 @@ class NotifyPushBullet(NotifyBase):
|
|||||||
# Return; we're done
|
# Return; we're done
|
||||||
has_error = True
|
has_error = True
|
||||||
|
|
||||||
except requests.ConnectionError as e:
|
except requests.RequestException as e:
|
||||||
self.logger.warning(
|
self.logger.warning(
|
||||||
'A Connection error occured sending PushBullet '
|
'A Connection error occured sending PushBullet '
|
||||||
'notification.'
|
'notification.'
|
||||||
@ -176,11 +175,7 @@ class NotifyPushBullet(NotifyBase):
|
|||||||
return results
|
return results
|
||||||
|
|
||||||
# Apply our settings now
|
# Apply our settings now
|
||||||
try:
|
recipients = NotifyBase.unquote(results['fullpath'])
|
||||||
recipients = NotifyBase.unquote(results['fullpath'])
|
|
||||||
|
|
||||||
except AttributeError:
|
|
||||||
recipients = ''
|
|
||||||
|
|
||||||
results['accesstoken'] = results['host']
|
results['accesstoken'] = results['host']
|
||||||
results['recipients'] = recipients
|
results['recipients'] = recipients
|
||||||
|
@ -43,10 +43,7 @@ class NotifyPushalot(NotifyBase):
|
|||||||
A wrapper for Pushalot Notifications
|
A wrapper for Pushalot Notifications
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# The default protocol
|
# The default protocol is always secured
|
||||||
protocol = 'palot'
|
|
||||||
|
|
||||||
# The default secure protocol
|
|
||||||
secure_protocol = 'palot'
|
secure_protocol = 'palot'
|
||||||
|
|
||||||
# Pushalot uses the http protocol with JSON requests
|
# Pushalot uses the http protocol with JSON requests
|
||||||
@ -117,7 +114,7 @@ class NotifyPushalot(NotifyBase):
|
|||||||
PUSHALOT_HTTP_ERROR_MAP[r.status_code],
|
PUSHALOT_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 Pushalot notification '
|
'Failed to send Pushalot notification '
|
||||||
'(error=%s).' % r.status_code)
|
'(error=%s).' % r.status_code)
|
||||||
@ -128,7 +125,7 @@ class NotifyPushalot(NotifyBase):
|
|||||||
else:
|
else:
|
||||||
self.logger.info('Sent Pushalot notification.')
|
self.logger.info('Sent Pushalot notification.')
|
||||||
|
|
||||||
except requests.ConnectionError as e:
|
except requests.RequestException as e:
|
||||||
self.logger.warning(
|
self.logger.warning(
|
||||||
'A Connection error occured sending Pushalot notification.')
|
'A Connection error occured sending Pushalot notification.')
|
||||||
self.logger.debug('Socket Exception: %s' % str(e))
|
self.logger.debug('Socket Exception: %s' % str(e))
|
||||||
|
@ -27,18 +27,18 @@ from .NotifyBase import HTTP_ERROR_MAP
|
|||||||
PUSHOVER_SEND_TO_ALL = 'ALL_DEVICES'
|
PUSHOVER_SEND_TO_ALL = 'ALL_DEVICES'
|
||||||
|
|
||||||
# Used to validate API Key
|
# Used to validate API Key
|
||||||
VALIDATE_TOKEN = re.compile(r'[A-Za-z0-9]{30}')
|
VALIDATE_TOKEN = re.compile(r'^[a-z0-9]{30}$', re.I)
|
||||||
|
|
||||||
# Used to detect a User and/or Group
|
# Used to detect a User and/or Group
|
||||||
VALIDATE_USERGROUP = re.compile(r'[A-Za-z0-9]{30}')
|
VALIDATE_USERGROUP = re.compile(r'^[a-z0-9]{30}$', re.I)
|
||||||
|
|
||||||
# Used to detect a User and/or Group
|
# Used to detect a User and/or Group
|
||||||
VALIDATE_DEVICE = re.compile(r'[A-Za-z0-9_]{1,25}')
|
VALIDATE_DEVICE = re.compile(r'^[a-z0-9_]{1,25}$', re.I)
|
||||||
|
|
||||||
|
|
||||||
# Priorities
|
# Priorities
|
||||||
class PushoverPriority(object):
|
class PushoverPriority(object):
|
||||||
VERY_LOW = -2
|
LOW = -2
|
||||||
MODERATE = -1
|
MODERATE = -1
|
||||||
NORMAL = 0
|
NORMAL = 0
|
||||||
HIGH = 1
|
HIGH = 1
|
||||||
@ -46,7 +46,7 @@ class PushoverPriority(object):
|
|||||||
|
|
||||||
|
|
||||||
PUSHOVER_PRIORITIES = (
|
PUSHOVER_PRIORITIES = (
|
||||||
PushoverPriority.VERY_LOW,
|
PushoverPriority.LOW,
|
||||||
PushoverPriority.MODERATE,
|
PushoverPriority.MODERATE,
|
||||||
PushoverPriority.NORMAL,
|
PushoverPriority.NORMAL,
|
||||||
PushoverPriority.HIGH,
|
PushoverPriority.HIGH,
|
||||||
@ -68,24 +68,29 @@ class NotifyPushover(NotifyBase):
|
|||||||
A wrapper for Pushover Notifications
|
A wrapper for Pushover Notifications
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# The default protocol
|
# All pushover requests are secure
|
||||||
protocol = 'pover'
|
|
||||||
|
|
||||||
# The default secure protocol
|
|
||||||
secure_protocol = 'pover'
|
secure_protocol = 'pover'
|
||||||
|
|
||||||
# Pushover uses the http protocol with JSON requests
|
# Pushover uses the http protocol with JSON requests
|
||||||
notify_url = 'https://api.pushover.net/1/messages.json'
|
notify_url = 'https://api.pushover.net/1/messages.json'
|
||||||
|
|
||||||
def __init__(self, token, devices=None,
|
def __init__(self, token, devices=None, priority=None, **kwargs):
|
||||||
priority=PushoverPriority.NORMAL, **kwargs):
|
|
||||||
"""
|
"""
|
||||||
Initialize Pushover Object
|
Initialize Pushover Object
|
||||||
"""
|
"""
|
||||||
super(NotifyPushover, self).__init__(
|
super(NotifyPushover, self).__init__(
|
||||||
title_maxlen=250, body_maxlen=512, **kwargs)
|
title_maxlen=250, body_maxlen=512, **kwargs)
|
||||||
|
|
||||||
if not VALIDATE_TOKEN.match(token.strip()):
|
try:
|
||||||
|
# The token associated with the account
|
||||||
|
self.token = token.strip()
|
||||||
|
|
||||||
|
except AttributeError:
|
||||||
|
# Token was None
|
||||||
|
self.logger.warning('No API Token was specified.')
|
||||||
|
raise TypeError('No API Token was specified.')
|
||||||
|
|
||||||
|
if not VALIDATE_TOKEN.match(self.token):
|
||||||
self.logger.warning(
|
self.logger.warning(
|
||||||
'The API Token specified (%s) is invalid.' % token,
|
'The API Token specified (%s) is invalid.' % token,
|
||||||
)
|
)
|
||||||
@ -93,13 +98,10 @@ class NotifyPushover(NotifyBase):
|
|||||||
'The API Token specified (%s) is invalid.' % token,
|
'The API Token specified (%s) is invalid.' % token,
|
||||||
)
|
)
|
||||||
|
|
||||||
# The token associated with the account
|
|
||||||
self.token = token.strip()
|
|
||||||
|
|
||||||
if compat_is_basestring(devices):
|
if compat_is_basestring(devices):
|
||||||
self.devices = filter(bool, DEVICE_LIST_DELIM.split(
|
self.devices = [x for x in filter(bool, DEVICE_LIST_DELIM.split(
|
||||||
devices,
|
devices,
|
||||||
))
|
))]
|
||||||
|
|
||||||
elif isinstance(devices, (set, tuple, list)):
|
elif isinstance(devices, (set, tuple, list)):
|
||||||
self.devices = devices
|
self.devices = devices
|
||||||
@ -121,10 +123,6 @@ class NotifyPushover(NotifyBase):
|
|||||||
self.logger.warning('No user was specified.')
|
self.logger.warning('No user was specified.')
|
||||||
raise TypeError('No user was specified.')
|
raise TypeError('No user was specified.')
|
||||||
|
|
||||||
if not self.token:
|
|
||||||
self.logger.warning('No token was specified.')
|
|
||||||
raise TypeError('No token was specified.')
|
|
||||||
|
|
||||||
if not VALIDATE_USERGROUP.match(self.user):
|
if not VALIDATE_USERGROUP.match(self.user):
|
||||||
self.logger.warning(
|
self.logger.warning(
|
||||||
'The user/group specified (%s) is invalid.' % self.user,
|
'The user/group specified (%s) is invalid.' % self.user,
|
||||||
@ -152,6 +150,13 @@ class NotifyPushover(NotifyBase):
|
|||||||
while len(devices):
|
while len(devices):
|
||||||
device = devices.pop(0)
|
device = devices.pop(0)
|
||||||
|
|
||||||
|
if VALIDATE_DEVICE.match(device) is None:
|
||||||
|
self.logger.warning(
|
||||||
|
'The device specified (%s) is invalid.' % device,
|
||||||
|
)
|
||||||
|
has_error = True
|
||||||
|
continue
|
||||||
|
|
||||||
# prepare JSON Object
|
# prepare JSON Object
|
||||||
payload = {
|
payload = {
|
||||||
'token': self.token,
|
'token': self.token,
|
||||||
@ -159,18 +164,9 @@ class NotifyPushover(NotifyBase):
|
|||||||
'priority': str(self.priority),
|
'priority': str(self.priority),
|
||||||
'title': title,
|
'title': title,
|
||||||
'message': body,
|
'message': body,
|
||||||
|
'device': device,
|
||||||
}
|
}
|
||||||
|
|
||||||
if device != PUSHOVER_SEND_TO_ALL:
|
|
||||||
if not VALIDATE_DEVICE.match(device):
|
|
||||||
self.logger.warning(
|
|
||||||
'The device specified (%s) is invalid.' % device,
|
|
||||||
)
|
|
||||||
has_error = True
|
|
||||||
continue
|
|
||||||
|
|
||||||
payload['device'] = device
|
|
||||||
|
|
||||||
self.logger.debug('Pushover POST URL: %s (cert_verify=%r)' % (
|
self.logger.debug('Pushover POST URL: %s (cert_verify=%r)' % (
|
||||||
self.notify_url, self.verify_certificate,
|
self.notify_url, self.verify_certificate,
|
||||||
))
|
))
|
||||||
@ -193,7 +189,7 @@ class NotifyPushover(NotifyBase):
|
|||||||
PUSHOVER_HTTP_ERROR_MAP[r.status_code],
|
PUSHOVER_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 Pushover:%s '
|
'Failed to send Pushover:%s '
|
||||||
'notification (error=%s).' % (
|
'notification (error=%s).' % (
|
||||||
@ -205,7 +201,7 @@ class NotifyPushover(NotifyBase):
|
|||||||
# Return; we're done
|
# Return; we're done
|
||||||
has_error = True
|
has_error = True
|
||||||
|
|
||||||
except requests.ConnectionError as e:
|
except requests.RequestException as e:
|
||||||
self.logger.warning(
|
self.logger.warning(
|
||||||
'A Connection error occured sending Pushover:%s ' % (
|
'A Connection error occured sending Pushover:%s ' % (
|
||||||
device) + 'notification.'
|
device) + 'notification.'
|
||||||
@ -217,7 +213,7 @@ class NotifyPushover(NotifyBase):
|
|||||||
# Prevent thrashing requests
|
# Prevent thrashing requests
|
||||||
self.throttle()
|
self.throttle()
|
||||||
|
|
||||||
return has_error
|
return not has_error
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def parse_url(url):
|
def parse_url(url):
|
||||||
@ -233,11 +229,28 @@ class NotifyPushover(NotifyBase):
|
|||||||
return results
|
return results
|
||||||
|
|
||||||
# Apply our settings now
|
# Apply our settings now
|
||||||
try:
|
devices = NotifyBase.unquote(results['fullpath'])
|
||||||
devices = NotifyBase.unquote(results['fullpath'])
|
|
||||||
|
|
||||||
except AttributeError:
|
if 'priority' in results['qsd'] and len(results['qsd']['priority']):
|
||||||
devices = ''
|
_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'] = \
|
||||||
|
_map[results['qsd']['priority'][0].lower()]
|
||||||
|
|
||||||
|
except KeyError:
|
||||||
|
# No priority was set
|
||||||
|
pass
|
||||||
|
|
||||||
results['token'] = results['host']
|
results['token'] = results['host']
|
||||||
results['devices'] = devices
|
results['devices'] = devices
|
||||||
|
@ -173,7 +173,7 @@ class NotifyRocketChat(NotifyBase):
|
|||||||
'%s (error=%s).' % (
|
'%s (error=%s).' % (
|
||||||
RC_HTTP_ERROR_MAP[r.status_code],
|
RC_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 Rocket.Chat notification ' +
|
'Failed to send Rocket.Chat notification ' +
|
||||||
'(error=%s).' % (
|
'(error=%s).' % (
|
||||||
@ -186,7 +186,7 @@ class NotifyRocketChat(NotifyBase):
|
|||||||
self.logger.debug('Rocket.Chat Server Response: %s.' % r.text)
|
self.logger.debug('Rocket.Chat Server Response: %s.' % r.text)
|
||||||
self.logger.info('Sent Rocket.Chat notification.')
|
self.logger.info('Sent Rocket.Chat notification.')
|
||||||
|
|
||||||
except requests.ConnectionError as e:
|
except requests.RequestException as e:
|
||||||
self.logger.warning(
|
self.logger.warning(
|
||||||
'A Connection error occured sending Rocket.Chat ' +
|
'A Connection error occured sending Rocket.Chat ' +
|
||||||
'notification.')
|
'notification.')
|
||||||
@ -277,6 +277,7 @@ class NotifyRocketChat(NotifyBase):
|
|||||||
'%s (error=%s).' % (
|
'%s (error=%s).' % (
|
||||||
RC_HTTP_ERROR_MAP[r.status_code],
|
RC_HTTP_ERROR_MAP[r.status_code],
|
||||||
r.status_code))
|
r.status_code))
|
||||||
|
|
||||||
except IndexError:
|
except IndexError:
|
||||||
self.logger.warning(
|
self.logger.warning(
|
||||||
'Failed to log off Rocket.Chat server ' +
|
'Failed to log off Rocket.Chat server ' +
|
||||||
@ -316,10 +317,6 @@ class NotifyRocketChat(NotifyBase):
|
|||||||
return results
|
return results
|
||||||
|
|
||||||
# Apply our settings now
|
# Apply our settings now
|
||||||
try:
|
results['recipients'] = NotifyBase.unquote(results['fullpath'])
|
||||||
results['recipients'] = NotifyBase.unquote(results['fullpath'])
|
|
||||||
|
|
||||||
except AttributeError:
|
|
||||||
return None
|
|
||||||
|
|
||||||
return results
|
return results
|
||||||
|
@ -49,13 +49,13 @@ from json import dumps
|
|||||||
from .NotifyBase import NotifyBase
|
from .NotifyBase import NotifyBase
|
||||||
from .NotifyBase import NotifyFormat
|
from .NotifyBase import NotifyFormat
|
||||||
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
|
||||||
|
|
||||||
# Token required as part of the API request
|
# Token required as part of the API request
|
||||||
# allow the word 'bot' infront
|
# allow the word 'bot' infront
|
||||||
VALIDATE_BOT_TOKEN = re.compile(
|
VALIDATE_BOT_TOKEN = re.compile(
|
||||||
r'(bot)?(?P<key>[0-9]+:[A-Za-z0-9_-]+)/*$',
|
r'^(bot)?(?P<key>[0-9]+:[a-z0-9_-]+)/*$',
|
||||||
re.IGNORECASE,
|
re.IGNORECASE,
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -63,7 +63,7 @@ VALIDATE_BOT_TOKEN = re.compile(
|
|||||||
# If the Chat ID is positive, then it's addressed to a single person
|
# If the Chat ID is positive, then it's addressed to a single person
|
||||||
# If the Chat ID is negative, then it's targeting a group
|
# If the Chat ID is negative, then it's targeting a group
|
||||||
IS_CHAT_ID_RE = re.compile(
|
IS_CHAT_ID_RE = re.compile(
|
||||||
r'(@*(?P<idno>-?[0-9]{1,32})|(?P<name>[a-z_-][a-z0-9_-]*))',
|
r'^(@*(?P<idno>-?[0-9]{1,32})|(?P<name>[a-z_-][a-z0-9_-]+))$',
|
||||||
re.IGNORECASE,
|
re.IGNORECASE,
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -71,7 +71,7 @@ IS_CHAT_ID_RE = re.compile(
|
|||||||
# The stickers/images are kind of big and consume a lot of space
|
# The stickers/images are kind of big and consume a lot of space
|
||||||
# It's not as appealing as just having the post not contain
|
# It's not as appealing as just having the post not contain
|
||||||
# an image at all.
|
# an image at all.
|
||||||
TELEGRAM_IMAGE_XY = None
|
TELEGRAM_IMAGE_XY = NotifyImageSize.XY_32
|
||||||
|
|
||||||
# Used to break path apart into list of chat identifiers
|
# Used to break path apart into list of chat identifiers
|
||||||
CHAT_ID_LIST_DELIM = re.compile(r'[ \t\r\n,#\\/]+')
|
CHAT_ID_LIST_DELIM = re.compile(r'[ \t\r\n,#\\/]+')
|
||||||
@ -88,32 +88,37 @@ class NotifyTelegram(NotifyBase):
|
|||||||
# Telegram uses the http protocol with JSON requests
|
# Telegram uses the http protocol with JSON requests
|
||||||
notify_url = 'https://api.telegram.org/bot'
|
notify_url = 'https://api.telegram.org/bot'
|
||||||
|
|
||||||
def __init__(self, bot_token, chat_ids, **kwargs):
|
def __init__(self, bot_token, chat_ids, notify_format=NotifyFormat.HTML,
|
||||||
|
**kwargs):
|
||||||
"""
|
"""
|
||||||
Initialize Telegram Object
|
Initialize Telegram Object
|
||||||
"""
|
"""
|
||||||
super(NotifyTelegram, self).__init__(
|
super(NotifyTelegram, self).__init__(
|
||||||
title_maxlen=250, body_maxlen=4096,
|
title_maxlen=250, body_maxlen=4096,
|
||||||
image_size=TELEGRAM_IMAGE_XY, **kwargs)
|
image_size=TELEGRAM_IMAGE_XY, notify_format=notify_format,
|
||||||
|
**kwargs)
|
||||||
|
|
||||||
if bot_token is None:
|
try:
|
||||||
raise TypeError(
|
self.bot_token = bot_token.strip()
|
||||||
'The Bot Token specified is invalid.'
|
|
||||||
)
|
|
||||||
|
|
||||||
result = VALIDATE_BOT_TOKEN.match(bot_token.strip())
|
except AttributeError:
|
||||||
|
# Token was None
|
||||||
|
self.logger.warning('No Bot Token was specified.')
|
||||||
|
raise TypeError('No Bot Token was specified.')
|
||||||
|
|
||||||
|
result = VALIDATE_BOT_TOKEN.match(self.bot_token)
|
||||||
if not result:
|
if not result:
|
||||||
raise TypeError(
|
raise TypeError(
|
||||||
'The Bot Token specified (%s) is invalid.' % bot_token,
|
'The Bot Token specified (%s) is invalid.' % bot_token,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Store our API Key
|
# Store our Bot Token
|
||||||
self.bot_token = result.group('key')
|
self.bot_token = result.group('key')
|
||||||
|
|
||||||
if compat_is_basestring(chat_ids):
|
if compat_is_basestring(chat_ids):
|
||||||
self.chat_ids = filter(bool, CHAT_ID_LIST_DELIM.split(
|
self.chat_ids = [x for x in filter(bool, CHAT_ID_LIST_DELIM.split(
|
||||||
chat_ids,
|
chat_ids,
|
||||||
))
|
))]
|
||||||
|
|
||||||
elif isinstance(chat_ids, (set, tuple, list)):
|
elif isinstance(chat_ids, (set, tuple, list)):
|
||||||
self.chat_ids = list(chat_ids)
|
self.chat_ids = list(chat_ids)
|
||||||
@ -125,51 +130,54 @@ class NotifyTelegram(NotifyBase):
|
|||||||
# Treat this as a channel too
|
# Treat this as a channel too
|
||||||
self.chat_ids.append(self.user)
|
self.chat_ids.append(self.user)
|
||||||
|
|
||||||
# Bot's can't send messages to themselves which is fair enough
|
|
||||||
# but if or when they can, this code will allow a default fallback
|
|
||||||
# solution if no chat_id and/or channel is specified
|
|
||||||
# if len(self.chat_ids) == 0:
|
|
||||||
#
|
|
||||||
# chat_id = self._get_chat_id()
|
|
||||||
# if chat_id is not None:
|
|
||||||
# self.logger.warning(
|
|
||||||
# 'No chat_id or @channel was specified; ' +\
|
|
||||||
# 'using detected bot_chat_id (%d).' % chat_id,
|
|
||||||
# )
|
|
||||||
# self.chat_ids.append(str(chat_id))
|
|
||||||
|
|
||||||
if len(self.chat_ids) == 0:
|
if len(self.chat_ids) == 0:
|
||||||
self.logger.warning('No chat_id(s) were specified.')
|
self.logger.warning('No chat_id(s) were specified.')
|
||||||
raise TypeError('No chat_id(s) were specified.')
|
raise TypeError('No chat_id(s) were specified.')
|
||||||
|
|
||||||
def _get_chat_id(self):
|
def notify_image(self, chat_id, notify_type, **kwargs):
|
||||||
"""
|
"""
|
||||||
This function retrieves the chat id belonging to the key specified
|
Sends the notification image based on the specified chat id
|
||||||
"""
|
|
||||||
headers = {
|
|
||||||
'User-Agent': self.app_id,
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
}
|
|
||||||
|
|
||||||
|
"""
|
||||||
|
image_content = self.image_raw(notify_type)
|
||||||
|
if image_content is None:
|
||||||
|
# Nothing to do
|
||||||
|
return True
|
||||||
|
|
||||||
|
# prepare our image URL
|
||||||
url = '%s%s/%s' % (
|
url = '%s%s/%s' % (
|
||||||
self.notify_url,
|
self.notify_url,
|
||||||
self.bot_token,
|
self.bot_token,
|
||||||
'getMe'
|
'sendPhoto'
|
||||||
)
|
)
|
||||||
|
|
||||||
self.logger.debug('Telegram (Detection) GET URL: %s' % url)
|
# Set up our upload
|
||||||
|
files = {'photo': ('%s.png' % notify_type, image_content)}
|
||||||
|
|
||||||
|
payload = {
|
||||||
|
'chat_id': chat_id,
|
||||||
|
'disable_notification': True,
|
||||||
|
}
|
||||||
|
|
||||||
|
self.logger.debug(
|
||||||
|
'Telegram (image) POST URL: %s (cert_verify=%r)' % (
|
||||||
|
url, self.verify_certificate))
|
||||||
|
|
||||||
|
self.logger.debug(
|
||||||
|
'Telegram (image) Payload: %s' % str(payload))
|
||||||
|
|
||||||
chat_id = None
|
|
||||||
try:
|
try:
|
||||||
r = requests.post(url, headers=headers)
|
r = requests.post(
|
||||||
if r.status_code == requests.codes.ok:
|
url,
|
||||||
# Extract our chat ID
|
data=payload,
|
||||||
result = loads(r.text)
|
headers={
|
||||||
if result.get('ok', False) is True:
|
'User-Agent': self.app_id,
|
||||||
chat_id = result['result'].get('id')
|
},
|
||||||
if chat_id <= 0:
|
files=files,
|
||||||
chat_id = None
|
verify=self.verify_certificate,
|
||||||
else:
|
)
|
||||||
|
|
||||||
|
if r.status_code != requests.codes.ok:
|
||||||
# We had a problem
|
# We had a problem
|
||||||
try:
|
try:
|
||||||
# Try to get the error message if we can:
|
# Try to get the error message if we can:
|
||||||
@ -181,28 +189,38 @@ class NotifyTelegram(NotifyBase):
|
|||||||
try:
|
try:
|
||||||
if error_msg:
|
if error_msg:
|
||||||
self.logger.warning(
|
self.logger.warning(
|
||||||
'Failed to lookup Telegram chat_id from '
|
'Failed to send Telegram Image:%s '
|
||||||
'apikey: (%s) %s.' % (r.status_code, error_msg))
|
'notification: (%s) %s.' % (
|
||||||
|
payload['chat_id'],
|
||||||
|
r.status_code, error_msg))
|
||||||
|
|
||||||
else:
|
else:
|
||||||
self.logger.warning(
|
self.logger.warning(
|
||||||
'Failed to lookup Telegram chat_id from '
|
'Failed to send Telegram Image:%s '
|
||||||
'apikey: %s (error=%s).' % (
|
'notification: %s (error=%s).' % (
|
||||||
|
payload['chat_id'],
|
||||||
HTTP_ERROR_MAP[r.status_code],
|
HTTP_ERROR_MAP[r.status_code],
|
||||||
r.status_code))
|
r.status_code))
|
||||||
|
|
||||||
except IndexError:
|
except KeyError:
|
||||||
self.logger.warning(
|
self.logger.warning(
|
||||||
'Failed to lookup Telegram chat_id from '
|
'Failed to send Telegram Image:%s '
|
||||||
'apikey: (error=%s).' % r.status_code)
|
'notification (error=%s).' % (
|
||||||
|
payload['chat_id'],
|
||||||
|
r.status_code))
|
||||||
|
|
||||||
except requests.ConnectionError as e:
|
return False
|
||||||
|
|
||||||
|
except requests.RequestException as e:
|
||||||
self.logger.warning(
|
self.logger.warning(
|
||||||
'A Connection error occured looking up Telegram chat_id '
|
'A Connection error occured sending Telegram:%s ' % (
|
||||||
'from apikey.')
|
payload['chat_id']) + 'notification.'
|
||||||
|
)
|
||||||
self.logger.debug('Socket Exception: %s' % str(e))
|
self.logger.debug('Socket Exception: %s' % str(e))
|
||||||
|
return False
|
||||||
|
|
||||||
return chat_id
|
# We were successful
|
||||||
|
return True
|
||||||
|
|
||||||
def notify(self, title, body, notify_type, **kwargs):
|
def notify(self, title, body, notify_type, **kwargs):
|
||||||
"""
|
"""
|
||||||
@ -217,19 +235,6 @@ class NotifyTelegram(NotifyBase):
|
|||||||
# error tracking (used for function return)
|
# error tracking (used for function return)
|
||||||
has_error = False
|
has_error = False
|
||||||
|
|
||||||
image_url = None
|
|
||||||
image_content = self.image_raw(notify_type)
|
|
||||||
if image_content is not None:
|
|
||||||
# prepare our image URL
|
|
||||||
image_url = '%s%s/%s' % (
|
|
||||||
self.notify_url,
|
|
||||||
self.bot_token,
|
|
||||||
'sendPhoto'
|
|
||||||
)
|
|
||||||
|
|
||||||
# Set up our upload
|
|
||||||
files = {'photo': ('%s.png' % notify_type, image_content)}
|
|
||||||
|
|
||||||
url = '%s%s/%s' % (
|
url = '%s%s/%s' % (
|
||||||
self.notify_url,
|
self.notify_url,
|
||||||
self.bot_token,
|
self.bot_token,
|
||||||
@ -263,6 +268,7 @@ class NotifyTelegram(NotifyBase):
|
|||||||
chat_id,
|
chat_id,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
has_error = True
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if chat_id.group('name') is not None:
|
if chat_id.group('name') is not None:
|
||||||
@ -273,72 +279,19 @@ class NotifyTelegram(NotifyBase):
|
|||||||
# ID
|
# ID
|
||||||
payload['chat_id'] = chat_id.group('idno')
|
payload['chat_id'] = chat_id.group('idno')
|
||||||
|
|
||||||
if image_url is not None:
|
if not self.notify_image(
|
||||||
image_payload = {
|
chat_id=payload['chat_id'], notify_type=notify_type):
|
||||||
'chat_id': payload['chat_id'],
|
# Uh oh... The image failed to post if we get here
|
||||||
'disable_notification': True,
|
|
||||||
}
|
|
||||||
|
|
||||||
self.logger.debug(
|
if len(chat_ids) > 0:
|
||||||
'Telegram (image) POST URL: %s (cert_verify=%r)' % (
|
# Prevent thrashing requests
|
||||||
image_url, self.verify_certificate))
|
self.throttle()
|
||||||
|
|
||||||
self.logger.debug(
|
# Flag our error
|
||||||
'Telegram (image) Payload: %s' % str(image_payload))
|
has_error = True
|
||||||
|
|
||||||
try:
|
# Move along
|
||||||
r = requests.post(
|
continue
|
||||||
image_url,
|
|
||||||
data=image_payload,
|
|
||||||
headers={
|
|
||||||
'User-Agent': self.app_id,
|
|
||||||
},
|
|
||||||
files=files,
|
|
||||||
verify=self.verify_certificate,
|
|
||||||
)
|
|
||||||
if r.status_code != requests.codes.ok:
|
|
||||||
# We had a problem
|
|
||||||
|
|
||||||
try:
|
|
||||||
# Try to get the error message if we can:
|
|
||||||
error_msg = loads(r.text)['description']
|
|
||||||
except:
|
|
||||||
error_msg = None
|
|
||||||
|
|
||||||
try:
|
|
||||||
if error_msg:
|
|
||||||
self.logger.warning(
|
|
||||||
'Failed to send Telegram Image:%s '
|
|
||||||
'notification: (%s) %s.' % (
|
|
||||||
payload['chat_id'],
|
|
||||||
r.status_code, error_msg))
|
|
||||||
|
|
||||||
else:
|
|
||||||
self.logger.warning(
|
|
||||||
'Failed to send Telegram Image:%s '
|
|
||||||
'notification: %s (error=%s).' % (
|
|
||||||
payload['chat_id'],
|
|
||||||
HTTP_ERROR_MAP[r.status_code],
|
|
||||||
r.status_code))
|
|
||||||
|
|
||||||
except IndexError:
|
|
||||||
self.logger.warning(
|
|
||||||
'Failed to send Telegram Image:%s '
|
|
||||||
'notification (error=%s).' % (
|
|
||||||
payload['chat_id'],
|
|
||||||
r.status_code))
|
|
||||||
|
|
||||||
has_error = True
|
|
||||||
continue
|
|
||||||
|
|
||||||
except requests.ConnectionError as e:
|
|
||||||
self.logger.warning(
|
|
||||||
'A Connection error occured sending Telegram:%s ' % (
|
|
||||||
payload['chat_id']) + 'notification.'
|
|
||||||
)
|
|
||||||
self.logger.debug('Socket Exception: %s' % str(e))
|
|
||||||
has_error = True
|
|
||||||
continue
|
|
||||||
|
|
||||||
self.logger.debug('Telegram POST URL: %s' % url)
|
self.logger.debug('Telegram POST URL: %s' % url)
|
||||||
self.logger.debug('Telegram POST URL: %s (cert_verify=%r)' % (
|
self.logger.debug('Telegram POST URL: %s (cert_verify=%r)' % (
|
||||||
@ -353,12 +306,14 @@ class NotifyTelegram(NotifyBase):
|
|||||||
headers=headers,
|
headers=headers,
|
||||||
verify=self.verify_certificate,
|
verify=self.verify_certificate,
|
||||||
)
|
)
|
||||||
|
|
||||||
if r.status_code != requests.codes.ok:
|
if r.status_code != requests.codes.ok:
|
||||||
# We had a problem
|
# We had a problem
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Try to get the error message if we can:
|
# Try to get the error message if we can:
|
||||||
error_msg = loads(r.text)['description']
|
error_msg = loads(r.text)['description']
|
||||||
|
|
||||||
except:
|
except:
|
||||||
error_msg = None
|
error_msg = None
|
||||||
|
|
||||||
@ -378,7 +333,7 @@ class NotifyTelegram(NotifyBase):
|
|||||||
HTTP_ERROR_MAP[r.status_code],
|
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 Telegram:%s '
|
'Failed to send Telegram:%s '
|
||||||
'notification (error=%s).' % (
|
'notification (error=%s).' % (
|
||||||
@ -386,10 +341,10 @@ class NotifyTelegram(NotifyBase):
|
|||||||
|
|
||||||
# self.logger.debug('Response Details: %s' % r.raw.read())
|
# self.logger.debug('Response Details: %s' % r.raw.read())
|
||||||
|
|
||||||
# Return; we're done
|
# Flag our error
|
||||||
has_error = True
|
has_error = True
|
||||||
|
|
||||||
except requests.ConnectionError as e:
|
except requests.RequestException as e:
|
||||||
self.logger.warning(
|
self.logger.warning(
|
||||||
'A Connection error occured sending Telegram:%s ' % (
|
'A Connection error occured sending Telegram:%s ' % (
|
||||||
payload['chat_id']) + 'notification.'
|
payload['chat_id']) + 'notification.'
|
||||||
@ -397,11 +352,12 @@ class NotifyTelegram(NotifyBase):
|
|||||||
self.logger.debug('Socket Exception: %s' % str(e))
|
self.logger.debug('Socket Exception: %s' % str(e))
|
||||||
has_error = True
|
has_error = True
|
||||||
|
|
||||||
if len(chat_ids):
|
finally:
|
||||||
# Prevent thrashing requests
|
if len(chat_ids):
|
||||||
self.throttle()
|
# Prevent thrashing requests
|
||||||
|
self.throttle()
|
||||||
|
|
||||||
return has_error
|
return not has_error
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def parse_url(url):
|
def parse_url(url):
|
||||||
@ -410,14 +366,6 @@ class NotifyTelegram(NotifyBase):
|
|||||||
us to substantiate this object.
|
us to substantiate this object.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
# super() is formatted slightly different when dealing with
|
|
||||||
# static method inheritance
|
|
||||||
results = NotifyBase.parse_url(url)
|
|
||||||
|
|
||||||
if results:
|
|
||||||
# We're done early
|
|
||||||
return results
|
|
||||||
|
|
||||||
# This is a dirty hack; but it's the only work around to
|
# This is a dirty hack; but it's the only work around to
|
||||||
# tgram:// messages since the bot_token has a colon in it.
|
# tgram:// messages since the bot_token has a colon in it.
|
||||||
# It invalidates an normal URL.
|
# It invalidates an normal URL.
|
||||||
@ -427,11 +375,16 @@ class NotifyTelegram(NotifyBase):
|
|||||||
# alternative is to ask users to actually change the colon
|
# alternative is to ask users to actually change the colon
|
||||||
# into a slash (which will work too), but it's more likely
|
# into a slash (which will work too), but it's more likely
|
||||||
# to cause confusion... So this is the next best thing
|
# to cause confusion... So this is the next best thing
|
||||||
tgram = re.match(
|
try:
|
||||||
r'(?P<protocol>%s://)(bot)?(?P<prefix>([a-z0-9_-]+)'
|
tgram = re.match(
|
||||||
r'(:[a-z0-9_-]+)?@)?(?P<btoken_a>[0-9]+):+'
|
r'(?P<protocol>%s://)(bot)?(?P<prefix>([a-z0-9_-]+)'
|
||||||
r'(?P<remaining>.*)$' % 'tgram',
|
r'(:[a-z0-9_-]+)?@)?(?P<btoken_a>[0-9]+):+'
|
||||||
url, re.I)
|
r'(?P<remaining>.*)$' % NotifyTelegram.secure_protocol,
|
||||||
|
url, re.I)
|
||||||
|
|
||||||
|
except (TypeError, AttributeError):
|
||||||
|
# url is bad; force tgram to be None
|
||||||
|
tgram = None
|
||||||
|
|
||||||
if not tgram:
|
if not tgram:
|
||||||
# Content is simply not parseable
|
# Content is simply not parseable
|
||||||
@ -439,7 +392,7 @@ class NotifyTelegram(NotifyBase):
|
|||||||
|
|
||||||
if tgram.group('prefix'):
|
if tgram.group('prefix'):
|
||||||
# Try again
|
# Try again
|
||||||
result = NotifyBase.parse_url(
|
results = NotifyBase.parse_url(
|
||||||
'%s%s%s/%s' % (
|
'%s%s%s/%s' % (
|
||||||
tgram.group('protocol'),
|
tgram.group('protocol'),
|
||||||
tgram.group('prefix'),
|
tgram.group('prefix'),
|
||||||
@ -450,7 +403,7 @@ class NotifyTelegram(NotifyBase):
|
|||||||
|
|
||||||
else:
|
else:
|
||||||
# Try again
|
# Try again
|
||||||
result = NotifyBase.parse_url(
|
results = NotifyBase.parse_url(
|
||||||
'%s%s/%s' % (
|
'%s%s/%s' % (
|
||||||
tgram.group('protocol'),
|
tgram.group('protocol'),
|
||||||
tgram.group('btoken_a'),
|
tgram.group('btoken_a'),
|
||||||
@ -459,30 +412,22 @@ class NotifyTelegram(NotifyBase):
|
|||||||
)
|
)
|
||||||
|
|
||||||
# The first token is stored in the hostnamee
|
# The first token is stored in the hostnamee
|
||||||
bot_token_a = result['host']
|
bot_token_a = results['host']
|
||||||
|
|
||||||
# Now fetch the remaining tokens
|
# Now fetch the remaining tokens
|
||||||
try:
|
bot_token_b = [x for x in filter(
|
||||||
bot_token_b = filter(
|
bool, NotifyBase.split_path(results['fullpath']))][0]
|
||||||
bool, NotifyBase.split_path(result['fullpath']))[0]
|
|
||||||
|
|
||||||
bot_token = '%s:%s' % (bot_token_a, bot_token_b)
|
bot_token = '%s:%s' % (bot_token_a, bot_token_b)
|
||||||
|
|
||||||
except (AttributeError, IndexError):
|
chat_ids = ','.join(
|
||||||
# Force a bad value that will get caught in parsing later
|
[x for x in filter(
|
||||||
bot_token = None
|
bool, NotifyBase.split_path(results['fullpath']))][1:])
|
||||||
|
|
||||||
try:
|
# Store our bot token
|
||||||
chat_ids = ','.join(
|
results['bot_token'] = bot_token
|
||||||
filter(bool, NotifyBase.split_path(result['fullpath']))[1:])
|
|
||||||
|
|
||||||
except (AttributeError, IndexError):
|
# Store our chat ids
|
||||||
# Force some bad values that will get caught
|
results['chat_ids'] = chat_ids
|
||||||
# in parsing later
|
|
||||||
chat_ids = None
|
|
||||||
|
|
||||||
# Return our results
|
return results
|
||||||
return result + {
|
|
||||||
'bot_token': bot_token,
|
|
||||||
'chat_ids': chat_ids,
|
|
||||||
}.items()
|
|
||||||
|
@ -52,14 +52,17 @@ class NotifyToasty(NotifyBase):
|
|||||||
**kwargs)
|
**kwargs)
|
||||||
|
|
||||||
if compat_is_basestring(devices):
|
if compat_is_basestring(devices):
|
||||||
self.devices = filter(bool, DEVICES_LIST_DELIM.split(
|
self.devices = [x for x in filter(bool, DEVICES_LIST_DELIM.split(
|
||||||
devices,
|
devices,
|
||||||
))
|
))]
|
||||||
|
|
||||||
elif isinstance(devices, (tuple, list)):
|
elif isinstance(devices, (set, tuple, list)):
|
||||||
self.devices = devices
|
self.devices = devices
|
||||||
|
|
||||||
else:
|
else:
|
||||||
|
self.devices = list()
|
||||||
|
|
||||||
|
if len(devices) == 0:
|
||||||
raise TypeError('You must specify at least 1 device.')
|
raise TypeError('You must specify at least 1 device.')
|
||||||
|
|
||||||
if not self.user:
|
if not self.user:
|
||||||
@ -118,7 +121,7 @@ class NotifyToasty(NotifyBase):
|
|||||||
HTTP_ERROR_MAP[r.status_code],
|
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 Toasty:%s '
|
'Failed to send Toasty:%s '
|
||||||
'notification (error=%s).' % (
|
'notification (error=%s).' % (
|
||||||
@ -130,7 +133,7 @@ class NotifyToasty(NotifyBase):
|
|||||||
# Return; we're done
|
# Return; we're done
|
||||||
has_error = True
|
has_error = True
|
||||||
|
|
||||||
except requests.ConnectionError as e:
|
except requests.RequestException as e:
|
||||||
self.logger.warning(
|
self.logger.warning(
|
||||||
'A Connection error occured sending Toasty:%s ' % (
|
'A Connection error occured sending Toasty:%s ' % (
|
||||||
device) + 'notification.'
|
device) + 'notification.'
|
||||||
@ -142,7 +145,7 @@ class NotifyToasty(NotifyBase):
|
|||||||
# Prevent thrashing requests
|
# Prevent thrashing requests
|
||||||
self.throttle()
|
self.throttle()
|
||||||
|
|
||||||
return has_error
|
return not has_error
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def parse_url(url):
|
def parse_url(url):
|
||||||
@ -158,11 +161,7 @@ class NotifyToasty(NotifyBase):
|
|||||||
return results
|
return results
|
||||||
|
|
||||||
# Apply our settings now
|
# Apply our settings now
|
||||||
try:
|
devices = NotifyBase.unquote(results['fullpath'])
|
||||||
devices = NotifyBase.unquote(results['fullpath'])
|
|
||||||
|
|
||||||
except AttributeError:
|
|
||||||
devices = ''
|
|
||||||
|
|
||||||
# Store our devices
|
# Store our devices
|
||||||
results['devices'] = '%s/%s' % (results['host'], devices)
|
results['devices'] = '%s/%s' % (results['host'], devices)
|
||||||
|
@ -20,10 +20,10 @@ from apprise import plugins
|
|||||||
from apprise import NotifyType
|
from apprise import NotifyType
|
||||||
from apprise import Apprise
|
from apprise import Apprise
|
||||||
from apprise import AppriseAsset
|
from apprise import AppriseAsset
|
||||||
|
from json import dumps
|
||||||
import requests
|
import requests
|
||||||
import mock
|
import mock
|
||||||
|
|
||||||
|
|
||||||
TEST_URLS = (
|
TEST_URLS = (
|
||||||
##################################
|
##################################
|
||||||
# NotifyBoxcar
|
# NotifyBoxcar
|
||||||
@ -176,7 +176,6 @@ TEST_URLS = (
|
|||||||
'response': False,
|
'response': False,
|
||||||
'requests_response_code': 999,
|
'requests_response_code': 999,
|
||||||
}),
|
}),
|
||||||
# apikey = a
|
|
||||||
('join://%s' % ('a' * 32), {
|
('join://%s' % ('a' * 32), {
|
||||||
'instance': plugins.NotifyJoin,
|
'instance': plugins.NotifyJoin,
|
||||||
# Throws a series of connection and transfer exceptions when this flag
|
# Throws a series of connection and transfer exceptions when this flag
|
||||||
@ -380,8 +379,6 @@ TEST_URLS = (
|
|||||||
}),
|
}),
|
||||||
# Invalid APIKey
|
# Invalid APIKey
|
||||||
('nma://%s' % ('a' * 24), {
|
('nma://%s' % ('a' * 24), {
|
||||||
'instance': None,
|
|
||||||
# Missing a channel
|
|
||||||
'exception': TypeError,
|
'exception': TypeError,
|
||||||
}),
|
}),
|
||||||
# APIKey
|
# APIKey
|
||||||
@ -390,6 +387,42 @@ TEST_URLS = (
|
|||||||
# don't include an image by default
|
# don't include an image by default
|
||||||
'include_image': False,
|
'include_image': False,
|
||||||
}),
|
}),
|
||||||
|
# APIKey + priority setting
|
||||||
|
('nma://%s?priority=high' % ('a' * 48), {
|
||||||
|
'instance': plugins.NotifyMyAndroid,
|
||||||
|
}),
|
||||||
|
# APIKey + invalid priority setting
|
||||||
|
('nma://%s?priority=invalid' % ('a' * 48), {
|
||||||
|
'instance': plugins.NotifyMyAndroid,
|
||||||
|
}),
|
||||||
|
# APIKey + priority setting (empty)
|
||||||
|
('nma://%s?priority=' % ('a' * 48), {
|
||||||
|
'instance': plugins.NotifyMyAndroid,
|
||||||
|
}),
|
||||||
|
# APIKey + Invalid DevAPI Key
|
||||||
|
('nma://%s/%s' % ('a' * 48, 'b' * 24), {
|
||||||
|
'exception': TypeError,
|
||||||
|
}),
|
||||||
|
# APIKey + DevAPI Key
|
||||||
|
('nma://%s/%s' % ('a' * 48, 'b' * 48), {
|
||||||
|
'instance': plugins.NotifyMyAndroid,
|
||||||
|
}),
|
||||||
|
# Testing valid format
|
||||||
|
('nma://%s?format=text' % ('a' * 48), {
|
||||||
|
'instance': plugins.NotifyMyAndroid,
|
||||||
|
}),
|
||||||
|
# Testing valid format
|
||||||
|
('nma://%s?format=html' % ('a' * 48), {
|
||||||
|
'instance': plugins.NotifyMyAndroid,
|
||||||
|
}),
|
||||||
|
# Testing invalid format (fall's back to html)
|
||||||
|
('nma://%s?format=invalid' % ('a' * 48), {
|
||||||
|
'instance': plugins.NotifyMyAndroid,
|
||||||
|
}),
|
||||||
|
# Testing empty format (falls back to html)
|
||||||
|
('nma://%s?format=' % ('a' * 48), {
|
||||||
|
'instance': plugins.NotifyMyAndroid,
|
||||||
|
}),
|
||||||
# APIKey + with image
|
# APIKey + with image
|
||||||
('nma://%s' % ('a' * 48), {
|
('nma://%s' % ('a' * 48), {
|
||||||
'instance': plugins.NotifyMyAndroid,
|
'instance': plugins.NotifyMyAndroid,
|
||||||
@ -410,7 +443,6 @@ TEST_URLS = (
|
|||||||
'response': False,
|
'response': False,
|
||||||
'requests_response_code': 999,
|
'requests_response_code': 999,
|
||||||
}),
|
}),
|
||||||
# apikey = a
|
|
||||||
('nma://%s' % ('a' * 48), {
|
('nma://%s' % ('a' * 48), {
|
||||||
'instance': plugins.NotifyMyAndroid,
|
'instance': plugins.NotifyMyAndroid,
|
||||||
# Throws a series of connection and transfer exceptions when this flag
|
# Throws a series of connection and transfer exceptions when this flag
|
||||||
@ -418,6 +450,262 @@ TEST_URLS = (
|
|||||||
'test_requests_exceptions': True,
|
'test_requests_exceptions': True,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
##################################
|
||||||
|
# NotifyProwl
|
||||||
|
##################################
|
||||||
|
('prowl://', {
|
||||||
|
'instance': None,
|
||||||
|
}),
|
||||||
|
# APIkey; no device
|
||||||
|
('prowl://%s' % ('a' * 40), {
|
||||||
|
'instance': plugins.NotifyProwl,
|
||||||
|
}),
|
||||||
|
# Invalid APIKey
|
||||||
|
('prowl://%s' % ('a' * 24), {
|
||||||
|
'exception': TypeError,
|
||||||
|
}),
|
||||||
|
# APIKey
|
||||||
|
('prowl://%s' % ('a' * 40), {
|
||||||
|
'instance': plugins.NotifyProwl,
|
||||||
|
# don't include an image by default
|
||||||
|
'include_image': False,
|
||||||
|
}),
|
||||||
|
# APIKey + priority setting
|
||||||
|
('prowl://%s?priority=high' % ('a' * 40), {
|
||||||
|
'instance': plugins.NotifyProwl,
|
||||||
|
}),
|
||||||
|
# APIKey + invalid priority setting
|
||||||
|
('prowl://%s?priority=invalid' % ('a' * 40), {
|
||||||
|
'instance': plugins.NotifyProwl,
|
||||||
|
}),
|
||||||
|
# APIKey + priority setting (empty)
|
||||||
|
('prowl://%s?priority=' % ('a' * 40), {
|
||||||
|
'instance': plugins.NotifyProwl,
|
||||||
|
}),
|
||||||
|
# APIKey + Invalid Provider Key
|
||||||
|
('prowl://%s/%s' % ('a' * 40, 'b' * 24), {
|
||||||
|
'exception': TypeError,
|
||||||
|
}),
|
||||||
|
# APIKey + No Provider Key (empty)
|
||||||
|
('prowl://%s///' % ('a' * 40), {
|
||||||
|
'instance': plugins.NotifyProwl,
|
||||||
|
}),
|
||||||
|
# APIKey + Provider Key
|
||||||
|
('prowl://%s/%s' % ('a' * 40, 'b' * 40), {
|
||||||
|
'instance': plugins.NotifyProwl,
|
||||||
|
}),
|
||||||
|
# APIKey + with image
|
||||||
|
('prowl://%s' % ('a' * 40), {
|
||||||
|
'instance': plugins.NotifyProwl,
|
||||||
|
}),
|
||||||
|
# bad url
|
||||||
|
('prowl://:@/', {
|
||||||
|
'instance': None,
|
||||||
|
}),
|
||||||
|
('prowl://%s' % ('a' * 40), {
|
||||||
|
'instance': plugins.NotifyProwl,
|
||||||
|
# force a failure
|
||||||
|
'response': False,
|
||||||
|
'requests_response_code': requests.codes.internal_server_error,
|
||||||
|
}),
|
||||||
|
('prowl://%s' % ('a' * 40), {
|
||||||
|
'instance': plugins.NotifyProwl,
|
||||||
|
# throw a bizzare code forcing us to fail to look it up
|
||||||
|
'response': False,
|
||||||
|
'requests_response_code': 999,
|
||||||
|
}),
|
||||||
|
('prowl://%s' % ('a' * 40), {
|
||||||
|
'instance': plugins.NotifyProwl,
|
||||||
|
# Throws a series of connection and transfer exceptions when this flag
|
||||||
|
# is set and tests that we gracfully handle them
|
||||||
|
'test_requests_exceptions': True,
|
||||||
|
}),
|
||||||
|
|
||||||
|
##################################
|
||||||
|
# NotifyPushalot
|
||||||
|
##################################
|
||||||
|
('palot://', {
|
||||||
|
'instance': None,
|
||||||
|
}),
|
||||||
|
# AuthToken
|
||||||
|
('palot://%s' % ('a' * 32), {
|
||||||
|
'instance': plugins.NotifyPushalot,
|
||||||
|
}),
|
||||||
|
# AuthToken, no image
|
||||||
|
('palot://%s' % ('a' * 32), {
|
||||||
|
'instance': plugins.NotifyPushalot,
|
||||||
|
# don't include an image by default
|
||||||
|
'include_image': False,
|
||||||
|
}),
|
||||||
|
# Invalid AuthToken
|
||||||
|
('palot://%s' % ('a' * 24), {
|
||||||
|
'instance': None,
|
||||||
|
# Missing a channel
|
||||||
|
'exception': TypeError,
|
||||||
|
}),
|
||||||
|
# AuthToken + bad url
|
||||||
|
('palot://:@/', {
|
||||||
|
'instance': None,
|
||||||
|
}),
|
||||||
|
('palot://%s' % ('a' * 32), {
|
||||||
|
'instance': plugins.NotifyPushalot,
|
||||||
|
# force a failure
|
||||||
|
'response': False,
|
||||||
|
'requests_response_code': requests.codes.internal_server_error,
|
||||||
|
}),
|
||||||
|
('palot://%s' % ('a' * 32), {
|
||||||
|
'instance': plugins.NotifyPushalot,
|
||||||
|
# throw a bizzare code forcing us to fail to look it up
|
||||||
|
'response': False,
|
||||||
|
'requests_response_code': 999,
|
||||||
|
}),
|
||||||
|
('palot://%s' % ('a' * 32), {
|
||||||
|
'instance': plugins.NotifyPushalot,
|
||||||
|
# Throws a series of connection and transfer exceptions when this flag
|
||||||
|
# is set and tests that we gracfully handle them
|
||||||
|
'test_requests_exceptions': True,
|
||||||
|
}),
|
||||||
|
|
||||||
|
##################################
|
||||||
|
# NotifyPushBullet
|
||||||
|
##################################
|
||||||
|
('pbul://', {
|
||||||
|
'instance': None,
|
||||||
|
}),
|
||||||
|
# APIkey
|
||||||
|
('pbul://%s' % ('a' * 32), {
|
||||||
|
'instance': plugins.NotifyPushBullet,
|
||||||
|
}),
|
||||||
|
# APIKey + channel
|
||||||
|
('pbul://%s/#channel/' % ('a' * 32), {
|
||||||
|
'instance': plugins.NotifyPushBullet,
|
||||||
|
}),
|
||||||
|
# APIKey + 2 channels
|
||||||
|
('pbul://%s/#channel1/#channel2' % ('a' * 32), {
|
||||||
|
'instance': plugins.NotifyPushBullet,
|
||||||
|
}),
|
||||||
|
# APIKey + device
|
||||||
|
('pbul://%s/device/' % ('a' * 32), {
|
||||||
|
'instance': plugins.NotifyPushBullet,
|
||||||
|
}),
|
||||||
|
# APIKey + 2 devices
|
||||||
|
('pbul://%s/device1/device2/' % ('a' * 32), {
|
||||||
|
'instance': plugins.NotifyPushBullet,
|
||||||
|
}),
|
||||||
|
# APIKey + email
|
||||||
|
('pbul://%s/user@example.com/' % ('a' * 32), {
|
||||||
|
'instance': plugins.NotifyPushBullet,
|
||||||
|
}),
|
||||||
|
# APIKey + 2 emails
|
||||||
|
('pbul://%s/user@example.com/abc@def.com/' % ('a' * 32), {
|
||||||
|
'instance': plugins.NotifyPushBullet,
|
||||||
|
}),
|
||||||
|
# APIKey + Combo
|
||||||
|
('pbul://%s/device/#channel/user@example.com/' % ('a' * 32), {
|
||||||
|
'instance': plugins.NotifyPushBullet,
|
||||||
|
}),
|
||||||
|
# APIKey + bad url
|
||||||
|
('pbul://:@/', {
|
||||||
|
'instance': None,
|
||||||
|
}),
|
||||||
|
('pbul://%s' % ('a' * 32), {
|
||||||
|
'instance': plugins.NotifyPushBullet,
|
||||||
|
# force a failure
|
||||||
|
'response': False,
|
||||||
|
'requests_response_code': requests.codes.internal_server_error,
|
||||||
|
}),
|
||||||
|
('pbul://%s' % ('a' * 32), {
|
||||||
|
'instance': plugins.NotifyPushBullet,
|
||||||
|
# throw a bizzare code forcing us to fail to look it up
|
||||||
|
'response': False,
|
||||||
|
'requests_response_code': 999,
|
||||||
|
}),
|
||||||
|
('pbul://%s' % ('a' * 32), {
|
||||||
|
'instance': plugins.NotifyPushBullet,
|
||||||
|
# Throws a series of connection and transfer exceptions when this flag
|
||||||
|
# is set and tests that we gracfully handle them
|
||||||
|
'test_requests_exceptions': True,
|
||||||
|
}),
|
||||||
|
|
||||||
|
##################################
|
||||||
|
# NotifyPushover
|
||||||
|
##################################
|
||||||
|
('pover://', {
|
||||||
|
'instance': None,
|
||||||
|
}),
|
||||||
|
# APIkey; no user
|
||||||
|
('pover://%s' % ('a' * 30), {
|
||||||
|
'exception': TypeError,
|
||||||
|
}),
|
||||||
|
# APIkey; invalid user
|
||||||
|
('pover://%s@%s' % ('u' * 20, 'a' * 30), {
|
||||||
|
'exception': TypeError,
|
||||||
|
}),
|
||||||
|
# Invalid APIKey; valid User
|
||||||
|
('pover://%s@%s' % ('u' * 30, 'a' * 24), {
|
||||||
|
'exception': TypeError,
|
||||||
|
}),
|
||||||
|
# APIKey + Valid User
|
||||||
|
('pover://%s@%s' % ('u' * 30, 'a' * 30), {
|
||||||
|
'instance': plugins.NotifyPushover,
|
||||||
|
# don't include an image by default
|
||||||
|
'include_image': False,
|
||||||
|
}),
|
||||||
|
# APIKey + Valid User + 1 Device
|
||||||
|
('pover://%s@%s/DEVICE' % ('u' * 30, 'a' * 30), {
|
||||||
|
'instance': plugins.NotifyPushover,
|
||||||
|
}),
|
||||||
|
# APIKey + Valid User + 2 Devices
|
||||||
|
('pover://%s@%s/DEVICE1/DEVICE2/' % ('u' * 30, 'a' * 30), {
|
||||||
|
'instance': plugins.NotifyPushover,
|
||||||
|
}),
|
||||||
|
# APIKey + Valid User + invalid device
|
||||||
|
('pover://%s@%s/%s/' % ('u' * 30, 'a' * 30, 'd' * 30), {
|
||||||
|
'instance': plugins.NotifyPushover,
|
||||||
|
# Notify will return False since there is a bad device in our list
|
||||||
|
'response': False,
|
||||||
|
}),
|
||||||
|
# APIKey + Valid User + device + invalid device
|
||||||
|
('pover://%s@%s/DEVICE1/%s/' % ('u' * 30, 'a' * 30, 'd' * 30), {
|
||||||
|
'instance': plugins.NotifyPushover,
|
||||||
|
# Notify will return False since there is a bad device in our list
|
||||||
|
'response': False,
|
||||||
|
}),
|
||||||
|
# APIKey + priority setting
|
||||||
|
('pover://%s@%s?priority=high' % ('u' * 30, 'a' * 30), {
|
||||||
|
'instance': plugins.NotifyPushover,
|
||||||
|
}),
|
||||||
|
# APIKey + invalid priority setting
|
||||||
|
('pover://%s@%s?priority=invalid' % ('u' * 30, 'a' * 30), {
|
||||||
|
'instance': plugins.NotifyPushover,
|
||||||
|
}),
|
||||||
|
# APIKey + priority setting (empty)
|
||||||
|
('pover://%s@%s?priority=' % ('u' * 30, 'a' * 30), {
|
||||||
|
'instance': plugins.NotifyPushover,
|
||||||
|
}),
|
||||||
|
# bad url
|
||||||
|
('pover://:@/', {
|
||||||
|
'instance': None,
|
||||||
|
}),
|
||||||
|
('pover://%s@%s' % ('u' * 30, 'a' * 30), {
|
||||||
|
'instance': plugins.NotifyPushover,
|
||||||
|
# force a failure
|
||||||
|
'response': False,
|
||||||
|
'requests_response_code': requests.codes.internal_server_error,
|
||||||
|
}),
|
||||||
|
('pover://%s@%s' % ('u' * 30, 'a' * 30), {
|
||||||
|
'instance': plugins.NotifyPushover,
|
||||||
|
# throw a bizzare code forcing us to fail to look it up
|
||||||
|
'response': False,
|
||||||
|
'requests_response_code': 999,
|
||||||
|
}),
|
||||||
|
('pover://%s@%s' % ('u' * 30, 'a' * 30), {
|
||||||
|
'instance': plugins.NotifyPushover,
|
||||||
|
# Throws a series of connection and transfer exceptions when this flag
|
||||||
|
# is set and tests that we gracfully handle them
|
||||||
|
'test_requests_exceptions': True,
|
||||||
|
}),
|
||||||
|
|
||||||
##################################
|
##################################
|
||||||
# NotifySlack
|
# NotifySlack
|
||||||
##################################
|
##################################
|
||||||
@ -486,6 +774,173 @@ TEST_URLS = (
|
|||||||
'test_requests_exceptions': True,
|
'test_requests_exceptions': True,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
##################################
|
||||||
|
# NotifyTelegram
|
||||||
|
##################################
|
||||||
|
('tgram://', {
|
||||||
|
'instance': None,
|
||||||
|
}),
|
||||||
|
# Simple Message
|
||||||
|
('tgram://123456789:abcdefg_hijklmnop/lead2gold/', {
|
||||||
|
'instance': plugins.NotifyTelegram,
|
||||||
|
}),
|
||||||
|
# Simple Message (no images)
|
||||||
|
('tgram://123456789:abcdefg_hijklmnop/lead2gold/', {
|
||||||
|
'instance': plugins.NotifyTelegram,
|
||||||
|
# don't include an image by default
|
||||||
|
'include_image': False,
|
||||||
|
}),
|
||||||
|
# Simple Message with multiple chat names
|
||||||
|
('tgram://123456789:abcdefg_hijklmnop/id1/id2/', {
|
||||||
|
'instance': plugins.NotifyTelegram,
|
||||||
|
}),
|
||||||
|
# Simple Message with an invalid chat ID
|
||||||
|
('tgram://123456789:abcdefg_hijklmnop/%$/', {
|
||||||
|
'instance': plugins.NotifyTelegram,
|
||||||
|
# Notify will fail
|
||||||
|
'response': False,
|
||||||
|
}),
|
||||||
|
# Simple Message with multiple chat ids
|
||||||
|
('tgram://123456789:abcdefg_hijklmnop/id1/id2/23423/-30/', {
|
||||||
|
'instance': plugins.NotifyTelegram,
|
||||||
|
}),
|
||||||
|
# Simple Message with multiple chat ids (no images)
|
||||||
|
('tgram://123456789:abcdefg_hijklmnop/id1/id2/23423/-30/', {
|
||||||
|
'instance': plugins.NotifyTelegram,
|
||||||
|
# don't include an image by default
|
||||||
|
'include_image': False,
|
||||||
|
}),
|
||||||
|
# Support bot keyword prefix
|
||||||
|
('tgram://bottest@123456789:abcdefg_hijklmnop/lead2gold/', {
|
||||||
|
'instance': plugins.NotifyTelegram,
|
||||||
|
}),
|
||||||
|
# Testing valid format
|
||||||
|
('tgram://123456789:abcdefg_hijklmnop/lead2gold/?format=text', {
|
||||||
|
'instance': plugins.NotifyTelegram,
|
||||||
|
}),
|
||||||
|
# Testing valid format
|
||||||
|
('tgram://123456789:abcdefg_hijklmnop/lead2gold/?format=html', {
|
||||||
|
'instance': plugins.NotifyTelegram,
|
||||||
|
}),
|
||||||
|
# Testing invalid format (fall's back to text)
|
||||||
|
('tgram://123456789:abcdefg_hijklmnop/lead2gold/?format=invalid', {
|
||||||
|
'instance': plugins.NotifyTelegram,
|
||||||
|
}),
|
||||||
|
# Testing empty format (falls back to text)
|
||||||
|
('tgram://123456789:abcdefg_hijklmnop/lead2gold/?format=', {
|
||||||
|
'instance': plugins.NotifyTelegram,
|
||||||
|
}),
|
||||||
|
# Simple Message without image
|
||||||
|
('tgram://123456789:abcdefg_hijklmnop/lead2gold/', {
|
||||||
|
'instance': plugins.NotifyTelegram,
|
||||||
|
# don't include an image by default
|
||||||
|
'include_image': False,
|
||||||
|
}),
|
||||||
|
# Invalid Bot Token
|
||||||
|
('tgram://alpha:abcdefg_hijklmnop/lead2gold/', {
|
||||||
|
'instance': None,
|
||||||
|
}),
|
||||||
|
# AuthToken + bad url
|
||||||
|
('tgram://:@/', {
|
||||||
|
'instance': None,
|
||||||
|
}),
|
||||||
|
('tgram://123456789:abcdefg_hijklmnop/lead2gold/', {
|
||||||
|
'instance': plugins.NotifyTelegram,
|
||||||
|
# force a failure
|
||||||
|
'response': False,
|
||||||
|
'requests_response_code': requests.codes.internal_server_error,
|
||||||
|
}),
|
||||||
|
('tgram://123456789:abcdefg_hijklmnop/lead2gold/', {
|
||||||
|
'instance': plugins.NotifyTelegram,
|
||||||
|
# force a failure without an image specified
|
||||||
|
'include_image': False,
|
||||||
|
'response': False,
|
||||||
|
'requests_response_code': requests.codes.internal_server_error,
|
||||||
|
}),
|
||||||
|
('tgram://123456789:abcdefg_hijklmnop/id1/id2/', {
|
||||||
|
'instance': plugins.NotifyTelegram,
|
||||||
|
# force a failure with multiple chat_ids
|
||||||
|
'response': False,
|
||||||
|
'requests_response_code': requests.codes.internal_server_error,
|
||||||
|
}),
|
||||||
|
('tgram://123456789:abcdefg_hijklmnop/id1/id2/', {
|
||||||
|
'instance': plugins.NotifyTelegram,
|
||||||
|
# force a failure without an image specified
|
||||||
|
'include_image': False,
|
||||||
|
'response': False,
|
||||||
|
'requests_response_code': requests.codes.internal_server_error,
|
||||||
|
}),
|
||||||
|
('tgram://123456789:abcdefg_hijklmnop/lead2gold/', {
|
||||||
|
'instance': plugins.NotifyTelegram,
|
||||||
|
# throw a bizzare code forcing us to fail to look it up
|
||||||
|
'response': False,
|
||||||
|
'requests_response_code': 999,
|
||||||
|
}),
|
||||||
|
('tgram://123456789:abcdefg_hijklmnop/lead2gold/', {
|
||||||
|
'instance': plugins.NotifyTelegram,
|
||||||
|
# throw a bizzare code forcing us to fail to look it up without
|
||||||
|
# having an image included
|
||||||
|
'include_image': False,
|
||||||
|
'response': False,
|
||||||
|
'requests_response_code': 999,
|
||||||
|
}),
|
||||||
|
('tgram://123456789:abcdefg_hijklmnop/lead2gold/', {
|
||||||
|
'instance': plugins.NotifyTelegram,
|
||||||
|
# Throws a series of connection and transfer exceptions when this flag
|
||||||
|
# is set and tests that we gracfully handle them
|
||||||
|
'test_requests_exceptions': True,
|
||||||
|
}),
|
||||||
|
('tgram://123456789:abcdefg_hijklmnop/lead2gold/', {
|
||||||
|
'instance': plugins.NotifyTelegram,
|
||||||
|
# Throws a series of connection and transfer exceptions when this flag
|
||||||
|
# is set and tests that we gracfully handle them without images set
|
||||||
|
'include_image': False,
|
||||||
|
'test_requests_exceptions': True,
|
||||||
|
}),
|
||||||
|
|
||||||
|
##################################
|
||||||
|
# NotifyToasty (SuperToasty)
|
||||||
|
##################################
|
||||||
|
('toasty://', {
|
||||||
|
'instance': None,
|
||||||
|
}),
|
||||||
|
# No username specified but contains a device
|
||||||
|
('toasty://%s' % ('d' * 32), {
|
||||||
|
'exception': TypeError,
|
||||||
|
}),
|
||||||
|
# User + 1 device
|
||||||
|
('toasty://user@device', {
|
||||||
|
'instance': plugins.NotifyToasty,
|
||||||
|
}),
|
||||||
|
# User + 3 devices
|
||||||
|
('toasty://user@device0/device1/device2/', {
|
||||||
|
'instance': plugins.NotifyToasty,
|
||||||
|
# don't include an image by default
|
||||||
|
'include_image': False,
|
||||||
|
}),
|
||||||
|
# bad url
|
||||||
|
('toasty://:@/', {
|
||||||
|
'instance': None,
|
||||||
|
}),
|
||||||
|
('toasty://user@device', {
|
||||||
|
'instance': plugins.NotifyToasty,
|
||||||
|
# force a failure
|
||||||
|
'response': False,
|
||||||
|
'requests_response_code': requests.codes.internal_server_error,
|
||||||
|
}),
|
||||||
|
('toasty://user@device', {
|
||||||
|
'instance': plugins.NotifyToasty,
|
||||||
|
# throw a bizzare code forcing us to fail to look it up
|
||||||
|
'response': False,
|
||||||
|
'requests_response_code': 999,
|
||||||
|
}),
|
||||||
|
('toasty://user@device', {
|
||||||
|
'instance': plugins.NotifyToasty,
|
||||||
|
# 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
|
# NotifyKODI
|
||||||
##################################
|
##################################
|
||||||
@ -696,6 +1151,9 @@ def test_rest_plugins(mock_post, mock_get):
|
|||||||
|
|
||||||
assert(isinstance(obj, instance))
|
assert(isinstance(obj, instance))
|
||||||
|
|
||||||
|
# Disable throttling to speed up unit tests
|
||||||
|
obj.throttle_attempt = 0
|
||||||
|
|
||||||
if self:
|
if self:
|
||||||
# Iterate over our expected entries inside of our object
|
# Iterate over our expected entries inside of our object
|
||||||
for key, val in self.items():
|
for key, val in self.items():
|
||||||
@ -711,9 +1169,9 @@ def test_rest_plugins(mock_post, mock_get):
|
|||||||
notify_type=notify_type) == response
|
notify_type=notify_type) == response
|
||||||
|
|
||||||
else:
|
else:
|
||||||
for exception in test_requests_exceptions:
|
for _exception in test_requests_exceptions:
|
||||||
mock_post.side_effect = exception
|
mock_post.side_effect = _exception
|
||||||
mock_get.side_effect = exception
|
mock_get.side_effect = _exception
|
||||||
try:
|
try:
|
||||||
assert obj.notify(
|
assert obj.notify(
|
||||||
title='test', body='body',
|
title='test', body='body',
|
||||||
@ -805,6 +1263,10 @@ def test_notify_boxcar_plugin(mock_post, mock_get):
|
|||||||
|
|
||||||
# Test notifications without a body or a title
|
# Test notifications without a body or a title
|
||||||
p = plugins.NotifyBoxcar(access=access, secret=secret, recipients=None)
|
p = plugins.NotifyBoxcar(access=access, secret=secret, recipients=None)
|
||||||
|
|
||||||
|
# Disable throttling to speed up unit tests
|
||||||
|
p.throttle_attempt = 0
|
||||||
|
|
||||||
p.notify(body=None, title=None, notify_type=NotifyType.INFO) is True
|
p.notify(body=None, title=None, notify_type=NotifyType.INFO) is True
|
||||||
|
|
||||||
|
|
||||||
@ -835,6 +1297,9 @@ def test_notify_join_plugin(mock_post, mock_get):
|
|||||||
mock_post.return_value.status_code = requests.codes.created
|
mock_post.return_value.status_code = requests.codes.created
|
||||||
mock_get.return_value.status_code = requests.codes.created
|
mock_get.return_value.status_code = requests.codes.created
|
||||||
|
|
||||||
|
# Disable throttling to speed up unit tests
|
||||||
|
p.throttle_attempt = 0
|
||||||
|
|
||||||
# Test notifications without a body or a title; nothing to send
|
# Test notifications without a body or a title; nothing to send
|
||||||
# so we return False
|
# so we return False
|
||||||
p.notify(body=None, title=None, notify_type=NotifyType.INFO) is False
|
p.notify(body=None, title=None, notify_type=NotifyType.INFO) is False
|
||||||
@ -859,6 +1324,8 @@ def test_notify_slack_plugin(mock_post, mock_get):
|
|||||||
obj = plugins.NotifySlack(
|
obj = plugins.NotifySlack(
|
||||||
token_a=token_a, token_b=token_b, token_c=token_c, channels=channels)
|
token_a=token_a, token_b=token_b, token_c=token_c, channels=channels)
|
||||||
assert(len(obj.channels) == 4)
|
assert(len(obj.channels) == 4)
|
||||||
|
|
||||||
|
# Prepare Mock
|
||||||
mock_get.return_value = requests.Request()
|
mock_get.return_value = requests.Request()
|
||||||
mock_post.return_value = requests.Request()
|
mock_post.return_value = requests.Request()
|
||||||
mock_post.return_value.status_code = requests.codes.ok
|
mock_post.return_value.status_code = requests.codes.ok
|
||||||
@ -880,6 +1347,271 @@ def test_notify_slack_plugin(mock_post, mock_get):
|
|||||||
token_a=token_a, token_b=token_b, token_c=token_c, channels=channels,
|
token_a=token_a, token_b=token_b, token_c=token_c, channels=channels,
|
||||||
include_image=True)
|
include_image=True)
|
||||||
|
|
||||||
|
# Disable throttling to speed up unit tests
|
||||||
|
obj.throttle_attempt = 0
|
||||||
|
|
||||||
# This call includes an image with it's payload:
|
# This call includes an image with it's payload:
|
||||||
assert obj.notify(title='title', body='body',
|
assert obj.notify(title='title', body='body',
|
||||||
notify_type=NotifyType.INFO) is True
|
notify_type=NotifyType.INFO) is True
|
||||||
|
|
||||||
|
|
||||||
|
@mock.patch('requests.get')
|
||||||
|
@mock.patch('requests.post')
|
||||||
|
def test_notify_pushbullet_plugin(mock_post, mock_get):
|
||||||
|
"""
|
||||||
|
API: NotifyPushBullet() Extra Checks
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Initialize some generic (but valid) tokens
|
||||||
|
accesstoken = 'a' * 32
|
||||||
|
|
||||||
|
# Support strings
|
||||||
|
recipients = '#chan1,#chan2,device,user@example.com,,,'
|
||||||
|
|
||||||
|
# Prepare Mock
|
||||||
|
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
|
||||||
|
|
||||||
|
obj = plugins.NotifyPushBullet(
|
||||||
|
accesstoken=accesstoken, recipients=recipients)
|
||||||
|
assert(isinstance(obj, plugins.NotifyPushBullet))
|
||||||
|
assert(len(obj.recipients) == 4)
|
||||||
|
|
||||||
|
obj = plugins.NotifyPushBullet(accesstoken=accesstoken)
|
||||||
|
assert(isinstance(obj, plugins.NotifyPushBullet))
|
||||||
|
# Default is to send to all devices, so there will be a
|
||||||
|
# recipient here
|
||||||
|
assert(len(obj.recipients) == 1)
|
||||||
|
|
||||||
|
obj = plugins.NotifyPushBullet(accesstoken=accesstoken, recipients=set())
|
||||||
|
assert(isinstance(obj, plugins.NotifyPushBullet))
|
||||||
|
# Default is to send to all devices, so there will be a
|
||||||
|
# recipient here
|
||||||
|
assert(len(obj.recipients) == 1)
|
||||||
|
|
||||||
|
# Support the handling of an empty and invalid URL strings
|
||||||
|
assert(plugins.NotifyPushBullet.parse_url(None) is None)
|
||||||
|
assert(plugins.NotifyPushBullet.parse_url('') is None)
|
||||||
|
assert(plugins.NotifyPushBullet.parse_url(42) is None)
|
||||||
|
|
||||||
|
|
||||||
|
@mock.patch('requests.get')
|
||||||
|
@mock.patch('requests.post')
|
||||||
|
def test_notify_pushover_plugin(mock_post, mock_get):
|
||||||
|
"""
|
||||||
|
API: NotifyPushover() Extra Checks
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Initialize some generic (but valid) tokens
|
||||||
|
token = 'a' * 30
|
||||||
|
user = 'u' * 30
|
||||||
|
|
||||||
|
invalid_device = 'd' * 35
|
||||||
|
|
||||||
|
# Support strings
|
||||||
|
devices = 'device1,device2,,,,%s' % invalid_device
|
||||||
|
|
||||||
|
# Prepare Mock
|
||||||
|
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
|
||||||
|
|
||||||
|
try:
|
||||||
|
obj = plugins.NotifyPushover(user=user, token=None)
|
||||||
|
# No token specified
|
||||||
|
assert(False)
|
||||||
|
|
||||||
|
except TypeError:
|
||||||
|
# Exception should be thrown about the fact no token was specified
|
||||||
|
assert(True)
|
||||||
|
|
||||||
|
obj = plugins.NotifyPushover(user=user, token=token, devices=devices)
|
||||||
|
assert(isinstance(obj, plugins.NotifyPushover))
|
||||||
|
assert(len(obj.devices) == 3)
|
||||||
|
|
||||||
|
# Disable throttling to speed up unit tests
|
||||||
|
obj.throttle_attempt = 0
|
||||||
|
|
||||||
|
# This call fails because there is 1 invalid device
|
||||||
|
assert obj.notify(title='title', body='body',
|
||||||
|
notify_type=NotifyType.INFO) is False
|
||||||
|
|
||||||
|
obj = plugins.NotifyPushover(user=user, token=token)
|
||||||
|
assert(isinstance(obj, plugins.NotifyPushover))
|
||||||
|
# Default is to send to all devices, so there will be a
|
||||||
|
# device defined here
|
||||||
|
assert(len(obj.devices) == 1)
|
||||||
|
|
||||||
|
# Disable throttling to speed up unit tests
|
||||||
|
obj.throttle_attempt = 0
|
||||||
|
|
||||||
|
# This call succeeds because all of the devices are valid
|
||||||
|
assert obj.notify(title='title', body='body',
|
||||||
|
notify_type=NotifyType.INFO) is True
|
||||||
|
|
||||||
|
obj = plugins.NotifyPushover(user=user, token=token, devices=set())
|
||||||
|
assert(isinstance(obj, plugins.NotifyPushover))
|
||||||
|
# Default is to send to all devices, so there will be a
|
||||||
|
# device defined here
|
||||||
|
assert(len(obj.devices) == 1)
|
||||||
|
|
||||||
|
# Support the handling of an empty and invalid URL strings
|
||||||
|
assert(plugins.NotifyPushover.parse_url(None) is None)
|
||||||
|
assert(plugins.NotifyPushover.parse_url('') is None)
|
||||||
|
assert(plugins.NotifyPushover.parse_url(42) is None)
|
||||||
|
|
||||||
|
|
||||||
|
@mock.patch('requests.get')
|
||||||
|
@mock.patch('requests.post')
|
||||||
|
def test_notify_toasty_plugin(mock_post, mock_get):
|
||||||
|
"""
|
||||||
|
API: NotifyToasty() Extra Checks
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Support strings
|
||||||
|
devices = 'device1,device2,,,,'
|
||||||
|
|
||||||
|
# User
|
||||||
|
user = 'l2g'
|
||||||
|
|
||||||
|
# Prepare Mock
|
||||||
|
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
|
||||||
|
|
||||||
|
try:
|
||||||
|
obj = plugins.NotifyToasty(user=user, devices=None)
|
||||||
|
# No devices specified
|
||||||
|
assert(False)
|
||||||
|
|
||||||
|
except TypeError:
|
||||||
|
# Exception should be thrown about the fact no token was specified
|
||||||
|
assert(True)
|
||||||
|
|
||||||
|
try:
|
||||||
|
obj = plugins.NotifyToasty(user=user, devices=set())
|
||||||
|
# No devices specified
|
||||||
|
assert(False)
|
||||||
|
|
||||||
|
except TypeError:
|
||||||
|
# Exception should be thrown about the fact no token was specified
|
||||||
|
assert(True)
|
||||||
|
|
||||||
|
obj = plugins.NotifyToasty(user=user, devices=devices)
|
||||||
|
assert(isinstance(obj, plugins.NotifyToasty))
|
||||||
|
assert(len(obj.devices) == 2)
|
||||||
|
|
||||||
|
# Support the handling of an empty and invalid URL strings
|
||||||
|
assert(plugins.NotifyToasty.parse_url(None) is None)
|
||||||
|
assert(plugins.NotifyToasty.parse_url('') is None)
|
||||||
|
assert(plugins.NotifyToasty.parse_url(42) is None)
|
||||||
|
|
||||||
|
|
||||||
|
@mock.patch('requests.get')
|
||||||
|
@mock.patch('requests.post')
|
||||||
|
def test_notify_telegram_plugin(mock_post, mock_get):
|
||||||
|
"""
|
||||||
|
API: NotifyTelegram() Extra Checks
|
||||||
|
|
||||||
|
"""
|
||||||
|
# Bot Token
|
||||||
|
bot_token = '123456789:abcdefg_hijklmnop'
|
||||||
|
invalid_bot_token = 'abcd:123'
|
||||||
|
|
||||||
|
# Chat ID
|
||||||
|
chat_ids = 'l2g, lead2gold'
|
||||||
|
|
||||||
|
# Prepare Mock
|
||||||
|
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
|
||||||
|
|
||||||
|
try:
|
||||||
|
obj = plugins.NotifyTelegram(bot_token=None, chat_ids=chat_ids)
|
||||||
|
# invalid bot token (None)
|
||||||
|
assert(False)
|
||||||
|
|
||||||
|
except TypeError:
|
||||||
|
# Exception should be thrown about the fact no token was specified
|
||||||
|
assert(True)
|
||||||
|
|
||||||
|
try:
|
||||||
|
obj = plugins.NotifyTelegram(
|
||||||
|
bot_token=invalid_bot_token, chat_ids=chat_ids)
|
||||||
|
# invalid bot token
|
||||||
|
assert(False)
|
||||||
|
|
||||||
|
except TypeError:
|
||||||
|
# Exception should be thrown about the fact an invalid token was
|
||||||
|
# specified
|
||||||
|
assert(True)
|
||||||
|
|
||||||
|
try:
|
||||||
|
obj = plugins.NotifyTelegram(bot_token=bot_token, chat_ids=None)
|
||||||
|
# No chat_ids specified
|
||||||
|
assert(False)
|
||||||
|
|
||||||
|
except TypeError:
|
||||||
|
# Exception should be thrown about the fact no token was specified
|
||||||
|
assert(True)
|
||||||
|
|
||||||
|
try:
|
||||||
|
obj = plugins.NotifyTelegram(bot_token=bot_token, chat_ids=set())
|
||||||
|
# No chat_ids specified
|
||||||
|
assert(False)
|
||||||
|
|
||||||
|
except TypeError:
|
||||||
|
# Exception should be thrown about the fact no token was specified
|
||||||
|
assert(True)
|
||||||
|
|
||||||
|
obj = plugins.NotifyTelegram(bot_token=bot_token, chat_ids=chat_ids)
|
||||||
|
assert(isinstance(obj, plugins.NotifyTelegram))
|
||||||
|
assert(len(obj.chat_ids) == 2)
|
||||||
|
|
||||||
|
# Support the handling of an empty and invalid URL strings
|
||||||
|
assert(plugins.NotifyTelegram.parse_url(None) is None)
|
||||||
|
assert(plugins.NotifyTelegram.parse_url('') is None)
|
||||||
|
assert(plugins.NotifyTelegram.parse_url(42) is None)
|
||||||
|
|
||||||
|
# Prepare Mock to fail
|
||||||
|
response = mock.Mock()
|
||||||
|
response.status_code = requests.codes.internal_server_error
|
||||||
|
|
||||||
|
# a error response
|
||||||
|
response.text = dumps({
|
||||||
|
'description': 'test',
|
||||||
|
})
|
||||||
|
mock_get.return_value = response
|
||||||
|
mock_post.return_value = response
|
||||||
|
|
||||||
|
# No image asset
|
||||||
|
nimg_obj = plugins.NotifyTelegram(bot_token=bot_token, chat_ids=chat_ids)
|
||||||
|
nimg_obj.asset = AppriseAsset(image_path_mask=False, image_url_mask=False)
|
||||||
|
|
||||||
|
# Disable throttling to speed up unit tests
|
||||||
|
nimg_obj.throttle_attempt = 0
|
||||||
|
obj.throttle_attempt = 0
|
||||||
|
|
||||||
|
# This tests erroneous messages involving multiple chat ids
|
||||||
|
assert obj.notify(
|
||||||
|
title='title', body='body', notify_type=NotifyType.INFO) is False
|
||||||
|
assert nimg_obj.notify(
|
||||||
|
title='title', body='body', notify_type=NotifyType.INFO) is False
|
||||||
|
|
||||||
|
# This tests erroneous messages involving a single chat id
|
||||||
|
obj = plugins.NotifyTelegram(bot_token=bot_token, chat_ids='l2g')
|
||||||
|
nimg_obj = plugins.NotifyTelegram(bot_token=bot_token, chat_ids='l2g')
|
||||||
|
nimg_obj.asset = AppriseAsset(image_path_mask=False, image_url_mask=False)
|
||||||
|
|
||||||
|
assert obj.notify(
|
||||||
|
title='title', body='body', notify_type=NotifyType.INFO) is False
|
||||||
|
assert nimg_obj.notify(
|
||||||
|
title='title', body='body', notify_type=NotifyType.INFO) is False
|
||||||
|
Loading…
Reference in New Issue
Block a user