Merge pull request #74 from caronc/api-improvements

API Improvements, Overflow support, Throttling Refactored, IFTTT Refactored, and +/- Params Support
pull/76/head
Chris Caron 2019-02-20 12:41:03 -05:00 committed by GitHub
commit c6cf06e6f8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
47 changed files with 2632 additions and 600 deletions

View File

@ -26,7 +26,7 @@ matrix:
install: install:
- pip install . - pip install .
- pip install codecov tox - pip install codecov
- pip install -r dev-requirements.txt - pip install -r dev-requirements.txt
- pip install -r requirements.txt - pip install -r requirements.txt
- if [[ $TRAVIS_PYTHON_VERSION != 'pypy'* ]]; then travis_retry pip install dbus-python; fi - if [[ $TRAVIS_PYTHON_VERSION != 'pypy'* ]]; then travis_retry pip install dbus-python; fi

View File

@ -39,7 +39,7 @@ The table below identifies the services this tool supports and some example serv
| [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
| [Gnome](https://github.com/caronc/apprise/wiki/Notify_gnome) | gnome:// | n/a | gnome:// | [Gnome](https://github.com/caronc/apprise/wiki/Notify_gnome) | gnome:// | n/a | gnome://
| [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
| [IFTTT](https://github.com/caronc/apprise/wiki/Notify_ifttt) | ifttt:// | (TCP) 443 | ifttt://webhooksID/EventToTrigger<br />ifttt://webhooksID/EventToTrigger/Value1/Value2/Value3<br />ifttt://webhooksID/EventToTrigger/?Value3=NewEntry&Value2=AnotherValue | [IFTTT](https://github.com/caronc/apprise/wiki/Notify_ifttt) | ifttt:// | (TCP) 443 | ifttt://webhooksID/Event<br />ifttt://webhooksID/Event1/Event2/EventN<br/>ifttt://webhooksID/Event1/?+Key=Value<br/>ifttt://webhooksID/Event1/?-Key=value1
| [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/
| [KODI](https://github.com/caronc/apprise/wiki/Notify_kodi) | kodi:// or kodis:// | (TCP) 8080 or 443 | kodi://hostname<br />kodi://user@hostname<br />kodi://user:password@hostname:port | [KODI](https://github.com/caronc/apprise/wiki/Notify_kodi) | kodi:// or kodis:// | (TCP) 8080 or 443 | kodi://hostname<br />kodi://user@hostname<br />kodi://user:password@hostname:port
| [Matrix](https://github.com/caronc/apprise/wiki/Notify_matrix) | matrix:// or matrixs:// | (TCP) 80 or 443 | matrix://token<br />matrix://user@token<br />matrixs://token?mode=slack<br />matrixs://user@token | [Matrix](https://github.com/caronc/apprise/wiki/Notify_matrix) | matrix:// or matrixs:// | (TCP) 80 or 443 | matrix://token<br />matrix://user@token<br />matrixs://token?mode=slack<br />matrixs://user@token
@ -110,8 +110,8 @@ apobj.add('pbul://o.gn5kj6nfhv736I7jC3cj3QLRiyhgl98b')
# Then notify these services any time you desire. The below would # Then notify these services any time you desire. The below would
# notify all of the services loaded into our Apprise object. # notify all of the services loaded into our Apprise object.
apobj.notify( apobj.notify(
title='my notification title',
body='what a great notification service!', body='what a great notification service!',
title='my notification title',
) )
``` ```

View File

@ -171,7 +171,7 @@ class Apprise(object):
# URL information # URL information
plugin = SCHEMA_MAP[results['schema']](**results) plugin = SCHEMA_MAP[results['schema']](**results)
except: except Exception:
# the arguments are invalid or can not be used. # the arguments are invalid or can not be used.
logger.error('Could not load URL: %s' % url) logger.error('Could not load URL: %s' % url)
return None return None
@ -238,7 +238,7 @@ class Apprise(object):
""" """
self.servers[:] = [] self.servers[:] = []
def notify(self, title, body, notify_type=NotifyType.INFO, def notify(self, body, title='', notify_type=NotifyType.INFO,
body_format=None, tag=None): body_format=None, tag=None):
""" """
Send a notification to all of the plugins previously loaded. Send a notification to all of the plugins previously loaded.
@ -366,8 +366,8 @@ class Apprise(object):
try: try:
# Send notification # Send notification
if not server.notify( if not server.notify(
title=title,
body=conversion_map[server.notify_format], body=conversion_map[server.notify_format],
title=title,
notify_type=notify_type): notify_type=notify_type):
# Toggle our return status flag # Toggle our return status flag
@ -375,7 +375,6 @@ class Apprise(object):
except TypeError: except TypeError:
# These our our internally thrown notifications # These our our internally thrown notifications
# TODO: Change this to a custom one such as AppriseNotifyError
status = False status = False
except Exception: except Exception:
@ -432,6 +431,33 @@ class Apprise(object):
return response return response
def urls(self):
"""
Returns all of the loaded URLs defined in this apprise object.
"""
return [x.url() for x in self.servers]
def pop(self, index):
"""
Removes an indexed Notification Service from the stack and
returns it.
"""
# Remove our entry
return self.servers.pop(index)
def __getitem__(self, index):
"""
Returns the indexed server entry of a loaded notification server
"""
return self.servers[index]
def __iter__(self):
"""
Returns an iterator to our server list
"""
return iter(self.servers)
def __len__(self): def __len__(self):
""" """
Returns the number of servers loaded Returns the number of servers loaded

View File

@ -37,6 +37,8 @@ from .common import NotifyImageSize
from .common import NOTIFY_IMAGE_SIZES from .common import NOTIFY_IMAGE_SIZES
from .common import NotifyFormat from .common import NotifyFormat
from .common import NOTIFY_FORMATS from .common import NOTIFY_FORMATS
from .common import OverflowMode
from .common import OVERFLOW_MODES
from .plugins.NotifyBase import NotifyBase from .plugins.NotifyBase import NotifyBase
from .Apprise import Apprise from .Apprise import Apprise
@ -52,6 +54,6 @@ __all__ = [
'Apprise', 'AppriseAsset', 'NotifyBase', 'Apprise', 'AppriseAsset', 'NotifyBase',
# Reference # Reference
'NotifyType', 'NotifyImageSize', 'NotifyFormat', 'NOTIFY_TYPES', 'NotifyType', 'NotifyImageSize', 'NotifyFormat', 'OverflowMode',
'NOTIFY_IMAGE_SIZES', 'NOTIFY_FORMATS', 'NOTIFY_TYPES', 'NOTIFY_IMAGE_SIZES', 'NOTIFY_FORMATS', 'OVERFLOW_MODES',
] ]

View File

@ -77,3 +77,31 @@ NOTIFY_FORMATS = (
NotifyFormat.HTML, NotifyFormat.HTML,
NotifyFormat.MARKDOWN, NotifyFormat.MARKDOWN,
) )
class OverflowMode(object):
"""
A list of pre-defined modes of how to handle the text when it exceeds the
defined maximum message size.
"""
# Send the data as is; untouched. Let the upstream server decide how the
# content is handled. Some upstream services might gracefully handle this
# with expected intentions; others might not.
UPSTREAM = 'upstream'
# Always truncate the text when it exceeds the maximum message size and
# send it anyway
TRUNCATE = 'truncate'
# Split the message into multiple smaller messages that fit within the
# limits of what is expected. The smaller messages are sent
SPLIT = 'split'
# Define our modes so we can verify if we need to
OVERFLOW_MODES = (
OverflowMode.UPSTREAM,
OverflowMode.TRUNCATE,
OverflowMode.SPLIT,
)

View File

@ -26,6 +26,8 @@
import re import re
import logging import logging
from time import sleep from time import sleep
from datetime import datetime
try: try:
# Python 2.7 # Python 2.7
from urllib import unquote as _unquote from urllib import unquote as _unquote
@ -42,9 +44,12 @@ from ..utils import parse_url
from ..utils import parse_bool from ..utils import parse_bool
from ..utils import parse_list from ..utils import parse_list
from ..utils import is_hostname from ..utils import is_hostname
from ..common import NotifyType
from ..common import NOTIFY_TYPES from ..common import NOTIFY_TYPES
from ..common import NotifyFormat from ..common import NotifyFormat
from ..common import NOTIFY_FORMATS from ..common import NOTIFY_FORMATS
from ..common import OverflowMode
from ..common import OVERFLOW_MODES
from ..AppriseAsset import AppriseAsset from ..AppriseAsset import AppriseAsset
@ -52,13 +57,6 @@ from ..AppriseAsset import AppriseAsset
from xml.sax.saxutils import escape as sax_escape from xml.sax.saxutils import escape as sax_escape
def _escape(text):
"""
saxutil escape tool
"""
return sax_escape(text, {"'": "&apos;", "\"": "&quot;"})
HTTP_ERROR_MAP = { HTTP_ERROR_MAP = {
400: 'Bad Request - Unsupported Parameters.', 400: 'Bad Request - Unsupported Parameters.',
401: 'Verification Failed.', 401: 'Verification Failed.',
@ -113,21 +111,33 @@ class NotifyBase(object):
setup_url = None setup_url = None
# Most Servers do not like more then 1 request per 5 seconds, so 5.5 gives # Most Servers do not like more then 1 request per 5 seconds, so 5.5 gives
# us a safe play range... # us a safe play range.
throttle_attempt = 5.5 request_rate_per_sec = 5.5
# Allows the user to specify the NotifyImageSize object # Allows the user to specify the NotifyImageSize object
image_size = None image_size = None
# The maximum allowable characters allowed in the body per message # The maximum allowable characters allowed in the body per message
body_maxlen = 32768 # We set it to what would virtually be an infinite value really
# 2^63 - 1 = 9223372036854775807
body_maxlen = 9223372036854775807
# Defines the maximum allowable characters in the title # Defines the maximum allowable characters in the title; set this to zero
# if a title can't be used. Titles that are not used but are defined are
# automatically placed into the body
title_maxlen = 250 title_maxlen = 250
# Set the maximum line count; if this is set to anything larger then zero
# the message (prior to it being sent) will be truncated to this number
# of lines. Setting this to zero disables this feature.
body_max_line_count = 0
# Default Notify Format # Default Notify Format
notify_format = NotifyFormat.TEXT notify_format = NotifyFormat.TEXT
# Default Overflow Mode
overflow_mode = OverflowMode.UPSTREAM
# Maintain a set of tags to associate with this specific notification # Maintain a set of tags to associate with this specific notification
tags = set() tags = set()
@ -162,7 +172,6 @@ class NotifyBase(object):
self.user = kwargs.get('user') self.user = kwargs.get('user')
self.password = kwargs.get('password') self.password = kwargs.get('password')
self.headers = kwargs.get('headers')
if 'format' in kwargs: if 'format' in kwargs:
# Store the specified format if specified # Store the specified format if specified
@ -177,25 +186,64 @@ class NotifyBase(object):
# Provide override # Provide override
self.notify_format = notify_format self.notify_format = notify_format
if 'overflow' in kwargs:
# Store the specified format if specified
overflow = kwargs.get('overflow', '')
if overflow.lower() not in OVERFLOW_MODES:
self.logger.error(
'Invalid overflow method %s' % overflow,
)
raise TypeError(
'Invalid overflow method %s' % overflow,
)
# Provide override
self.overflow_mode = overflow
if 'tag' in kwargs: if 'tag' in kwargs:
# We want to associate some tags with our notification service. # We want to associate some tags with our notification service.
# the code below gets the 'tag' argument if defined, otherwise # the code below gets the 'tag' argument if defined, otherwise
# it just falls back to whatever was already defined globally # it just falls back to whatever was already defined globally
self.tags = set(parse_list(kwargs.get('tag', self.tags))) self.tags = set(parse_list(kwargs.get('tag', self.tags)))
def throttle(self, throttle_time=None): # Tracks the time any i/o was made to the remote server. This value
# is automatically set and controlled through the throttle() call.
self._last_io_datetime = None
def throttle(self, last_io=None):
""" """
A common throttle control A common throttle control
""" """
self.logger.debug('Throttling...')
throttle_time = throttle_time \ if last_io is not None:
if throttle_time is not None else self.throttle_attempt # Assume specified last_io
self._last_io_datetime = last_io
# Perform throttle # Get ourselves a reference time of 'now'
if throttle_time > 0: reference = datetime.now()
sleep(throttle_time)
if self._last_io_datetime is None:
# Set time to 'now' and no need to throttle
self._last_io_datetime = reference
return
if self.request_rate_per_sec <= 0.0:
# We're done if there is no throttle limit set
return
# If we reach here, we need to do additional logic.
# If the difference between the reference time and 'now' is less than
# the defined request_rate_per_sec then we need to throttle for the
# remaining balance of this time.
elapsed = (reference - self._last_io_datetime).total_seconds()
if elapsed < self.request_rate_per_sec:
self.logger.debug('Throttling for {}s...'.format(
self.request_rate_per_sec - elapsed))
sleep(self.request_rate_per_sec - elapsed)
# Update our timestamp before we leave
self._last_io_datetime = reference
return return
def image_url(self, notify_type, logo=False, extension=None): def image_url(self, notify_type, logo=False, extension=None):
@ -260,6 +308,117 @@ class NotifyBase(object):
color_type=color_type, color_type=color_type,
) )
def notify(self, body, title=None, notify_type=NotifyType.INFO,
overflow=None, **kwargs):
"""
Performs notification
"""
# Handle situations where the title is None
title = '' if not title else title
# Apply our overflow (if defined)
for chunk in self._apply_overflow(body=body, title=title,
overflow=overflow):
# Send notification
if not self.send(body=chunk['body'], title=chunk['title'],
notify_type=notify_type):
# Toggle our return status flag
return False
return True
def _apply_overflow(self, body, title=None, overflow=None):
"""
Takes the message body and title as input. This function then
applies any defined overflow restrictions associated with the
notification service and may alter the message if/as required.
The function will always return a list object in the following
structure:
[
{
title: 'the title goes here',
body: 'the message body goes here',
},
{
title: 'the title goes here',
body: 'the message body goes here',
},
]
"""
response = list()
# tidy
title = '' if not title else title.strip()
body = '' if not body else body.rstrip()
if overflow is None:
# default
overflow = self.overflow_mode
if self.title_maxlen <= 0:
# Content is appended to body
body = '{}\r\n{}'.format(title, body)
title = ''
# Enforce the line count first always
if self.body_max_line_count > 0:
# Limit results to just the first 2 line otherwise
# there is just to much content to display
body = re.split(r'\r*\n', body)
body = '\r\n'.join(body[0:self.body_max_line_count])
if overflow == OverflowMode.UPSTREAM:
# Nothing more to do
response.append({'body': body, 'title': title})
return response
elif len(title) > self.title_maxlen:
# Truncate our Title
title = title[:self.title_maxlen]
if self.body_maxlen > 0 and len(body) <= self.body_maxlen:
response.append({'body': body, 'title': title})
return response
if overflow == OverflowMode.TRUNCATE:
# Truncate our body and return
response.append({
'body': body[:self.body_maxlen],
'title': title,
})
# For truncate mode, we're done now
return response
# If we reach here, then we are in SPLIT mode.
# For here, we want to split the message as many times as we have to
# in order to fit it within the designated limits.
response = [{
'body': body[i: i + self.body_maxlen],
'title': title} for i in range(0, len(body), self.body_maxlen)]
return response
def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs):
"""
Should preform the actual notification itself.
"""
raise NotImplementedError("send() is implimented by the child class.")
def url(self):
"""
Assembles the URL associated with the notification based on the
arguments provied.
"""
raise NotImplementedError("url() is implimented by the child class.")
def __contains__(self, tags): def __contains__(self, tags):
""" """
Returns true if the tag specified is associated with this notification. Returns true if the tag specified is associated with this notification.
@ -290,12 +449,21 @@ class NotifyBase(object):
""" """
Takes html text as input and escapes it so that it won't Takes html text as input and escapes it so that it won't
conflict with any xml/html wrapping characters. conflict with any xml/html wrapping characters.
Args:
html (str): The HTML code to escape
convert_new_lines (:obj:`bool`, optional): escape new lines (\n)
whitespace (:obj:`bool`, optional): escape whitespace
Returns:
str: The escaped html
""" """
if not html: if not html:
# nothing more to do; return object as is # nothing more to do; return object as is
return html return html
escaped = _escape(html) # Escape HTML
escaped = sax_escape(html, {"'": "&apos;", "\"": "&quot;"})
if whitespace: if whitespace:
# Tidy up whitespace too # Tidy up whitespace too
@ -311,8 +479,25 @@ class NotifyBase(object):
@staticmethod @staticmethod
def unquote(content, encoding='utf-8', errors='replace'): def unquote(content, encoding='utf-8', errors='replace'):
""" """
common unquote function Replace %xx escapes by their single-character equivalent. The optional
encoding and errors parameters specify how to decode percent-encoded
sequences.
Wrapper to Python's unquote while remaining compatible with both
Python 2 & 3 since the reference to this function changed between
versions.
Note: errors set to 'replace' means that invalid sequences are
replaced by a placeholder character.
Args:
content (str): The quoted URI string you wish to unquote
encoding (:obj:`str`, optional): encoding type
errors (:obj:`str`, errors): how to handle invalid character found
in encoded string (defined by encoding)
Returns:
str: The unquoted URI string
""" """
if not content: if not content:
return '' return ''
@ -327,9 +512,25 @@ class NotifyBase(object):
@staticmethod @staticmethod
def quote(content, safe='/', encoding=None, errors=None): def quote(content, safe='/', encoding=None, errors=None):
""" """ Replaces single character non-ascii characters and URI specific
common quote function ones by their %xx code.
Wrapper to Python's unquote while remaining compatible with both
Python 2 & 3 since the reference to this function changed between
versions.
Args:
content (str): The URI string you wish to quote
safe (str): non-ascii characters and URI specific ones that you
do not wish to escape (if detected). Setting this
string to an empty one causes everything to be
escaped.
encoding (:obj:`str`, optional): encoding type
errors (:obj:`str`, errors): how to handle invalid character found
in encoded string (defined by encoding)
Returns:
str: The quoted URI string
""" """
if not content: if not content:
return '' return ''
@ -344,26 +545,60 @@ class NotifyBase(object):
@staticmethod @staticmethod
def urlencode(query, doseq=False, safe='', encoding=None, errors=None): def urlencode(query, doseq=False, safe='', encoding=None, errors=None):
""" """Convert a mapping object or a sequence of two-element tuples
common urlencode function
Wrapper to Python's unquote while remaining compatible with both
Python 2 & 3 since the reference to this function changed between
versions.
The resulting string is a series of key=value pairs separated by '&'
characters, where both key and value are quoted using the quote()
function.
Note: If the dictionary entry contains an entry that is set to None
it is not included in the final result set. If you want to
pass in an empty variable, set it to an empty string.
Args:
query (str): The dictionary to encode
doseq (:obj:`bool`, optional): Handle sequences
safe (:obj:`str`): non-ascii characters and URI specific ones that
you do not wish to escape (if detected). Setting this string
to an empty one causes everything to be escaped.
encoding (:obj:`str`, optional): encoding type
errors (:obj:`str`, errors): how to handle invalid character found
in encoded string (defined by encoding)
Returns:
str: The escaped parameters returned as a string
""" """
# Tidy query by eliminating any records set to None
_query = {k: v for (k, v) in query.items() if v is not None}
try: try:
# Python v3.x # Python v3.x
return _urlencode( return _urlencode(
query, doseq=doseq, safe=safe, encoding=encoding, _query, doseq=doseq, safe=safe, encoding=encoding,
errors=errors) errors=errors)
except TypeError: except TypeError:
# Python v2.7 # Python v2.7
return _urlencode(query) return _urlencode(_query)
@staticmethod @staticmethod
def split_path(path, unquote=True): def split_path(path, unquote=True):
""" """Splits a URL up into a list object.
Splits a URL up into a list object.
Parses a specified URL and breaks it into a list.
Args:
path (str): The path to split up into a list.
unquote (:obj:`bool`, optional): call unquote on each element
added to the returned list.
Returns:
list: A list containing all of the elements in the path
""" """
if unquote: if unquote:
return PATHSPLIT_LIST_DELIM.split( return PATHSPLIT_LIST_DELIM.split(
NotifyBase.unquote(path).lstrip('/')) NotifyBase.unquote(path).lstrip('/'))
@ -371,26 +606,51 @@ class NotifyBase(object):
@staticmethod @staticmethod
def is_email(address): def is_email(address):
""" """Determine if the specified entry is an email address
Returns True if specified entry is an email address
Args:
address (str): The string you want to check.
Returns:
bool: Returns True if the address specified is an email address
and False if it isn't.
""" """
return IS_EMAIL_RE.match(address) is not None return IS_EMAIL_RE.match(address) is not None
@staticmethod @staticmethod
def is_hostname(hostname): def is_hostname(hostname):
""" """Determine if the specified entry is a hostname
Returns True if specified entry is a hostname
Args:
hostname (str): The string you want to check.
Returns:
bool: Returns True if the hostname specified is in fact a hostame
and False if it isn't.
""" """
return is_hostname(hostname) return is_hostname(hostname)
@staticmethod @staticmethod
def parse_url(url, verify_host=True): 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.
This is very specific and customized for Apprise.
Args:
url (str): The URL you want to fully parse.
verify_host (:obj:`bool`, optional): a flag kept with the parsed
URL which some child classes will later use to verify SSL
keys (if SSL transactions take place). Unless under very
specific circumstances, it is strongly recomended that
you leave this default value set to True.
Returns:
A dictionary is returned containing the URL fully parsed if
successful, otherwise None is returned.
""" """
results = parse_url( results = parse_url(
url, default_schema='unknown', verify_host=verify_host) url, default_schema='unknown', verify_host=verify_host)
@ -417,6 +677,15 @@ class NotifyBase(object):
results['format'])) results['format']))
del results['format'] del results['format']
# Allow overriding the default overflow
if 'overflow' in results['qsd']:
results['overflow'] = results['qsd'].get('overflow')
if results['overflow'] not in OVERFLOW_MODES:
NotifyBase.logger.warning(
'Unsupported overflow specified {}'.format(
results['overflow']))
del results['overflow']
# Password overrides # Password overrides
if 'pass' in results['qsd']: if 'pass' in results['qsd']:
results['password'] = results['qsd']['pass'] results['password'] = results['qsd']['pass']
@ -425,6 +694,4 @@ class NotifyBase(object):
if 'user' in results['qsd']: if 'user' in results['qsd']:
results['user'] = results['qsd']['user'] results['user'] = results['qsd']['user']
results['headers'] = {k[1:]: v for k, v in results['qsd'].items()
if re.match(r'^-.', k)}
return results return results

View File

@ -23,12 +23,13 @@
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE. # THE SOFTWARE.
from json import dumps
import requests
import re import re
from time import time import requests
import hmac import hmac
from json import dumps
from time import time
from hashlib import sha1 from hashlib import sha1
from itertools import chain
try: try:
from urlparse import urlparse from urlparse import urlparse
@ -37,7 +38,7 @@ except ImportError:
from .NotifyBase import NotifyBase from .NotifyBase import NotifyBase
from .NotifyBase import HTTP_ERROR_MAP from .NotifyBase import HTTP_ERROR_MAP
from ..common import NotifyType
from ..common import NotifyImageSize from ..common import NotifyImageSize
from ..utils import compat_is_basestring from ..utils import compat_is_basestring
@ -168,7 +169,7 @@ class NotifyBoxcar(NotifyBase):
'(%s) specified.' % recipient, '(%s) specified.' % recipient,
) )
def notify(self, title, body, notify_type, **kwargs): def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs):
""" """
Perform Boxcar Notification Perform Boxcar Notification
""" """
@ -229,6 +230,9 @@ class NotifyBoxcar(NotifyBase):
)) ))
self.logger.debug('Boxcar Payload: %s' % str(payload)) self.logger.debug('Boxcar Payload: %s' % str(payload))
# Always call throttle before any remote server i/o is made
self.throttle()
try: try:
r = requests.post( r = requests.post(
notify_url, notify_url,
@ -272,6 +276,27 @@ class NotifyBoxcar(NotifyBase):
return True return True
def url(self):
"""
Returns the URL built dynamically based on specified arguments.
"""
# Define any arguments set
args = {
'format': self.notify_format,
'overflow': self.overflow_mode,
}
return '{schema}://{access}/{secret}/{recipients}/?{args}'.format(
schema=self.secure_protocol,
access=self.quote(self.access),
secret=self.quote(self.secret),
recipients='/'.join([
self.quote(x) for x in chain(
self.tags, self.device_tokens) if x != DEFAULT_TAG]),
args=self.urlencode(args),
)
@staticmethod @staticmethod
def parse_url(url): def parse_url(url):
""" """

View File

@ -26,10 +26,9 @@
from __future__ import absolute_import from __future__ import absolute_import
from __future__ import print_function from __future__ import print_function
import re
from .NotifyBase import NotifyBase from .NotifyBase import NotifyBase
from ..common import NotifyImageSize from ..common import NotifyImageSize
from ..common import NotifyType
from ..utils import GET_SCHEMA_RE from ..utils import GET_SCHEMA_RE
# Default our global support flag # Default our global support flag
@ -142,12 +141,23 @@ class NotifyDBus(NotifyBase):
# A URL that takes you to the setup/help of the specific protocol # A URL that takes you to the setup/help of the specific protocol
setup_url = 'https://github.com/caronc/apprise/wiki/Notify_dbus' setup_url = 'https://github.com/caronc/apprise/wiki/Notify_dbus'
# No throttling required for DBus queries
request_rate_per_sec = 0
# Allows the user to specify the NotifyImageSize object # Allows the user to specify the NotifyImageSize object
image_size = NotifyImageSize.XY_128 image_size = NotifyImageSize.XY_128
# The number of seconds to keep the message present for # The number of seconds to keep the message present for
message_timeout_ms = 13000 message_timeout_ms = 13000
# Limit results to just the first 10 line otherwise there is just to much
# content to display
body_max_line_count = 10
# A title can not be used for SMS Messages. Setting this to zero will
# cause any title (if defined) to get placed into the message body.
title_maxlen = 0
# This entry is a bit hacky, but it allows us to unit-test this library # This entry is a bit hacky, but it allows us to unit-test this library
# in an environment that simply doesn't have the gnome packages # in an environment that simply doesn't have the gnome packages
# available to us. It also allows us to handle situations where the # available to us. It also allows us to handle situations where the
@ -190,7 +200,7 @@ class NotifyDBus(NotifyBase):
self.x_axis = x_axis self.x_axis = x_axis
self.y_axis = y_axis self.y_axis = y_axis
def notify(self, title, body, notify_type, **kwargs): def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs):
""" """
Perform DBus Notification Perform DBus Notification
""" """
@ -249,16 +259,10 @@ class NotifyDBus(NotifyBase):
"Could not load Gnome notification icon ({}): {}" "Could not load Gnome notification icon ({}): {}"
.format(icon_path, e)) .format(icon_path, e))
# Limit results to just the first 10 line otherwise
# there is just to much content to display
body = re.split('[\r\n]+', body)
if title:
# Place title on first line if it exists
body.insert(0, title)
body = '\r\n'.join(body[0:10])
try: try:
# Always call throttle() before any remote execution is made
self.throttle()
dbus_iface.Notify( dbus_iface.Notify(
# Application Identifier # Application Identifier
self.app_id, self.app_id,
@ -280,13 +284,20 @@ class NotifyDBus(NotifyBase):
self.logger.info('Sent DBus notification.') self.logger.info('Sent DBus notification.')
except Exception as e: except Exception:
self.logger.warning('Failed to send DBus notification.') self.logger.warning('Failed to send DBus notification.')
self.logger.exception('DBus Exception') self.logger.exception('DBus Exception')
return False return False
return True return True
def url(self):
"""
Returns the URL built dynamically based on specified arguments.
"""
return '{schema}://'.format(schema=self.schema)
@staticmethod @staticmethod
def parse_url(url): def parse_url(url):
""" """

View File

@ -48,6 +48,7 @@ from .NotifyBase import NotifyBase
from .NotifyBase import HTTP_ERROR_MAP from .NotifyBase import HTTP_ERROR_MAP
from ..common import NotifyImageSize from ..common import NotifyImageSize
from ..common import NotifyFormat from ..common import NotifyFormat
from ..common import NotifyType
from ..utils import parse_bool from ..utils import parse_bool
@ -113,7 +114,7 @@ class NotifyDiscord(NotifyBase):
return return
def notify(self, title, body, notify_type, **kwargs): def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs):
""" """
Perform Discord Notification Perform Discord Notification
""" """
@ -180,8 +181,8 @@ class NotifyDiscord(NotifyBase):
else: else:
# not markdown # not markdown
payload['content'] = body if not title \ payload['content'] = \
else "{}\r\n{}".format(title, body) body if not title else "{}\r\n{}".format(title, body)
if self.avatar and image_url: if self.avatar and image_url:
payload['avatar_url'] = image_url payload['avatar_url'] = image_url
@ -201,6 +202,10 @@ class NotifyDiscord(NotifyBase):
notify_url, self.verify_certificate, notify_url, self.verify_certificate,
)) ))
self.logger.debug('Discord Payload: %s' % str(payload)) self.logger.debug('Discord Payload: %s' % str(payload))
# Always call throttle before any remote server i/o is made
self.throttle()
try: try:
r = requests.post( r = requests.post(
notify_url, notify_url,
@ -241,6 +246,28 @@ class NotifyDiscord(NotifyBase):
return True return True
def url(self):
"""
Returns the URL built dynamically based on specified arguments.
"""
# Define any arguments set
args = {
'format': self.notify_format,
'overflow': self.overflow_mode,
'tts': 'yes' if self.tts else 'no',
'avatar': 'yes' if self.avatar else 'no',
'footer': 'yes' if self.footer else 'no',
'thumbnail': 'yes' if self.thumbnail else 'no',
}
return '{schema}://{webhook_id}/{webhook_token}/?{args}'.format(
schema=self.secure_protocol,
webhook_id=self.quote(self.webhook_id),
webhook_token=self.quote(self.webhook_token),
args=self.urlencode(args),
)
@staticmethod @staticmethod
def parse_url(url): def parse_url(url):
""" """

View File

@ -24,15 +24,14 @@
# THE SOFTWARE. # THE SOFTWARE.
import re import re
from datetime import datetime
import smtplib import smtplib
from socket import error as SocketError
from email.mime.text import MIMEText from email.mime.text import MIMEText
from socket import error as SocketError
from datetime import datetime
from .NotifyBase import NotifyBase from .NotifyBase import NotifyBase
from ..common import NotifyFormat from ..common import NotifyFormat
from ..common import NotifyType
class WebBaseLogin(object): class WebBaseLogin(object):
@ -344,7 +343,7 @@ class NotifyEmail(NotifyBase):
break break
def notify(self, title, body, **kwargs): def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs):
""" """
Perform Email Notification Perform Email Notification
""" """
@ -375,6 +374,10 @@ class NotifyEmail(NotifyBase):
# bind the socket variable to the current namespace # bind the socket variable to the current namespace
socket = None socket = None
# Always call throttle before any remote server i/o is made
self.throttle()
try: try:
self.logger.debug('Connecting to remote SMTP server...') self.logger.debug('Connecting to remote SMTP server...')
socket_func = smtplib.SMTP socket_func = smtplib.SMTP
@ -421,6 +424,53 @@ class NotifyEmail(NotifyBase):
return True return True
def url(self):
"""
Returns the URL built dynamically based on specified arguments.
"""
# Define any arguments set
args = {
'format': self.notify_format,
'overflow': self.overflow_mode,
'to': self.to_addr,
'from': self.from_addr,
'name': self.from_name,
'mode': self.secure_mode,
'smtp': self.smtp_host,
'timeout': self.timeout,
'user': self.user,
}
# pull email suffix from username (if present)
user = self.user.split('@')[0]
# Determine Authentication
auth = ''
if self.user and self.password:
auth = '{user}:{password}@'.format(
user=self.quote(user, safe=''),
password=self.quote(self.password, safe=''),
)
else:
# user url
auth = '{user}@'.format(
user=self.quote(user, safe=''),
)
# Default Port setup
default_port = \
self.default_secure_port if self.secure else self.default_port
return '{schema}://{auth}{hostname}{port}/?{args}'.format(
schema=self.secure_protocol if self.secure else self.protocol,
auth=auth,
hostname=self.host,
port='' if self.port is None or self.port == default_port
else ':{}'.format(self.port),
args=self.urlencode(args),
)
@staticmethod @staticmethod
def parse_url(url): def parse_url(url):
""" """

View File

@ -37,6 +37,7 @@ from json import loads
from .NotifyBase import NotifyBase from .NotifyBase import NotifyBase
from .NotifyBase import HTTP_ERROR_MAP from .NotifyBase import HTTP_ERROR_MAP
from ..utils import parse_bool from ..utils import parse_bool
from ..common import NotifyType
from .. import __version__ as VERSION from .. import __version__ as VERSION
@ -445,7 +446,7 @@ class NotifyEmby(NotifyBase):
self.user_id = None self.user_id = None
return True return True
def notify(self, title, body, notify_type, **kwargs): def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs):
""" """
Perform Emby Notification Perform Emby Notification
""" """
@ -494,6 +495,10 @@ class NotifyEmby(NotifyBase):
session_url, self.verify_certificate, session_url, self.verify_certificate,
)) ))
self.logger.debug('Emby Payload: %s' % str(payload)) self.logger.debug('Emby Payload: %s' % str(payload))
# Always call throttle before the requests are made
self.throttle()
try: try:
r = requests.post( r = requests.post(
session_url, session_url,
@ -535,6 +540,39 @@ class NotifyEmby(NotifyBase):
return not has_error return not has_error
def url(self):
"""
Returns the URL built dynamically based on specified arguments.
"""
# Define any arguments set
args = {
'format': self.notify_format,
'overflow': self.overflow_mode,
'modal': 'yes' if self.modal else 'no',
}
# Determine Authentication
auth = ''
if self.user and self.password:
auth = '{user}:{password}@'.format(
user=self.quote(self.user, safe=''),
password=self.quote(self.password, safe=''),
)
else: # self.user is set
auth = '{user}@'.format(
user=self.quote(self.user, safe=''),
)
return '{schema}://{auth}{hostname}{port}/?{args}'.format(
schema=self.secure_protocol if self.secure else self.protocol,
auth=auth,
hostname=self.host,
port='' if self.port is None or self.port == self.emby_default_port
else ':{}'.format(self.port),
args=self.urlencode(args),
)
@property @property
def is_authenticated(self): def is_authenticated(self):
""" """

View File

@ -27,6 +27,7 @@ import requests
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 ..common import NotifyImageSize
from ..common import NotifyType
class NotifyFaast(NotifyBase): class NotifyFaast(NotifyBase):
@ -60,7 +61,7 @@ class NotifyFaast(NotifyBase):
self.authtoken = authtoken self.authtoken = authtoken
def notify(self, title, body, notify_type, **kwargs): def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs):
""" """
Perform Faast Notification Perform Faast Notification
""" """
@ -85,6 +86,10 @@ class NotifyFaast(NotifyBase):
self.notify_url, self.verify_certificate, self.notify_url, self.verify_certificate,
)) ))
self.logger.debug('Faast Payload: %s' % str(payload)) self.logger.debug('Faast Payload: %s' % str(payload))
# Always call throttle before any remote server i/o is made
self.throttle()
try: try:
r = requests.post( r = requests.post(
self.notify_url, self.notify_url,
@ -124,6 +129,23 @@ class NotifyFaast(NotifyBase):
return True return True
def url(self):
"""
Returns the URL built dynamically based on specified arguments.
"""
# Define any arguments set
args = {
'format': self.notify_format,
'overflow': self.overflow_mode,
}
return '{schema}://{authtoken}/?{args}'.format(
schema=self.protocol,
authtoken=self.quote(self.authtoken, safe=''),
args=self.urlencode(args),
)
@staticmethod @staticmethod
def parse_url(url): def parse_url(url):
""" """

View File

@ -26,10 +26,9 @@
from __future__ import absolute_import from __future__ import absolute_import
from __future__ import print_function from __future__ import print_function
import re
from .NotifyBase import NotifyBase from .NotifyBase import NotifyBase
from ..common import NotifyImageSize from ..common import NotifyImageSize
from ..common import NotifyType
# Default our global support flag # Default our global support flag
NOTIFY_GNOME_SUPPORT_ENABLED = False NOTIFY_GNOME_SUPPORT_ENABLED = False
@ -86,6 +85,18 @@ class NotifyGnome(NotifyBase):
# Allows the user to specify the NotifyImageSize object # Allows the user to specify the NotifyImageSize object
image_size = NotifyImageSize.XY_128 image_size = NotifyImageSize.XY_128
# Disable throttle rate for Gnome requests since they are normally
# local anyway
request_rate_per_sec = 0
# Limit results to just the first 10 line otherwise there is just to much
# content to display
body_max_line_count = 10
# A title can not be used for SMS Messages. Setting this to zero will
# cause any title (if defined) to get placed into the message body.
title_maxlen = 0
# This entry is a bit hacky, but it allows us to unit-test this library # This entry is a bit hacky, but it allows us to unit-test this library
# in an environment that simply doesn't have the gnome packages # in an environment that simply doesn't have the gnome packages
# available to us. It also allows us to handle situations where the # available to us. It also allows us to handle situations where the
@ -109,7 +120,7 @@ class NotifyGnome(NotifyBase):
else: else:
self.urgency = urgency self.urgency = urgency
def notify(self, title, body, notify_type, **kwargs): def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs):
""" """
Perform Gnome Notification Perform Gnome Notification
""" """
@ -119,15 +130,6 @@ class NotifyGnome(NotifyBase):
"Gnome Notifications are not supported by this system.") "Gnome Notifications are not supported by this system.")
return False return False
# Limit results to just the first 10 line otherwise
# there is just to much content to display
body = re.split('[\r\n]+', body)
if title:
# Place title on first line if it exists
body.insert(0, title)
body = '\r\n'.join(body[0:10])
try: try:
# App initialization # App initialization
Notify.init(self.app_id) Notify.init(self.app_id)
@ -141,6 +143,9 @@ class NotifyGnome(NotifyBase):
# Assign urgency # Assign urgency
notification.set_urgency(self.urgency) notification.set_urgency(self.urgency)
# Always call throttle before any remote server i/o is made
self.throttle()
try: try:
# Use Pixbuf to create the proper image type # Use Pixbuf to create the proper image type
image = GdkPixbuf.Pixbuf.new_from_file(icon_path) image = GdkPixbuf.Pixbuf.new_from_file(icon_path)
@ -157,13 +162,20 @@ class NotifyGnome(NotifyBase):
notification.show() notification.show()
self.logger.info('Sent Gnome notification.') self.logger.info('Sent Gnome notification.')
except Exception as e: except Exception:
self.logger.warning('Failed to send Gnome notification.') self.logger.warning('Failed to send Gnome notification.')
self.logger.exception('Gnome Exception') self.logger.exception('Gnome Exception')
return False return False
return True return True
def url(self):
"""
Returns the URL built dynamically based on specified arguments.
"""
return '{schema}://'.format(schema=self.protocol)
@staticmethod @staticmethod
def parse_url(url): def parse_url(url):
""" """

View File

@ -23,12 +23,11 @@
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE. # THE SOFTWARE.
import re
from .gntp import notifier from .gntp import notifier
from .gntp import errors from .gntp import errors
from ..NotifyBase import NotifyBase from ..NotifyBase import NotifyBase
from ...common import NotifyImageSize from ...common import NotifyImageSize
from ...common import NotifyType
# Priorities # Priorities
@ -69,12 +68,24 @@ class NotifyGrowl(NotifyBase):
# A URL that takes you to the setup/help of the specific protocol # A URL that takes you to the setup/help of the specific protocol
setup_url = 'https://github.com/caronc/apprise/wiki/Notify_growl' setup_url = 'https://github.com/caronc/apprise/wiki/Notify_growl'
# Default Growl Port
default_port = 23053
# Allows the user to specify the NotifyImageSize object # Allows the user to specify the NotifyImageSize object
image_size = NotifyImageSize.XY_72 image_size = NotifyImageSize.XY_72
# Disable throttle rate for Growl requests since they are normally
# local anyway
request_rate_per_sec = 0
# A title can not be used for Growl Messages. Setting this to zero will
# cause any title (if defined) to get placed into the message body.
title_maxlen = 0
# Limit results to just the first 10 line otherwise there is just to much
# content to display
body_max_line_count = 2
# Default Growl Port
default_port = 23053
def __init__(self, priority=None, version=2, **kwargs): def __init__(self, priority=None, version=2, **kwargs):
""" """
Initialize Growl Object Initialize Growl Object
@ -143,17 +154,11 @@ class NotifyGrowl(NotifyBase):
return return
def notify(self, title, body, notify_type, **kwargs): def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs):
""" """
Perform Growl Notification Perform Growl Notification
""" """
# Limit results to just the first 2 line otherwise there is just to
# much content to display
body = re.split('[\r\n]+', body)
body[0] = body[0].strip('#').strip()
body = '\r\n'.join(body[0:2])
icon = None icon = None
if self.version >= 2: if self.version >= 2:
# URL Based # URL Based
@ -178,6 +183,9 @@ class NotifyGrowl(NotifyBase):
# print the binary contents of an image # print the binary contents of an image
payload['icon'] = icon payload['icon'] = icon
# Always call throttle before any remote server i/o is made
self.throttle()
try: try:
response = self.growl.notify(**payload) response = self.growl.notify(**payload)
if not isinstance(response, bool): if not isinstance(response, bool):
@ -207,6 +215,44 @@ class NotifyGrowl(NotifyBase):
return True return True
def url(self):
"""
Returns the URL built dynamically based on specified arguments.
"""
_map = {
GrowlPriority.LOW: 'low',
GrowlPriority.MODERATE: 'moderate',
GrowlPriority.NORMAL: 'normal',
GrowlPriority.HIGH: 'high',
GrowlPriority.EMERGENCY: 'emergency',
}
# Define any arguments set
args = {
'format': self.notify_format,
'overflow': self.overflow_mode,
'priority':
_map[GrowlPriority.NORMAL] if self.priority not in _map
else _map[self.priority],
'version': self.version,
}
auth = ''
if self.password:
auth = '{password}@'.format(
password=self.quote(self.user, safe=''),
)
return '{schema}://{auth}{hostname}{port}/?{args}'.format(
schema=self.secure_protocol if self.secure else self.protocol,
auth=auth,
hostname=self.host,
port='' if self.port is None or self.port == self.default_port
else ':{}'.format(self.port),
args=self.urlencode(args),
)
@staticmethod @staticmethod
def parse_url(url): def parse_url(url):
""" """
@ -239,15 +285,10 @@ class NotifyGrowl(NotifyBase):
if 'priority' in results['qsd'] and len(results['qsd']['priority']): if 'priority' in results['qsd'] and len(results['qsd']['priority']):
_map = { _map = {
'l': GrowlPriority.LOW, 'l': GrowlPriority.LOW,
'-2': GrowlPriority.LOW,
'm': GrowlPriority.MODERATE, 'm': GrowlPriority.MODERATE,
'-1': GrowlPriority.MODERATE,
'n': GrowlPriority.NORMAL, 'n': GrowlPriority.NORMAL,
'0': GrowlPriority.NORMAL,
'h': GrowlPriority.HIGH, 'h': GrowlPriority.HIGH,
'1': GrowlPriority.HIGH,
'e': GrowlPriority.EMERGENCY, 'e': GrowlPriority.EMERGENCY,
'2': GrowlPriority.EMERGENCY,
} }
try: try:
results['priority'] = \ results['priority'] = \

View File

@ -34,16 +34,18 @@
# URL. For example, it might look like this: # URL. For example, it might look like this:
# https://maker.ifttt.com/use/a3nHB7gA9TfBQSqJAHklod # https://maker.ifttt.com/use/a3nHB7gA9TfBQSqJAHklod
# #
# In the above example a3nHB7gA9TfBQSqJAHklod becomes your {apikey} # In the above example a3nHB7gA9TfBQSqJAHklod becomes your {webhook_id}
# You will need this to make this notification work correctly # You will need this to make this notification work correctly
# #
# For each event you create you will assign it a name (this will be known as # For each event you create you will assign it a name (this will be known as
# the {event} when building your URL. # the {event} when building your URL.
import requests import requests
from json import dumps from json import dumps
from .NotifyBase import NotifyBase from .NotifyBase import NotifyBase
from .NotifyBase import HTTP_ERROR_MAP from .NotifyBase import HTTP_ERROR_MAP
from ..common import NotifyType
from ..utils import parse_list
class NotifyIFTTT(NotifyBase): class NotifyIFTTT(NotifyBase):
@ -59,7 +61,7 @@ class NotifyIFTTT(NotifyBase):
service_url = 'https://ifttt.com/' service_url = 'https://ifttt.com/'
# The default protocol # The default protocol
protocol = 'ifttt' secure_protocol = 'ifttt'
# A URL that takes you to the setup/help of the specific protocol # A URL that takes you to the setup/help of the specific protocol
setup_url = 'https://github.com/caronc/apprise/wiki/Notify_ifttt' setup_url = 'https://github.com/caronc/apprise/wiki/Notify_ifttt'
@ -87,37 +89,55 @@ class NotifyIFTTT(NotifyBase):
ifttt_default_type_key = 'value3' ifttt_default_type_key = 'value3'
# IFTTT uses the http protocol with JSON requests # IFTTT uses the http protocol with JSON requests
notify_url = 'https://maker.ifttt.com/trigger/{event}/with/key/{apikey}' notify_url = 'https://maker.ifttt.com/' \
'trigger/{event}/with/key/{webhook_id}'
def __init__(self, apikey, event, event_args=None, **kwargs): def __init__(self, webhook_id, events, add_tokens=None, del_tokens=None,
**kwargs):
""" """
Initialize IFTTT Object Initialize IFTTT Object
add_tokens can optionally be a dictionary of key/value pairs
that you want to include in the IFTTT post to the server.
del_tokens can optionally be a list/tuple/set of tokens
that you want to eliminate from the IFTTT post. There isn't
much real functionality to this one unless you want to remove
reference to Value1, Value2, and/or Value3
""" """
super(NotifyIFTTT, self).__init__(**kwargs) super(NotifyIFTTT, self).__init__(**kwargs)
if not apikey: if not webhook_id:
raise TypeError('You must specify the Webhooks apikey.') raise TypeError('You must specify the Webhooks webhook_id.')
if not event: # Store our Events we wish to trigger
raise TypeError('You must specify the Event you wish to trigger.') self.events = parse_list(events)
if not self.events:
raise TypeError(
'You must specify at least one event you wish to trigger on.')
# Store our APIKey # Store our APIKey
self.apikey = apikey self.webhook_id = webhook_id
# Store our Event we wish to trigger # Tokens to include in post
self.event = event self.add_tokens = {}
if add_tokens:
self.add_tokens.update(add_tokens)
if isinstance(event_args, dict): # Tokens to remove
# Make a copy of the arguments so that they can't change self.del_tokens = []
# outside of this plugin if del_tokens is not None:
self.event_args = event_args.copy() if isinstance(del_tokens, (list, tuple, set)):
self.del_tokens = del_tokens
else: else:
# Force a dictionary raise TypeError(
self.event_args = dict() 'del_token must be a list; {} was provided'.format(
str(type(del_tokens))))
def notify(self, title, body, notify_type, **kwargs): def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs):
""" """
Perform IFTTT Notification Perform IFTTT Notification
""" """
@ -134,72 +154,106 @@ class NotifyIFTTT(NotifyBase):
self.ifttt_default_type_key: notify_type, self.ifttt_default_type_key: notify_type,
} }
# Update our payload using any other event_args specified # Add any new tokens expected (this can also potentially override
payload.update(self.event_args) # any entries defined above)
payload.update(self.add_tokens)
# Eliminate empty fields; users wishing to cancel the use of the # Eliminate fields flagged for removal
# self.ifttt_default_ entries can preset these keys to being payload = {x: y for x, y in payload.items()
# empty so that they get caught here and removed. if x not in self.del_tokens}
payload = {x: y for x, y in payload.items() if y}
# URL to transmit content via # Track our failures
url = self.notify_url.format( error_count = 0
apikey=self.apikey,
event=self.event, # Create a copy of our event lit
events = list(self.events)
while len(events):
# Retrive an entry off of our event list
event = events.pop(0)
# URL to transmit content via
url = self.notify_url.format(
webhook_id=self.webhook_id,
event=event,
)
self.logger.debug('IFTTT POST URL: %s (cert_verify=%r)' % (
url, self.verify_certificate,
))
self.logger.debug('IFTTT Payload: %s' % str(payload))
# Always call throttle before any remote server i/o is made
self.throttle()
try:
r = requests.post(
url,
data=dumps(payload),
headers=headers,
verify=self.verify_certificate,
)
self.logger.debug(
u"IFTTT HTTP response headers: %r" % r.headers)
self.logger.debug(
u"IFTTT HTTP response body: %r" % r.content)
if r.status_code != requests.codes.ok:
# We had a problem
try:
self.logger.warning(
'Failed to send IFTTT:%s '
'notification: %s (error=%s).' % (
event,
HTTP_ERROR_MAP[r.status_code],
r.status_code))
except KeyError:
self.logger.warning(
'Failed to send IFTTT:%s '
'notification (error=%s).' % (
event, r.status_code))
# self.logger.debug('Response Details: %s' % r.content)
error_count += 1
else:
self.logger.info(
'Sent IFTTT notification to Event %s.' % event)
except requests.RequestException as e:
self.logger.warning(
'A Connection error occured sending IFTTT:%s ' % (
event) + 'notification.'
)
self.logger.debug('Socket Exception: %s' % str(e))
error_count += 1
return (error_count == 0)
def url(self):
"""
Returns the URL built dynamically based on specified arguments.
"""
# Define any arguments set
args = {
'format': self.notify_format,
'overflow': self.overflow_mode,
}
# Store any new key/value pairs added to our list
args.update({'+{}'.format(k): v for k, v in self.add_tokens})
args.update({'-{}'.format(k): '' for k in self.del_tokens})
return '{schema}://{webhook_id}@{events}/?{args}'.format(
schema=self.secure_protocol,
webhook_id=self.webhook_id,
events='/'.join([self.quote(x, safe='') for x in self.events]),
args=self.urlencode(args),
) )
self.logger.debug('IFTTT POST URL: %s (cert_verify=%r)' % (
url, self.verify_certificate,
))
self.logger.debug('IFTTT Payload: %s' % str(payload))
try:
r = requests.post(
url,
data=dumps(payload),
headers=headers,
verify=self.verify_certificate,
)
self.logger.debug(
u"IFTTT HTTP response status: %r" % r.status_code)
self.logger.debug(
u"IFTTT HTTP response headers: %r" % r.headers)
self.logger.debug(
u"IFTTT HTTP response body: %r" % r.content)
if r.status_code != requests.codes.ok:
# We had a problem
try:
self.logger.warning(
'Failed to send IFTTT:%s '
'notification: %s (error=%s).' % (
self.event,
HTTP_ERROR_MAP[r.status_code],
r.status_code))
except KeyError:
self.logger.warning(
'Failed to send IFTTT:%s '
'notification (error=%s).' % (
self.event,
r.status_code))
# self.logger.debug('Response Details: %s' % r.content)
return False
else:
self.logger.info(
'Sent IFTTT notification to Event %s.' % self.event)
except requests.RequestException as e:
self.logger.warning(
'A Connection error occured sending IFTTT:%s ' % (
self.event) + 'notification.'
)
self.logger.debug('Socket Exception: %s' % str(e))
return False
return True
@staticmethod @staticmethod
def parse_url(url): def parse_url(url):
""" """
@ -214,22 +268,14 @@ class NotifyIFTTT(NotifyBase):
return results return results
# Our Event # Our Event
results['event'] = results['host'] results['events'] = list()
results['events'].append(results['host'])
# Our API Key # Our API Key
results['apikey'] = results['user'] results['webhook_id'] = results['user']
# Store ValueX entries based on each entry past the host # Now fetch the remaining tokens
results['event_args'] = { results['events'].extend([x for x in filter(
'{0}{1}'.format(NotifyIFTTT.ifttt_default_key_prefix, n + 1): bool, NotifyBase.split_path(results['fullpath']))][0:])
NotifyBase.unquote(x)
for n, x in enumerate(
NotifyBase.split_path(results['fullpath'])) if x}
# Allow users to set key=val parameters to specify more types
# of payload options
results['event_args'].update(
{k: NotifyBase.unquote(v)
for k, v in results['qsd'].items()})
return results return results

View File

@ -29,6 +29,7 @@ from json import dumps
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 ..common import NotifyImageSize
from ..common import NotifyType
from ..utils import compat_is_basestring from ..utils import compat_is_basestring
@ -52,9 +53,17 @@ class NotifyJSON(NotifyBase):
# Allows the user to specify the NotifyImageSize object # Allows the user to specify the NotifyImageSize object
image_size = NotifyImageSize.XY_128 image_size = NotifyImageSize.XY_128
def __init__(self, **kwargs): # Disable throttle rate for JSON requests since they are normally
# local anyway
request_rate_per_sec = 0
def __init__(self, headers, **kwargs):
""" """
Initialize JSON Object Initialize JSON Object
headers can be a dictionary of key/value pairs that you want to
additionally include as part of the server headers to post with
""" """
super(NotifyJSON, self).__init__(**kwargs) super(NotifyJSON, self).__init__(**kwargs)
@ -68,9 +77,51 @@ class NotifyJSON(NotifyBase):
if not compat_is_basestring(self.fullpath): if not compat_is_basestring(self.fullpath):
self.fullpath = '/' self.fullpath = '/'
self.headers = {}
if headers:
# Store our extra headers
self.headers.update(headers)
return return
def notify(self, title, body, notify_type, **kwargs): def url(self):
"""
Returns the URL built dynamically based on specified arguments.
"""
# Define any arguments set
args = {
'format': self.notify_format,
'overflow': self.overflow_mode,
}
# Append our headers into our args
args.update({'+{}'.format(k): v for k, v in self.headers.items()})
# Determine Authentication
auth = ''
if self.user and self.password:
auth = '{user}:{password}@'.format(
user=self.quote(self.user, safe=''),
password=self.quote(self.password, safe=''),
)
elif self.user:
auth = '{user}@'.format(
user=self.quote(self.user, safe=''),
)
default_port = 443 if self.secure else 80
return '{schema}://{auth}{hostname}{port}/?{args}'.format(
schema=self.secure_protocol if self.secure else self.protocol,
auth=auth,
hostname=self.host,
port='' if self.port is None or self.port == default_port
else ':{}'.format(self.port),
args=self.urlencode(args),
)
def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs):
""" """
Perform JSON Notification Perform JSON Notification
""" """
@ -91,8 +142,8 @@ class NotifyJSON(NotifyBase):
'Content-Type': 'application/json' 'Content-Type': 'application/json'
} }
if self.headers: # Apply any/all header over-rides defined
headers.update(self.headers) headers.update(self.headers)
auth = None auth = None
if self.user: if self.user:
@ -108,6 +159,10 @@ class NotifyJSON(NotifyBase):
url, self.verify_certificate, url, self.verify_certificate,
)) ))
self.logger.debug('JSON Payload: %s' % str(payload)) self.logger.debug('JSON Payload: %s' % str(payload))
# Always call throttle before any remote server i/o is made
self.throttle()
try: try:
r = requests.post( r = requests.post(
url, url,
@ -145,3 +200,23 @@ class NotifyJSON(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 as we couldn't load the results
return results
# Add our headers that the user can potentially over-ride if they wish
# to to our returned result set
results['headers'] = results['qsd-']
results['headers'].update(results['qsd+'])
return results

View File

@ -39,6 +39,7 @@ import requests
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 ..common import NotifyImageSize
from ..common import NotifyType
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
@ -78,7 +79,7 @@ class NotifyJoin(NotifyBase):
service_url = 'https://joaoapps.com/join/' service_url = 'https://joaoapps.com/join/'
# The default protocol # The default protocol
protocol = 'join' secure_protocol = 'join'
# A URL that takes you to the setup/help of the specific protocol # A URL that takes you to the setup/help of the specific protocol
setup_url = 'https://github.com/caronc/apprise/wiki/Notify_join' setup_url = 'https://github.com/caronc/apprise/wiki/Notify_join'
@ -90,6 +91,10 @@ class NotifyJoin(NotifyBase):
# Allows the user to specify the NotifyImageSize object # Allows the user to specify the NotifyImageSize object
image_size = NotifyImageSize.XY_72 image_size = NotifyImageSize.XY_72
# Limit results to just the first 2 line otherwise there is just to much
# content to display
body_max_line_count = 2
# The maximum allowable characters allowed in the body per message # The maximum allowable characters allowed in the body per message
body_maxlen = 1000 body_maxlen = 1000
@ -126,22 +131,11 @@ class NotifyJoin(NotifyBase):
# Default to everyone # Default to everyone
self.devices.append('group.all') self.devices.append('group.all')
def notify(self, title, body, notify_type, **kwargs): def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs):
""" """
Perform Join Notification Perform Join Notification
""" """
try:
# Limit results to just the first 2 line otherwise
# there is just to much content to display
body = re.split('[\r\n]+', body)
body[0] = body[0].strip('#').strip()
body = '\r\n'.join(body[0:2])
except (AttributeError, TypeError):
# body was None or not of a type string
body = ''
headers = { headers = {
'User-Agent': self.app_id, 'User-Agent': self.app_id,
'Content-Type': 'application/x-www-form-urlencoded', 'Content-Type': 'application/x-www-form-urlencoded',
@ -188,6 +182,9 @@ class NotifyJoin(NotifyBase):
)) ))
self.logger.debug('Join Payload: %s' % str(payload)) self.logger.debug('Join Payload: %s' % str(payload))
# Always call throttle before any remote server i/o is made
self.throttle()
try: try:
r = requests.post( r = requests.post(
url, url,
@ -227,12 +224,25 @@ class NotifyJoin(NotifyBase):
self.logger.debug('Socket Exception: %s' % str(e)) self.logger.debug('Socket Exception: %s' % str(e))
return_status = False return_status = False
if len(devices):
# Prevent thrashing requests
self.throttle()
return return_status return return_status
def url(self):
"""
Returns the URL built dynamically based on specified arguments.
"""
# Define any arguments set
args = {
'format': self.notify_format,
'overflow': self.overflow_mode,
}
return '{schema}://{apikey}/{devices}/?{args}'.format(
schema=self.secure_protocol,
apikey=self.quote(self.apikey, safe=''),
devices='/'.join([self.quote(x) for x in self.devices]),
args=self.urlencode(args))
@staticmethod @staticmethod
def parse_url(url): def parse_url(url):
""" """

View File

@ -30,6 +30,7 @@ from time import time
from .NotifyBase import NotifyBase from .NotifyBase import NotifyBase
from .NotifyBase import HTTP_ERROR_MAP from .NotifyBase import HTTP_ERROR_MAP
from ..common import NotifyType
# Token required as part of the API request # Token required as part of the API request
VALIDATE_TOKEN = re.compile(r'[A-Za-z0-9]{64}') VALIDATE_TOKEN = re.compile(r'[A-Za-z0-9]{64}')
@ -112,7 +113,6 @@ class NotifyMatrix(NotifyBase):
if not self.user: if not self.user:
self.logger.warning( self.logger.warning(
'No user was specified; using %s.' % MATRIX_DEFAULT_USER) 'No user was specified; using %s.' % MATRIX_DEFAULT_USER)
self.user = MATRIX_DEFAULT_USER
if mode not in MATRIX_NOTIFICATION_MODES: if mode not in MATRIX_NOTIFICATION_MODES:
self.logger.warning('The mode specified (%s) is invalid.' % mode) self.logger.warning('The mode specified (%s) is invalid.' % mode)
@ -135,7 +135,7 @@ class NotifyMatrix(NotifyBase):
re.IGNORECASE, re.IGNORECASE,
) )
def notify(self, title, body, notify_type, **kwargs): def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs):
""" """
Perform Matrix Notification Perform Matrix Notification
""" """
@ -170,6 +170,10 @@ class NotifyMatrix(NotifyBase):
url, self.verify_certificate, url, self.verify_certificate,
)) ))
self.logger.debug('Matrix Payload: %s' % str(payload)) self.logger.debug('Matrix Payload: %s' % str(payload))
# Always call throttle before any remote server i/o is made
self.throttle()
try: try:
r = requests.post( r = requests.post(
url, url,
@ -209,7 +213,7 @@ class NotifyMatrix(NotifyBase):
def __slack_mode_payload(self, title, body, notify_type): def __slack_mode_payload(self, title, body, notify_type):
# prepare JSON Object # prepare JSON Object
payload = { payload = {
'username': self.user, 'username': self.user if self.user else MATRIX_DEFAULT_USER,
# Use Markdown language # Use Markdown language
'mrkdwn': True, 'mrkdwn': True,
'attachments': [{ 'attachments': [{
@ -230,13 +234,44 @@ class NotifyMatrix(NotifyBase):
msg = '<h4>%s</h4>%s<br/>' % (title, body) msg = '<h4>%s</h4>%s<br/>' % (title, body)
payload = { payload = {
'displayName': self.user, 'displayName': self.user if self.user else MATRIX_DEFAULT_USER,
'format': 'html', 'format': 'html',
'text': msg, 'text': msg,
} }
return payload return payload
def url(self):
"""
Returns the URL built dynamically based on specified arguments.
"""
# Define any arguments set
args = {
'format': self.notify_format,
'overflow': self.overflow_mode,
'mode': self.mode,
}
# Determine Authentication
auth = ''
if self.user:
auth = '{user}@'.format(
user=self.quote(self.user, safe=''),
)
default_port = 443 if self.secure else 80
return '{schema}://{auth}{host}/{token}{port}/?{args}'.format(
schema=self.secure_protocol if self.secure else self.protocol,
host=self.host,
auth=auth,
token=self.token,
port='' if self.port is None or self.port == default_port
else ':{}'.format(self.port),
args=self.urlencode(args),
)
@staticmethod @staticmethod
def parse_url(url): def parse_url(url):
""" """

View File

@ -30,6 +30,7 @@ from json import dumps
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 ..common import NotifyImageSize
from ..common import NotifyType
# Some Reference Locations: # Some Reference Locations:
# - https://docs.mattermost.com/developer/webhooks-incoming.html # - https://docs.mattermost.com/developer/webhooks-incoming.html
@ -68,6 +69,9 @@ class NotifyMatterMost(NotifyBase):
# The maximum allowable characters allowed in the body per message # The maximum allowable characters allowed in the body per message
body_maxlen = 4000 body_maxlen = 4000
# Mattermost does not have a title
title_maxlen = 0
def __init__(self, authtoken, channel=None, **kwargs): def __init__(self, authtoken, channel=None, **kwargs):
""" """
Initialize MatterMost Object Initialize MatterMost Object
@ -108,7 +112,7 @@ class NotifyMatterMost(NotifyBase):
return return
def notify(self, title, body, notify_type, **kwargs): def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs):
""" """
Perform MatterMost Notification Perform MatterMost Notification
""" """
@ -120,7 +124,7 @@ class NotifyMatterMost(NotifyBase):
# prepare JSON Object # prepare JSON Object
payload = { payload = {
'text': '###### %s\n%s' % (title, body), 'text': body,
'icon_url': self.image_url(notify_type), 'icon_url': self.image_url(notify_type),
} }
@ -140,6 +144,10 @@ class NotifyMatterMost(NotifyBase):
url, self.verify_certificate, url, self.verify_certificate,
)) ))
self.logger.debug('MatterMost Payload: %s' % str(payload)) self.logger.debug('MatterMost Payload: %s' % str(payload))
# Always call throttle before any remote server i/o is made
self.throttle()
try: try:
r = requests.post( r = requests.post(
url, url,
@ -179,6 +187,29 @@ class NotifyMatterMost(NotifyBase):
return True return True
def url(self):
"""
Returns the URL built dynamically based on specified arguments.
"""
# Define any arguments set
args = {
'format': self.notify_format,
'overflow': self.overflow_mode,
}
default_port = 443 if self.secure else self.default_port
default_schema = self.secure_protocol if self.secure else self.protocol
return '{schema}://{hostname}{port}/{authtoken}/?{args}'.format(
schema=default_schema,
hostname=self.host,
port='' if not self.port or self.port == default_port
else ':{}'.format(self.port),
authtoken=self.quote(self.authtoken, safe=''),
args=self.urlencode(args),
)
@staticmethod @staticmethod
def parse_url(url): def parse_url(url):
""" """

View File

@ -28,6 +28,7 @@ import requests
from .NotifyBase import NotifyBase from .NotifyBase import NotifyBase
from .NotifyBase import HTTP_ERROR_MAP from .NotifyBase import HTTP_ERROR_MAP
from ..common import NotifyType
# Used to validate API Key # Used to validate API Key
VALIDATE_APIKEY = re.compile(r'[A-Za-z0-9]{40}') VALIDATE_APIKEY = re.compile(r'[A-Za-z0-9]{40}')
@ -81,6 +82,10 @@ 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'
# Disable throttle rate for Prowl requests since they are normally
# local anyway
request_rate_per_sec = 0
# The maximum allowable characters allowed in the body per message # The maximum allowable characters allowed in the body per message
body_maxlen = 10000 body_maxlen = 10000
@ -124,7 +129,7 @@ class NotifyProwl(NotifyBase):
# Store the Provider Key # Store the Provider Key
self.providerkey = providerkey self.providerkey = providerkey
def notify(self, title, body, **kwargs): def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs):
""" """
Perform Prowl Notification Perform Prowl Notification
""" """
@ -150,6 +155,10 @@ class NotifyProwl(NotifyBase):
self.notify_url, self.verify_certificate, self.notify_url, self.verify_certificate,
)) ))
self.logger.debug('Prowl Payload: %s' % str(payload)) self.logger.debug('Prowl Payload: %s' % str(payload))
# Always call throttle before any remote server i/o is made
self.throttle()
try: try:
r = requests.post( r = requests.post(
self.notify_url, self.notify_url,
@ -190,6 +199,35 @@ class NotifyProwl(NotifyBase):
return True return True
def url(self):
"""
Returns the URL built dynamically based on specified arguments.
"""
_map = {
ProwlPriority.LOW: 'low',
ProwlPriority.MODERATE: 'moderate',
ProwlPriority.NORMAL: 'normal',
ProwlPriority.HIGH: 'high',
ProwlPriority.EMERGENCY: 'emergency',
}
# Define any arguments set
args = {
'format': self.notify_format,
'overflow': self.overflow_mode,
'priority': 'normal' if self.priority not in _map
else _map[self.priority]
}
return '{schema}://{apikey}/{providerkey}/?{args}'.format(
schema=self.secure_protocol,
apikey=self.quote(self.apikey, safe=''),
providerkey='' if not self.providerkey
else self.quote(self.providerkey, safe=''),
args=self.urlencode(args),
)
@staticmethod @staticmethod
def parse_url(url): def parse_url(url):
""" """
@ -216,15 +254,10 @@ class NotifyProwl(NotifyBase):
if 'priority' in results['qsd'] and len(results['qsd']['priority']): if 'priority' in results['qsd'] and len(results['qsd']['priority']):
_map = { _map = {
'l': ProwlPriority.LOW, 'l': ProwlPriority.LOW,
'-2': ProwlPriority.LOW,
'm': ProwlPriority.MODERATE, 'm': ProwlPriority.MODERATE,
'-1': ProwlPriority.MODERATE,
'n': ProwlPriority.NORMAL, 'n': ProwlPriority.NORMAL,
'0': ProwlPriority.NORMAL,
'h': ProwlPriority.HIGH, 'h': ProwlPriority.HIGH,
'1': ProwlPriority.HIGH,
'e': ProwlPriority.EMERGENCY, 'e': ProwlPriority.EMERGENCY,
'2': ProwlPriority.EMERGENCY,
} }
try: try:
results['priority'] = \ results['priority'] = \

View File

@ -30,7 +30,7 @@ from json import dumps
from .NotifyBase import NotifyBase from .NotifyBase import NotifyBase
from .NotifyBase import HTTP_ERROR_MAP from .NotifyBase import HTTP_ERROR_MAP
from .NotifyBase import IS_EMAIL_RE from .NotifyBase import IS_EMAIL_RE
from ..common import NotifyType
from ..utils import compat_is_basestring from ..utils import compat_is_basestring
# Flag used as a placeholder to sending to all devices # Flag used as a placeholder to sending to all devices
@ -87,7 +87,7 @@ class NotifyPushBullet(NotifyBase):
if len(self.recipients) == 0: if len(self.recipients) == 0:
self.recipients = (PUSHBULLET_SEND_TO_ALL, ) self.recipients = (PUSHBULLET_SEND_TO_ALL, )
def notify(self, title, body, **kwargs): def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs):
""" """
Perform PushBullet Notification Perform PushBullet Notification
""" """
@ -135,6 +135,10 @@ class NotifyPushBullet(NotifyBase):
self.notify_url, self.verify_certificate, self.notify_url, self.verify_certificate,
)) ))
self.logger.debug('PushBullet Payload: %s' % str(payload)) self.logger.debug('PushBullet Payload: %s' % str(payload))
# Always call throttle before any remote server i/o is made
self.throttle()
try: try:
r = requests.post( r = requests.post(
self.notify_url, self.notify_url,
@ -176,12 +180,31 @@ class NotifyPushBullet(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(recipients):
# Prevent thrashing requests
self.throttle()
return not has_error return not has_error
def url(self):
"""
Returns the URL built dynamically based on specified arguments.
"""
# Define any arguments set
args = {
'format': self.notify_format,
'overflow': self.overflow_mode,
}
recipients = '/'.join([self.quote(x) for x in self.recipients])
if recipients == PUSHBULLET_SEND_TO_ALL:
# keyword is reserved for internal usage only; it's safe to remove
# it from the recipients list
recipients = ''
return '{schema}://{accesstoken}/{recipients}/?{args}'.format(
schema=self.secure_protocol,
accesstoken=self.quote(self.accesstoken, safe=''),
recipients=recipients,
args=self.urlencode(args))
@staticmethod @staticmethod
def parse_url(url): def parse_url(url):
""" """

View File

@ -26,9 +26,11 @@
import re import re
import requests import requests
from json import dumps from json import dumps
from itertools import chain
from .NotifyBase import NotifyBase from .NotifyBase import NotifyBase
from .NotifyBase import HTTP_ERROR_MAP from .NotifyBase import HTTP_ERROR_MAP
from ..common import NotifyType
from ..utils import compat_is_basestring from ..utils import compat_is_basestring
# Used to detect and parse channels # Used to detect and parse channels
@ -127,7 +129,7 @@ class NotifyPushed(NotifyBase):
return return
def notify(self, title, body, notify_type, **kwargs): def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs):
""" """
Perform Pushed Notification Perform Pushed Notification
""" """
@ -152,7 +154,7 @@ class NotifyPushed(NotifyBase):
if len(self.channels) + len(self.users) == 0: if len(self.channels) + len(self.users) == 0:
# Just notify the app # Just notify the app
return self.send_notification( return self._send(
payload=payload, notify_type=notify_type, **kwargs) payload=payload, notify_type=notify_type, **kwargs)
# If our code reaches here, we want to target channels and users (by # If our code reaches here, we want to target channels and users (by
@ -170,16 +172,12 @@ class NotifyPushed(NotifyBase):
# Get Channel # Get Channel
_payload['target_alias'] = channels.pop(0) _payload['target_alias'] = channels.pop(0)
if not self.send_notification( if not self._send(
payload=_payload, notify_type=notify_type, **kwargs): payload=_payload, notify_type=notify_type, **kwargs):
# toggle flag # toggle flag
has_error = True has_error = True
if len(channels) + len(users) > 0:
# Prevent thrashing requests
self.throttle()
# Copy our payload # Copy our payload
_payload = dict(payload) _payload = dict(payload)
_payload['target_type'] = 'pushed_id' _payload['target_type'] = 'pushed_id'
@ -188,23 +186,20 @@ class NotifyPushed(NotifyBase):
while len(users): while len(users):
# Get User's Pushed ID # Get User's Pushed ID
_payload['pushed_id'] = users.pop(0) _payload['pushed_id'] = users.pop(0)
if not self.send_notification(
if not self._send(
payload=_payload, notify_type=notify_type, **kwargs): payload=_payload, notify_type=notify_type, **kwargs):
# toggle flag # toggle flag
has_error = True has_error = True
if len(users) > 0:
# Prevent thrashing requests
self.throttle()
return not has_error return not has_error
def send_notification(self, payload, notify_type, **kwargs): def _send(self, payload, notify_type, **kwargs):
""" """
A lower level call that directly pushes a payload to the Pushed A lower level call that directly pushes a payload to the Pushed
Notification servers. This should never be called directly; it is Notification servers. This should never be called directly; it is
referenced automatically through the notify() function. referenced automatically through the send() function.
""" """
headers = { headers = {
@ -216,6 +211,10 @@ class NotifyPushed(NotifyBase):
self.notify_url, self.verify_certificate, self.notify_url, self.verify_certificate,
)) ))
self.logger.debug('Pushed Payload: %s' % str(payload)) self.logger.debug('Pushed Payload: %s' % str(payload))
# Always call throttle before any remote server i/o is made
self.throttle()
try: try:
r = requests.post( r = requests.post(
self.notify_url, self.notify_url,
@ -256,6 +255,30 @@ class NotifyPushed(NotifyBase):
return True return True
def url(self):
"""
Returns the URL built dynamically based on specified arguments.
"""
# Define any arguments set
args = {
'format': self.notify_format,
'overflow': self.overflow_mode,
}
return '{schema}://{app_key}/{app_secret}/{targets}/?{args}'.format(
schema=self.secure_protocol,
app_key=self.quote(self.app_key, safe=''),
app_secret=self.quote(self.app_secret, safe=''),
targets='/'.join(
[self.quote(x) for x in chain(
# Channels are prefixed with a pound/hashtag symbol
['#{}'.format(x) for x in self.channels],
# Users are prefixed with an @ symbol
['@{}'.format(x) for x in self.users],
)]),
args=self.urlencode(args))
@staticmethod @staticmethod
def parse_url(url): def parse_url(url):
""" """

View File

@ -28,6 +28,7 @@ from .pushjet import errors
from .pushjet import pushjet from .pushjet import pushjet
from ..NotifyBase import NotifyBase from ..NotifyBase import NotifyBase
from ...common import NotifyType
PUBLIC_KEY_RE = re.compile( PUBLIC_KEY_RE = re.compile(
r'^[a-z0-9]{4}-[a-z0-9]{6}-[a-z0-9]{12}-[a-z0-9]{5}-[a-z0-9]{9}$', re.I) r'^[a-z0-9]{4}-[a-z0-9]{6}-[a-z0-9]{12}-[a-z0-9]{5}-[a-z0-9]{9}$', re.I)
@ -52,27 +53,35 @@ class NotifyPushjet(NotifyBase):
# A URL that takes you to the setup/help of the specific protocol # A URL that takes you to the setup/help of the specific protocol
setup_url = 'https://github.com/caronc/apprise/wiki/Notify_pushjet' setup_url = 'https://github.com/caronc/apprise/wiki/Notify_pushjet'
def __init__(self, **kwargs): # Disable throttle rate for Pushjet requests since they are normally
# local anyway (the remote/online service is no more)
request_rate_per_sec = 0
def __init__(self, secret_key, **kwargs):
""" """
Initialize Pushjet Object Initialize Pushjet Object
""" """
super(NotifyPushjet, self).__init__(**kwargs) super(NotifyPushjet, self).__init__(**kwargs)
def notify(self, title, body, notify_type): # store our key
self.secret_key = secret_key
def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs):
""" """
Perform Pushjet Notification Perform Pushjet Notification
""" """
# Always call throttle before any remote server i/o is made
self.throttle()
server = "https://" if self.secure else "http://"
server += self.host
if self.port:
server += ":" + str(self.port)
try: try:
server = "http://"
if self.secure:
server = "https://"
server += self.host
if self.port:
server += ":" + str(self.port)
api = pushjet.Api(server) api = pushjet.Api(server)
service = api.Service(secret_key=self.user) service = api.Service(secret_key=self.secret_key)
service.send(body, title) service.send(body, title)
self.logger.info('Sent Pushjet notification.') self.logger.info('Sent Pushjet notification.')
@ -84,6 +93,28 @@ class NotifyPushjet(NotifyBase):
return True return True
def url(self):
"""
Returns the URL built dynamically based on specified arguments.
"""
# Define any arguments set
args = {
'format': self.notify_format,
'overflow': self.overflow_mode,
}
default_port = 443 if self.secure else 80
return '{schema}://{secret_key}@{hostname}{port}/?{args}'.format(
schema=self.secure_protocol if self.secure else self.protocol,
secret_key=self.quote(self.secret_key, safe=''),
hostname=self.host,
port='' if self.port is None or self.port == default_port
else ':{}'.format(self.port),
args=self.urlencode(args),
)
@staticmethod @staticmethod
def parse_url(url): def parse_url(url):
""" """
@ -91,10 +122,10 @@ class NotifyPushjet(NotifyBase):
us to substantiate this object. us to substantiate this object.
Syntax: Syntax:
pjet://secret@hostname pjet://secret_key@hostname
pjet://secret@hostname:port pjet://secret_key@hostname:port
pjets://secret@hostname pjets://secret_key@hostname
pjets://secret@hostname:port pjets://secret_key@hostname:port
""" """
results = NotifyBase.parse_url(url) results = NotifyBase.parse_url(url)
@ -107,4 +138,7 @@ class NotifyPushjet(NotifyBase):
# a username is required # a username is required
return None return None
# Store it as it's value
results['secret_key'] = results.get('user')
return results return results

View File

@ -26,9 +26,10 @@
import re import re
import requests import requests
from ..utils import compat_is_basestring
from .NotifyBase import NotifyBase from .NotifyBase import NotifyBase
from .NotifyBase import HTTP_ERROR_MAP from .NotifyBase import HTTP_ERROR_MAP
from ..common import NotifyType
from ..utils import compat_is_basestring
# Flag used as a placeholder to sending to all devices # Flag used as a placeholder to sending to all devices
PUSHOVER_SEND_TO_ALL = 'ALL_DEVICES' PUSHOVER_SEND_TO_ALL = 'ALL_DEVICES'
@ -149,7 +150,7 @@ class NotifyPushover(NotifyBase):
'The user/group specified (%s) is invalid.' % self.user, 'The user/group specified (%s) is invalid.' % self.user,
) )
def notify(self, title, body, **kwargs): def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs):
""" """
Perform Pushover Notification Perform Pushover Notification
""" """
@ -189,6 +190,10 @@ class NotifyPushover(NotifyBase):
self.notify_url, self.verify_certificate, self.notify_url, self.verify_certificate,
)) ))
self.logger.debug('Pushover Payload: %s' % str(payload)) self.logger.debug('Pushover Payload: %s' % str(payload))
# Always call throttle before any remote server i/o is made
self.throttle()
try: try:
r = requests.post( r = requests.post(
self.notify_url, self.notify_url,
@ -231,12 +236,44 @@ class NotifyPushover(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(devices):
# Prevent thrashing requests
self.throttle()
return not has_error return not has_error
def url(self):
"""
Returns the URL built dynamically based on specified arguments.
"""
_map = {
PushoverPriority.LOW: 'low',
PushoverPriority.MODERATE: 'moderate',
PushoverPriority.NORMAL: 'normal',
PushoverPriority.HIGH: 'high',
PushoverPriority.EMERGENCY: 'emergency',
}
# Define any arguments set
args = {
'format': self.notify_format,
'overflow': self.overflow_mode,
'priority':
_map[PushoverPriority.NORMAL] if self.priority not in _map
else _map[self.priority],
}
devices = '/'.join([self.quote(x) for x in self.devices])
if devices == PUSHOVER_SEND_TO_ALL:
# keyword is reserved for internal usage only; it's safe to remove
# it from the devices list
devices = ''
return '{schema}://{auth}{token}/{devices}/?{args}'.format(
schema=self.secure_protocol,
auth='' if not self.user
else '{user}@'.format(user=self.quote(self.user, safe='')),
token=self.quote(self.token, safe=''),
devices=devices,
args=self.urlencode(args))
@staticmethod @staticmethod
def parse_url(url): def parse_url(url):
""" """

View File

@ -26,9 +26,11 @@
import re import re
import requests import requests
from json import loads from json import loads
from itertools import chain
from .NotifyBase import NotifyBase from .NotifyBase import NotifyBase
from .NotifyBase import HTTP_ERROR_MAP from .NotifyBase import HTTP_ERROR_MAP
from ..common import NotifyType
from ..utils import compat_is_basestring from ..utils import compat_is_basestring
IS_CHANNEL = re.compile(r'^#(?P<name>[A-Za-z0-9]+)$') IS_CHANNEL = re.compile(r'^#(?P<name>[A-Za-z0-9]+)$')
@ -66,8 +68,11 @@ class NotifyRocketChat(NotifyBase):
# A URL that takes you to the setup/help of the specific protocol # A URL that takes you to the setup/help of the specific protocol
setup_url = 'https://github.com/caronc/apprise/wiki/Notify_rocketchat' setup_url = 'https://github.com/caronc/apprise/wiki/Notify_rocketchat'
# Defines the maximum allowable characters in the title # The title is not used
title_maxlen = 200 title_maxlen = 0
# The maximum size of the message
body_maxlen = 200
def __init__(self, recipients=None, **kwargs): def __init__(self, recipients=None, **kwargs):
""" """
@ -106,6 +111,12 @@ class NotifyRocketChat(NotifyBase):
elif not isinstance(recipients, (set, tuple, list)): elif not isinstance(recipients, (set, tuple, list)):
recipients = [] recipients = []
if not (self.user and self.password):
# Username & Password is required for Rocket Chat to work
raise TypeError(
'No Rocket.Chat user/pass combo specified.'
)
# Validate recipients and drop bad ones: # Validate recipients and drop bad ones:
for recipient in recipients: for recipient in recipients:
result = IS_CHANNEL.match(recipient) result = IS_CHANNEL.match(recipient)
@ -133,9 +144,44 @@ class NotifyRocketChat(NotifyBase):
# Used to track token headers upon authentication (if successful) # Used to track token headers upon authentication (if successful)
self.headers = {} self.headers = {}
def notify(self, title, body, notify_type, **kwargs): def url(self):
""" """
wrapper to send_notification since we can alert more then one channel Returns the URL built dynamically based on specified arguments.
"""
# Define any arguments set
args = {
'format': self.notify_format,
'overflow': self.overflow_mode,
}
# Determine Authentication
auth = '{user}:{password}@'.format(
user=self.quote(self.user, safe=''),
password=self.quote(self.password, safe=''),
)
default_port = 443 if self.secure else 80
return '{schema}://{auth}{hostname}{port}/{targets}/?{args}'.format(
schema=self.secure_protocol if self.secure else self.protocol,
auth=auth,
hostname=self.host,
port='' if self.port is None or self.port == default_port
else ':{}'.format(self.port),
targets='/'.join(
[self.quote(x) for x in chain(
# Channels are prefixed with a pound/hashtag symbol
['#{}'.format(x) for x in self.channels],
# Rooms are as is
self.rooms,
)]),
args=self.urlencode(args),
)
def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs):
"""
wrapper to _send since we can alert more then one channel
""" """
# Track whether we authenticated okay # Track whether we authenticated okay
@ -143,8 +189,8 @@ class NotifyRocketChat(NotifyBase):
if not self.login(): if not self.login():
return False return False
# Prepare our message # Prepare our message using the body only
text = '*%s*\r\n%s' % (title.replace('*', '\\*'), body) text = body
# Initiaize our error tracking # Initiaize our error tracking
has_error = False has_error = False
@ -157,7 +203,7 @@ class NotifyRocketChat(NotifyBase):
# Get Channel # Get Channel
channel = channels.pop(0) channel = channels.pop(0)
if not self.send_notification( if not self._send(
{ {
'text': text, 'text': text,
'channel': channel, 'channel': channel,
@ -166,16 +212,12 @@ class NotifyRocketChat(NotifyBase):
# toggle flag # toggle flag
has_error = True has_error = True
if len(channels) + len(rooms) > 0:
# Prevent thrashing requests
self.throttle()
# Send all our defined room id's # Send all our defined room id's
while len(rooms): while len(rooms):
# Get Room # Get Room
room = rooms.pop(0) room = rooms.pop(0)
if not self.send_notification( if not self._send(
{ {
'text': text, 'text': text,
'roomId': room, 'roomId': room,
@ -184,16 +226,12 @@ class NotifyRocketChat(NotifyBase):
# toggle flag # toggle flag
has_error = True has_error = True
if len(rooms) > 0:
# Prevent thrashing requests
self.throttle()
# logout # logout
self.logout() self.logout()
return not has_error return not has_error
def send_notification(self, payload, notify_type, **kwargs): def _send(self, payload, notify_type, **kwargs):
""" """
Perform Notify Rocket.Chat Notification Perform Notify Rocket.Chat Notification
""" """
@ -202,6 +240,10 @@ class NotifyRocketChat(NotifyBase):
self.api_url + 'chat.postMessage', self.verify_certificate, self.api_url + 'chat.postMessage', self.verify_certificate,
)) ))
self.logger.debug('Rocket.Chat Payload: %s' % str(payload)) self.logger.debug('Rocket.Chat Payload: %s' % str(payload))
# Always call throttle before any remote server i/o is made
self.throttle()
try: try:
r = requests.post( r = requests.post(
self.api_url + 'chat.postMessage', self.api_url + 'chat.postMessage',

View File

@ -38,6 +38,7 @@ from json import dumps
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 ..common import NotifyImageSize
from ..common import NotifyType
# Token required as part of the API request # Token required as part of the API request
VALIDATE_TOKEN = re.compile(r'[A-Za-z0-9]{15}') VALIDATE_TOKEN = re.compile(r'[A-Za-z0-9]{15}')
@ -141,7 +142,7 @@ class NotifyRyver(NotifyBase):
re.IGNORECASE, re.IGNORECASE,
) )
def notify(self, title, body, notify_type, **kwargs): def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs):
""" """
Perform Ryver Notification Perform Ryver Notification
""" """
@ -178,6 +179,10 @@ class NotifyRyver(NotifyBase):
url, self.verify_certificate, url, self.verify_certificate,
)) ))
self.logger.debug('Ryver Payload: %s' % str(payload)) self.logger.debug('Ryver Payload: %s' % str(payload))
# Always call throttle before any remote server i/o is made
self.throttle()
try: try:
r = requests.post( r = requests.post(
url, url,
@ -221,6 +226,33 @@ class NotifyRyver(NotifyBase):
return True return True
def url(self):
"""
Returns the URL built dynamically based on specified arguments.
"""
# Define any arguments set
args = {
'format': self.notify_format,
'overflow': self.overflow_mode,
'webhook': self.webhook,
}
# Determine if there is a botname present
botname = ''
if self.user:
botname = '{botname}@'.format(
botname=self.quote(self.user, safe=''),
)
return '{schema}://{botname}{organization}/{token}/?{args}'.format(
schema=self.secure_protocol,
botname=botname,
organization=self.quote(self.organization, safe=''),
token=self.quote(self.token, safe=''),
args=self.urlencode(args),
)
@staticmethod @staticmethod
def parse_url(url): def parse_url(url):
""" """

View File

@ -24,16 +24,17 @@
# THE SOFTWARE. # THE SOFTWARE.
import re import re
import hmac import hmac
import requests import requests
from hashlib import sha256 from hashlib import sha256
from datetime import datetime from datetime import datetime
from collections import OrderedDict from collections import OrderedDict
from xml.etree import ElementTree from xml.etree import ElementTree
from itertools import chain
from .NotifyBase import NotifyBase from .NotifyBase import NotifyBase
from .NotifyBase import HTTP_ERROR_MAP from .NotifyBase import HTTP_ERROR_MAP
from ..common import NotifyType
from ..utils import compat_is_basestring from ..utils import compat_is_basestring
# Some Phone Number Detection # Some Phone Number Detection
@ -58,8 +59,8 @@ LIST_DELIM = re.compile(r'[ \t\r\n,\\/]+')
# region as a delimiter. This is a bit hacky; but it's much easier than having # region as a delimiter. This is a bit hacky; but it's much easier than having
# users of this product search though this Access Key Secret and escape all # users of this product search though this Access Key Secret and escape all
# of the forward slashes! # of the forward slashes!
IS_REGION = re.compile(r'^\s*(?P<country>[a-z]{2})-' IS_REGION = re.compile(
r'(?P<area>[a-z]+)-(?P<no>[0-9]+)\s*$', re.I) r'^\s*(?P<country>[a-z]{2})-(?P<area>[a-z]+)-(?P<no>[0-9]+)\s*$', re.I)
# Extend HTTP Error Messages # Extend HTTP Error Messages
AWS_HTTP_ERROR_MAP = HTTP_ERROR_MAP.copy() AWS_HTTP_ERROR_MAP = HTTP_ERROR_MAP.copy()
@ -85,10 +86,18 @@ class NotifySNS(NotifyBase):
# A URL that takes you to the setup/help of the specific protocol # A URL that takes you to the setup/help of the specific protocol
setup_url = 'https://github.com/caronc/apprise/wiki/Notify_sns' setup_url = 'https://github.com/caronc/apprise/wiki/Notify_sns'
# AWS is pretty good for handling data load so request limits
# can occur in much shorter bursts
request_rate_per_sec = 2.5
# The maximum length of the body # The maximum length of the body
# Source: https://docs.aws.amazon.com/sns/latest/api/API_Publish.html # Source: https://docs.aws.amazon.com/sns/latest/api/API_Publish.html
body_maxlen = 140 body_maxlen = 140
# A title can not be used for SMS Messages. Setting this to zero will
# cause any title (if defined) to get placed into the message body.
title_maxlen = 0
def __init__(self, access_key_id, secret_access_key, region_name, def __init__(self, access_key_id, secret_access_key, region_name,
recipients=None, **kwargs): recipients=None, **kwargs):
""" """
@ -185,7 +194,7 @@ class NotifySNS(NotifyBase):
self.logger.warning( self.logger.warning(
'There are no valid recipient identified to notify.') 'There are no valid recipient identified to notify.')
def notify(self, title, body, notify_type, **kwargs): def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs):
""" """
wrapper to send_notification since we can alert more then one channel wrapper to send_notification since we can alert more then one channel
""" """
@ -214,10 +223,6 @@ class NotifySNS(NotifyBase):
if not result: if not result:
error_count += 1 error_count += 1
if len(phone) > 0:
# Prevent thrashing requests
self.throttle()
# Send all our defined topic id's # Send all our defined topic id's
while len(topics): while len(topics):
@ -256,21 +261,24 @@ class NotifySNS(NotifyBase):
if not result: if not result:
error_count += 1 error_count += 1
if len(topics) > 0:
# Prevent thrashing requests
self.throttle()
return error_count == 0 return error_count == 0
def _post(self, payload, to): def _post(self, payload, to):
""" """
Wrapper to request.post() to manage it's response better and make Wrapper to request.post() to manage it's response better and make
the notify() function cleaner and easier to maintain. the send() function cleaner and easier to maintain.
This function returns True if the _post was successful and False This function returns True if the _post was successful and False
if it wasn't. if it wasn't.
""" """
# Always call throttle before any remote server i/o is made; for AWS
# time plays a huge factor in the headers being sent with the payload.
# So for AWS (SNS) requests we must throttle before they're generated
# and not directly before the i/o call like other notification
# services do.
self.throttle()
# Convert our payload from a dict() into a urlencoded string # Convert our payload from a dict() into a urlencoded string
payload = self.urlencode(payload) payload = self.urlencode(payload)
@ -282,6 +290,7 @@ class NotifySNS(NotifyBase):
self.notify_url, self.verify_certificate, self.notify_url, self.verify_certificate,
)) ))
self.logger.debug('AWS Payload: %s' % str(payload)) self.logger.debug('AWS Payload: %s' % str(payload))
try: try:
r = requests.post( r = requests.post(
self.notify_url, self.notify_url,
@ -521,6 +530,33 @@ class NotifySNS(NotifyBase):
return response return response
def url(self):
"""
Returns the URL built dynamically based on specified arguments.
"""
# Define any arguments set
args = {
'format': self.notify_format,
'overflow': self.overflow_mode,
}
return '{schema}://{key_id}/{key_secret}/{region}/{targets}/'\
'?{args}'.format(
schema=self.secure_protocol,
key_id=self.quote(self.aws_access_key_id, safe=''),
key_secret=self.quote(self.aws_secret_access_key, safe=''),
region=self.quote(self.aws_region_name, safe=''),
targets='/'.join(
[self.quote(x) for x in chain(
# Phone # are prefixed with a plus symbol
['+{}'.format(x) for x in self.phone],
# Topics are prefixed with a pound/hashtag symbol
['#{}'.format(x) for x in self.topics],
)]),
args=self.urlencode(args),
)
@staticmethod @staticmethod
def parse_url(url): def parse_url(url):
""" """

View File

@ -43,6 +43,7 @@ from time import time
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 ..common import NotifyImageSize
from ..common import NotifyType
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
@ -141,7 +142,6 @@ class NotifySlack(NotifyBase):
if not self.user: if not self.user:
self.logger.warning( self.logger.warning(
'No user was specified; using %s.' % SLACK_DEFAULT_USER) 'No user was specified; using %s.' % SLACK_DEFAULT_USER)
self.user = SLACK_DEFAULT_USER
if compat_is_basestring(channels): if compat_is_basestring(channels):
self.channels = [x for x in filter(bool, CHANNEL_LIST_DELIM.split( self.channels = [x for x in filter(bool, CHANNEL_LIST_DELIM.split(
@ -175,7 +175,7 @@ class NotifySlack(NotifyBase):
re.IGNORECASE, re.IGNORECASE,
) )
def notify(self, title, body, notify_type, **kwargs): def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs):
""" """
Perform Slack Notification Perform Slack Notification
""" """
@ -231,7 +231,7 @@ class NotifySlack(NotifyBase):
# prepare JSON Object # prepare JSON Object
payload = { payload = {
'channel': _channel, 'channel': _channel,
'username': self.user, 'username': self.user if self.user else SLACK_DEFAULT_USER,
# Use Markdown language # Use Markdown language
'mrkdwn': True, 'mrkdwn': True,
'attachments': [{ 'attachments': [{
@ -251,6 +251,9 @@ class NotifySlack(NotifyBase):
url, self.verify_certificate, url, self.verify_certificate,
)) ))
self.logger.debug('Slack Payload: %s' % str(payload)) self.logger.debug('Slack Payload: %s' % str(payload))
# Always call throttle before any remote server i/o is made
self.throttle()
try: try:
r = requests.post( r = requests.post(
url, url,
@ -275,7 +278,7 @@ class NotifySlack(NotifyBase):
channel, channel,
r.status_code)) r.status_code))
# self.logger.debug('Response Details: %s' % r.raw.read()) # self.logger.debug('Response Details: %s' % r.content)
# Return; we're done # Return; we're done
notify_okay = False notify_okay = False
@ -291,12 +294,38 @@ class NotifySlack(NotifyBase):
self.logger.debug('Socket Exception: %s' % str(e)) self.logger.debug('Socket Exception: %s' % str(e))
notify_okay = False notify_okay = False
if len(channels):
# Prevent thrashing requests
self.throttle()
return notify_okay return notify_okay
def url(self):
"""
Returns the URL built dynamically based on specified arguments.
"""
# Define any arguments set
args = {
'format': self.notify_format,
'overflow': self.overflow_mode,
}
# Determine if there is a botname present
botname = ''
if self.user:
botname = '{botname}@'.format(
botname=self.quote(self.user, safe=''),
)
return '{schema}://{botname}{token_a}/{token_b}/{token_c}/{targets}/'\
'?{args}'.format(
schema=self.secure_protocol,
botname=botname,
token_a=self.quote(self.token_a, safe=''),
token_b=self.quote(self.token_b, safe=''),
token_c=self.quote(self.token_c, safe=''),
targets='/'.join(
[self.quote(x, safe='') for x in self.channels]),
args=self.urlencode(args),
)
@staticmethod @staticmethod
def parse_url(url): def parse_url(url):
""" """

View File

@ -59,10 +59,11 @@ from json import dumps
from .NotifyBase import NotifyBase from .NotifyBase import NotifyBase
from .NotifyBase import HTTP_ERROR_MAP from .NotifyBase import HTTP_ERROR_MAP
from ..common import NotifyType
from ..common import NotifyImageSize from ..common import NotifyImageSize
from ..utils import compat_is_basestring
from ..utils import parse_bool
from ..common import NotifyFormat from ..common import NotifyFormat
from ..utils import parse_bool
from ..utils import parse_list
TELEGRAM_IMAGE_XY = NotifyImageSize.XY_256 TELEGRAM_IMAGE_XY = NotifyImageSize.XY_256
@ -81,9 +82,6 @@ IS_CHAT_ID_RE = re.compile(
re.IGNORECASE, re.IGNORECASE,
) )
# Used to break path apart into list of chat identifiers
CHAT_ID_LIST_DELIM = re.compile(r'[ \t\r\n,#\\/]+')
class NotifyTelegram(NotifyBase): class NotifyTelegram(NotifyBase):
""" """
@ -134,16 +132,8 @@ class NotifyTelegram(NotifyBase):
# Store our Bot Token # Store our Bot Token
self.bot_token = result.group('key') self.bot_token = result.group('key')
if compat_is_basestring(chat_ids): # Parse our list
self.chat_ids = [x for x in filter(bool, CHAT_ID_LIST_DELIM.split( self.chat_ids = parse_list(chat_ids)
chat_ids,
))]
elif isinstance(chat_ids, (set, tuple, list)):
self.chat_ids = list(chat_ids)
else:
self.chat_ids = list()
if self.user: if self.user:
# Treat this as a channel too # Treat this as a channel too
@ -153,7 +143,7 @@ class NotifyTelegram(NotifyBase):
_id = self.detect_bot_owner() _id = self.detect_bot_owner()
if _id: if _id:
# Store our id # Store our id
self.chat_ids = [str(_id)] self.chat_ids.append(str(_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.')
@ -336,7 +326,7 @@ class NotifyTelegram(NotifyBase):
return 0 return 0
def notify(self, title, body, notify_type, **kwargs): def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs):
""" """
Perform Telegram Notification Perform Telegram Notification
""" """
@ -424,14 +414,14 @@ class NotifyTelegram(NotifyBase):
# ID # ID
payload['chat_id'] = int(chat_id.group('idno')) payload['chat_id'] = int(chat_id.group('idno'))
# Always call throttle before any remote server i/o is made;
# Telegram throttles to occur before sending the image so that
# content can arrive together.
self.throttle()
if self.include_image is True: if self.include_image is True:
# Send an image # Send an image
if self.send_image( self.send_image(payload['chat_id'], notify_type)
payload['chat_id'], notify_type) is not None:
# We sent a post (whether we were successful or not)
# we still hit the remote server... just throttle
# before our next hit server query
self.throttle()
self.logger.debug('Telegram POST URL: %s (cert_verify=%r)' % ( self.logger.debug('Telegram POST URL: %s (cert_verify=%r)' % (
url, self.verify_certificate, url, self.verify_certificate,
@ -494,13 +484,28 @@ 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
finally:
if len(chat_ids):
# Prevent thrashing requests
self.throttle()
return not has_error return not has_error
def url(self):
"""
Returns the URL built dynamically based on specified arguments.
"""
# Define any arguments set
args = {
'format': self.notify_format,
'overflow': self.overflow_mode,
}
# No need to check the user token because the user automatically gets
# appended into the list of chat ids
return '{schema}://{bot_token}/{targets}/?{args}'.format(
schema=self.secure_protocol,
bot_token=self.quote(self.bot_token, safe=''),
targets='/'.join(
[self.quote('@{}'.format(x)) for x in self.chat_ids]),
args=self.urlencode(args))
@staticmethod @staticmethod
def parse_url(url): def parse_url(url):
""" """
@ -512,17 +517,17 @@ class NotifyTelegram(NotifyBase):
# 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.
# This hack searches for this bogus URL and corrects it # This hack searches for this bogus URL and corrects it so we can
# so we can properly load it further down. The other # properly load it further down. The other alternative is to ask users
# alternative is to ask users to actually change the colon # to actually change the colon into a slash (which will work too), but
# into a slash (which will work too), but it's more likely # it's more likely to cause confusion... So this is the next best thing
# to cause confusion... So this is the next best thing # we also check for %3A (incase the URL is encoded) as %3A == :
try: try:
tgram = re.match( tgram = re.match(
r'(?P<protocol>%s://)(bot)?(?P<prefix>([a-z0-9_-]+)' r'(?P<protocol>{schema}://)(bot)?(?P<prefix>([a-z0-9_-]+)'
r'(:[a-z0-9_-]+)?@)?(?P<btoken_a>[0-9]+):+' r'(:[a-z0-9_-]+)?@)?(?P<btoken_a>[0-9]+)(:|%3A)+'
r'(?P<remaining>.*)$' % NotifyTelegram.secure_protocol, r'(?P<remaining>.*)$'.format(
url, re.I) schema=NotifyTelegram.secure_protocol), url, re.I)
except (TypeError, AttributeError): except (TypeError, AttributeError):
# url is bad; force tgram to be None # url is bad; force tgram to be None
@ -534,14 +539,11 @@ class NotifyTelegram(NotifyBase):
if tgram.group('prefix'): if tgram.group('prefix'):
# Try again # Try again
results = 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'), tgram.group('btoken_a'),
tgram.group('btoken_a'), tgram.group('remaining')))
tgram.group('remaining'),
),
)
else: else:
# Try again # Try again
@ -562,9 +564,8 @@ class NotifyTelegram(NotifyBase):
bot_token = '%s:%s' % (bot_token_a, bot_token_b) bot_token = '%s:%s' % (bot_token_a, bot_token_b)
chat_ids = ','.join( chat_ids = [x for x in filter(
[x for x in filter( bool, NotifyBase.split_path(results['fullpath']))][1:]
bool, NotifyBase.split_path(results['fullpath']))][1:])
# Store our bot token # Store our bot token
results['bot_token'] = bot_token results['bot_token'] = bot_token

View File

@ -25,6 +25,7 @@
from . import tweepy from . import tweepy
from ..NotifyBase import NotifyBase from ..NotifyBase import NotifyBase
from ...common import NotifyType
class NotifyTwitter(NotifyBase): class NotifyTwitter(NotifyBase):
@ -50,6 +51,9 @@ class NotifyTwitter(NotifyBase):
# which are limited to 240 characters) # which are limited to 240 characters)
body_maxlen = 4096 body_maxlen = 4096
# Twitter does have titles when creating a message
title_maxlen = 0
def __init__(self, ckey, csecret, akey, asecret, **kwargs): def __init__(self, ckey, csecret, akey, asecret, **kwargs):
""" """
Initialize Twitter Object Initialize Twitter Object
@ -90,7 +94,7 @@ class NotifyTwitter(NotifyBase):
return return
def notify(self, title, body, notify_type, **kwargs): def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs):
""" """
Perform Twitter Notification Perform Twitter Notification
""" """
@ -109,13 +113,16 @@ class NotifyTwitter(NotifyBase):
) )
return False return False
text = '%s\r\n%s' % (title, body) # Always call throttle before any remote server i/o is made to avoid
# thrashing the remote server and risk being blocked.
self.throttle()
try: try:
# Get our API # Get our API
api = tweepy.API(self.auth) api = tweepy.API(self.auth)
# Send our Direct Message # Send our Direct Message
api.send_direct_message(self.user, text=text) api.send_direct_message(self.user, text=body)
self.logger.info('Sent Twitter DM notification.') self.logger.info('Sent Twitter DM notification.')
except Exception as e: except Exception as e:

View File

@ -26,11 +26,11 @@
from __future__ import absolute_import from __future__ import absolute_import
from __future__ import print_function from __future__ import print_function
import re
from time import sleep from time import sleep
from .NotifyBase import NotifyBase from .NotifyBase import NotifyBase
from ..common import NotifyImageSize from ..common import NotifyImageSize
from ..common import NotifyType
# Default our global support flag # Default our global support flag
NOTIFY_WINDOWS_SUPPORT_ENABLED = False NOTIFY_WINDOWS_SUPPORT_ENABLED = False
@ -64,9 +64,17 @@ class NotifyWindows(NotifyBase):
# A URL that takes you to the setup/help of the specific protocol # A URL that takes you to the setup/help of the specific protocol
setup_url = 'https://github.com/caronc/apprise/wiki/Notify_windows' setup_url = 'https://github.com/caronc/apprise/wiki/Notify_windows'
# Disable throttle rate for Windows requests since they are normally
# local anyway
request_rate_per_sec = 0
# Allows the user to specify the NotifyImageSize object # Allows the user to specify the NotifyImageSize object
image_size = NotifyImageSize.XY_128 image_size = NotifyImageSize.XY_128
# Limit results to just the first 2 line otherwise there is just to much
# content to display
body_max_line_count = 2
# This entry is a bit hacky, but it allows us to unit-test this library # This entry is a bit hacky, but it allows us to unit-test this library
# in an environment that simply doesn't have the windows packages # in an environment that simply doesn't have the windows packages
# available to us. It also allows us to handle situations where the # available to us. It also allows us to handle situations where the
@ -100,7 +108,7 @@ class NotifyWindows(NotifyBase):
return None return None
def notify(self, title, body, notify_type, **kwargs): def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs):
""" """
Perform Windows Notification Perform Windows Notification
""" """
@ -110,11 +118,8 @@ class NotifyWindows(NotifyBase):
"Windows Notifications are not supported by this system.") "Windows Notifications are not supported by this system.")
return False return False
# Limit results to just the first 2 line otherwise # Always call throttle before any remote server i/o is made
# there is just to much content to display self.throttle()
body = re.split('[\r\n]+', body)
body[0] = body[0].strip('#').strip()
body = '\r\n'.join(body[0:2])
try: try:
# Register destruction callback # Register destruction callback
@ -168,13 +173,20 @@ class NotifyWindows(NotifyBase):
self.logger.info('Sent Windows notification.') self.logger.info('Sent Windows notification.')
except Exception as e: except Exception:
self.logger.warning('Failed to send Windows notification.') self.logger.warning('Failed to send Windows notification.')
self.logger.exception('Windows Exception') self.logger.exception('Windows Exception')
return False return False
return True return True
def url(self):
"""
Returns the URL built dynamically based on specified arguments.
"""
return '{schema}://'.format(schema=self.protocol)
@staticmethod @staticmethod
def parse_url(url): def parse_url(url):
""" """

View File

@ -23,7 +23,6 @@
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE. # THE SOFTWARE.
import re
import requests import requests
from json import dumps from json import dumps
@ -44,15 +43,28 @@ class NotifyXBMC(NotifyBase):
# The services URL # The services URL
service_url = 'http://kodi.tv/' service_url = 'http://kodi.tv/'
xbmc_protocol = 'xbmc'
xbmc_secure_protocol = 'xbmcs'
kodi_protocol = 'kodi'
kodi_secure_protocol = 'kodis'
# The default protocols # The default protocols
protocol = ('xbmc', 'kodi') protocol = (xbmc_protocol, kodi_protocol)
# The default secure protocols # The default secure protocols
secure_protocol = ('xbmc', 'kodis') secure_protocol = (xbmc_secure_protocol, kodi_secure_protocol)
# A URL that takes you to the setup/help of the specific protocol # A URL that takes you to the setup/help of the specific protocol
setup_url = 'https://github.com/caronc/apprise/wiki/Notify_kodi' setup_url = 'https://github.com/caronc/apprise/wiki/Notify_kodi'
# Disable throttle rate for XBMC/KODI requests since they are normally
# local anyway
request_rate_per_sec = 0
# Limit results to just the first 2 line otherwise there is just to much
# content to display
body_max_line_count = 2
# XBMC uses the http protocol with JSON requests # XBMC uses the http protocol with JSON requests
xbmc_default_port = 8080 xbmc_default_port = 8080
@ -149,17 +161,11 @@ class NotifyXBMC(NotifyBase):
return (self.headers, dumps(payload)) return (self.headers, dumps(payload))
def notify(self, title, body, notify_type, **kwargs): def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs):
""" """
Perform XBMC/KODI Notification Perform XBMC/KODI Notification
""" """
# Limit results to just the first 2 line otherwise
# there is just to much content to display
body = re.split('[\r\n]+', body)
body[0] = body[0].strip('#').strip()
body = '\r\n'.join(body[0:2])
if self.protocol == self.xbmc_remote_protocol: if self.protocol == self.xbmc_remote_protocol:
# XBMC v2.0 # XBMC v2.0
(headers, payload) = self._payload_20( (headers, payload) = self._payload_20(
@ -184,6 +190,10 @@ class NotifyXBMC(NotifyBase):
url, self.verify_certificate, url, self.verify_certificate,
)) ))
self.logger.debug('XBMC/KODI Payload: %s' % str(payload)) self.logger.debug('XBMC/KODI Payload: %s' % str(payload))
# Always call throttle before any remote server i/o is made
self.throttle()
try: try:
r = requests.post( r = requests.post(
url, url,
@ -224,6 +234,45 @@ class NotifyXBMC(NotifyBase):
return True return True
def url(self):
"""
Returns the URL built dynamically based on specified arguments.
"""
# Define any arguments set
args = {
'format': self.notify_format,
'overflow': self.overflow_mode,
}
# Determine Authentication
auth = ''
if self.user and self.password:
auth = '{user}:{password}@'.format(
user=self.quote(self.user, safe=''),
password=self.quote(self.password, safe=''),
)
elif self.user:
auth = '{user}@'.format(
user=self.quote(self.user, safe=''),
)
default_schema = self.xbmc_protocol if (
self.protocol <= self.xbmc_remote_protocol) else self.kodi_protocol
default_port = 443 if self.secure else self.xbmc_default_port
if self.secure:
# Append 's' to schema
default_schema + 's'
return '{schema}://{auth}{hostname}{port}/?{args}'.format(
schema=default_schema,
auth=auth,
hostname=self.host,
port='' if not self.port or self.port == default_port
else ':{}'.format(self.port),
args=self.urlencode(args),
)
@staticmethod @staticmethod
def parse_url(url): def parse_url(url):
""" """

View File

@ -29,6 +29,7 @@ import requests
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 ..common import NotifyImageSize
from ..common import NotifyType
from ..utils import compat_is_basestring from ..utils import compat_is_basestring
@ -52,9 +53,17 @@ class NotifyXML(NotifyBase):
# Allows the user to specify the NotifyImageSize object # Allows the user to specify the NotifyImageSize object
image_size = NotifyImageSize.XY_128 image_size = NotifyImageSize.XY_128
def __init__(self, **kwargs): # Disable throttle rate for JSON requests since they are normally
# local anyway
request_rate_per_sec = 0
def __init__(self, headers=None, **kwargs):
""" """
Initialize XML Object Initialize XML Object
headers can be a dictionary of key/value pairs that you want to
additionally include as part of the server headers to post with
""" """
super(NotifyXML, self).__init__(**kwargs) super(NotifyXML, self).__init__(**kwargs)
@ -83,9 +92,51 @@ class NotifyXML(NotifyBase):
if not compat_is_basestring(self.fullpath): if not compat_is_basestring(self.fullpath):
self.fullpath = '/' self.fullpath = '/'
self.headers = {}
if headers:
# Store our extra headers
self.headers.update(headers)
return return
def notify(self, title, body, notify_type, **kwargs): def url(self):
"""
Returns the URL built dynamically based on specified arguments.
"""
# Define any arguments set
args = {
'format': self.notify_format,
'overflow': self.overflow_mode,
}
# Append our headers into our args
args.update({'+{}'.format(k): v for k, v in self.headers.items()})
# Determine Authentication
auth = ''
if self.user and self.password:
auth = '{user}:{password}@'.format(
user=self.quote(self.user, safe=''),
password=self.quote(self.password, safe=''),
)
elif self.user:
auth = '{user}@'.format(
user=self.quote(self.user, safe=''),
)
default_port = 443 if self.secure else 80
return '{schema}://{auth}{hostname}{port}/?{args}'.format(
schema=self.secure_protocol if self.secure else self.protocol,
auth=auth,
hostname=self.host,
port='' if self.port is None or self.port == default_port
else ':{}'.format(self.port),
args=self.urlencode(args),
)
def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs):
""" """
Perform XML Notification Perform XML Notification
""" """
@ -96,8 +147,8 @@ class NotifyXML(NotifyBase):
'Content-Type': 'application/xml' 'Content-Type': 'application/xml'
} }
if self.headers: # Apply any/all header over-rides defined
headers.update(self.headers) headers.update(self.headers)
re_map = { re_map = {
'{MESSAGE_TYPE}': NotifyBase.quote(notify_type), '{MESSAGE_TYPE}': NotifyBase.quote(notify_type),
@ -126,6 +177,10 @@ class NotifyXML(NotifyBase):
url, self.verify_certificate, url, self.verify_certificate,
)) ))
self.logger.debug('XML Payload: %s' % str(payload)) self.logger.debug('XML Payload: %s' % str(payload))
# Always call throttle before any remote server i/o is made
self.throttle()
try: try:
r = requests.post( r = requests.post(
url, url,
@ -163,3 +218,23 @@ class NotifyXML(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 as we couldn't load the results
return results
# Add our headers that the user can potentially over-ride if they wish
# to to our returned result set
results['headers'] = results['qsd-']
results['headers'].update(results['qsd+'])
return results

View File

@ -32,14 +32,12 @@ try:
from urllib import unquote from urllib import unquote
from urllib import quote from urllib import quote
from urlparse import urlparse from urlparse import urlparse
from urlparse import parse_qsl
except ImportError: except ImportError:
# Python 3.x # Python 3.x
from urllib.parse import unquote from urllib.parse import unquote
from urllib.parse import quote from urllib.parse import quote
from urllib.parse import urlparse from urllib.parse import urlparse
from urllib.parse import parse_qsl
import logging import logging
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -91,6 +89,12 @@ TIDY_NUX_TRIM_RE = re.compile(
), ),
) )
# The handling of custom arguments passed in the URL; we treat any
# argument (which would otherwise appear in the qsd area of our parse_url()
# function differently if they start with a + or - value
NOTIFY_CUSTOM_ADD_TOKENS = re.compile(r'^( |\+)(?P<key>.*)\s*')
NOTIFY_CUSTOM_DEL_TOKENS = re.compile(r'^-(?P<key>.*)\s*')
# Used for attempting to acquire the schema if the URL can't be parsed. # Used for attempting to acquire the schema if the URL can't be parsed.
GET_SCHEMA_RE = re.compile(r'\s*(?P<schema>[a-z0-9]{2,9})://.*$', re.I) GET_SCHEMA_RE = re.compile(r'\s*(?P<schema>[a-z0-9]{2,9})://.*$', re.I)
@ -143,6 +147,81 @@ def tidy_path(path):
return path return path
def parse_qsd(qs):
"""
Query String Dictionary Builder
A custom implimentation of the parse_qsl() function already provided
by Python. This function is slightly more light weight and gives us
more control over parsing out arguments such as the plus/+ symbol
at the head of a key/value pair.
qs should be a query string part made up as part of the URL such as
a=1&c=2&d=
a=1 gets interpreted as { 'a': '1' }
a= gets interpreted as { 'a': '' }
a gets interpreted as { 'a': '' }
This function returns a result object that fits with the apprise
expected parameters (populating the 'qsd' portion of the dictionary
"""
# Our return result set:
result = {
# The arguments passed in (the parsed query). This is in a dictionary
# of {'key': 'val', etc }. Keys are all made lowercase before storing
# to simplify access to them.
'qsd': {},
# Detected Entries that start with + or - are additionally stored in
# these values (un-touched). The +/- however are stripped from their
# name before they are stored here.
'qsd+': {},
'qsd-': {},
}
pairs = [s2 for s1 in qs.split('&') for s2 in s1.split(';')]
for name_value in pairs:
nv = name_value.split('=', 1)
# Handle case of a control-name with no equal sign
if len(nv) != 2:
nv.append('')
# Apprise keys can start with a + symbol; so we need to skip over
# the very first entry
key = '{}{}'.format(
'' if len(nv[0]) == 0 else nv[0][0],
'' if len(nv[0]) <= 1 else nv[0][1:].replace('+', ' '),
)
key = unquote(key)
key = '' if not key else key
val = nv[1].replace('+', ' ')
val = unquote(val)
val = '' if not val else val.strip()
# Always Query String Dictionary (qsd) for every entry we have
# content is always made lowercase for easy indexing
result['qsd'][key.lower().strip()] = val
# Check for tokens that start with a addition/plus symbol (+)
k = NOTIFY_CUSTOM_ADD_TOKENS.match(key)
if k is not None:
# Store content 'as-is'
result['qsd+'][k.group('key')] = val
# Check for tokens that start with a subtraction/hyphen symbol (-)
k = NOTIFY_CUSTOM_DEL_TOKENS.match(key)
if k is not None:
# Store content 'as-is'
result['qsd-'][k.group('key')] = val
return result
def parse_url(url, default_schema='http', verify_host=True): 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.
@ -190,10 +269,17 @@ def parse_url(url, default_schema='http', verify_host=True):
'schema': None, 'schema': None,
# The schema # The schema
'url': None, 'url': None,
# The arguments passed in (the parsed query) # The arguments passed in (the parsed query). This is in a dictionary
# This is in a dictionary of {'key': 'val', etc } # of {'key': 'val', etc }. Keys are all made lowercase before storing
# to simplify access to them.
# qsd = Query String Dictionary # qsd = Query String Dictionary
'qsd': {} 'qsd': {},
# Detected Entries that start with + or - are additionally stored in
# these values (un-touched). The +/- however are stripped from their
# name before they are stored here.
'qsd+': {},
'qsd-': {},
} }
qsdata = '' qsdata = ''
@ -220,6 +306,11 @@ def parse_url(url, default_schema='http', verify_host=True):
# No qsdata # No qsdata
pass pass
# Parse Query Arugments ?val=key&key=val
# while ensuring that all keys are lowercase
if qsdata:
result.update(parse_qsd(qsdata))
# Now do a proper extraction of data # Now do a proper extraction of data
parsed = urlparse('http://%s' % host) parsed = urlparse('http://%s' % host)
@ -231,6 +322,7 @@ def parse_url(url, default_schema='http', verify_host=True):
return None return None
result['fullpath'] = quote(unquote(tidy_path(parsed[2].strip()))) result['fullpath'] = quote(unquote(tidy_path(parsed[2].strip())))
try: try:
# Handle trailing slashes removed by tidy_path # Handle trailing slashes removed by tidy_path
if result['fullpath'][-1] not in ('/', '\\') and \ if result['fullpath'][-1] not in ('/', '\\') and \
@ -242,16 +334,6 @@ def parse_url(url, default_schema='http', verify_host=True):
# and therefore, no trailing slash # and therefore, no trailing slash
pass pass
# Parse Query Arugments ?val=key&key=val
# while ensureing that all keys are lowercase
if qsdata:
result['qsd'] = dict([(k.lower().strip(), v.strip())
for k, v in parse_qsl(
qsdata,
keep_blank_values=True,
strict_parsing=False,
)])
if not result['fullpath']: if not result['fullpath']:
# Default # Default
result['fullpath'] = None result['fullpath'] = None
@ -397,6 +479,10 @@ def parse_list(*args):
elif isinstance(arg, (set, list, tuple)): elif isinstance(arg, (set, list, tuple)):
result += parse_list(*arg) result += parse_list(*arg)
elif arg is None:
# Ignore
continue
else: else:
# Convert whatever it is to a string and work with it # Convert whatever it is to a string and work with it
result += parse_list(str(arg)) result += parse_list(str(arg))

View File

@ -11,6 +11,12 @@ exclude = .eggs,.tox,gntp,tweepy,pushjet
ignore = E722,W503,W504 ignore = E722,W503,W504
statistics = true statistics = true
[flake8]
# We exclude packages we don't maintain
exclude = .eggs,.tox,gntp,tweepy,pushjet
ignore = E722,W503,W504
statistics = true
[aliases] [aliases]
test=pytest test=pytest

View File

@ -71,15 +71,34 @@ def test_apprise():
a = Apprise(servers=servers) a = Apprise(servers=servers)
# 3 servers loaded # 2 servers loaded
assert(len(a) == 2) assert(len(a) == 2)
# We can retrieve our URLs this way:
assert(len(a.urls()) == 2)
# We can add another server # We can add another server
assert( assert(
a.add('mmosts://mattermost.server.local/' a.add('mmosts://mattermost.server.local/'
'3ccdd113474722377935511fc85d3dd4') is True) '3ccdd113474722377935511fc85d3dd4') is True)
assert(len(a) == 3) assert(len(a) == 3)
# We can pop an object off of our stack by it's indexed value:
obj = a.pop(0)
assert(isinstance(obj, NotifyBase) is True)
assert(len(a) == 2)
# We can retrieve elements from our list too by reference:
assert(compat_is_basestring(a[0].url()) is True)
# We can iterate over our list too:
count = 0
for o in a:
assert(compat_is_basestring(o.url()) is True)
count += 1
# verify that we did indeed iterate over each element
assert(len(a) == count)
# We can empty our set # We can empty our set
a.clear() a.clear()
assert(len(a) == 0) assert(len(a) == 0)

View File

@ -26,6 +26,7 @@
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.utils import compat_is_basestring
from apprise.plugins import NotifyEmailBase from apprise.plugins import NotifyEmailBase
import smtplib import smtplib
@ -49,7 +50,7 @@ TEST_URLS = (
# No Username # No Username
('mailtos://:pass@nuxref.com:567', { ('mailtos://:pass@nuxref.com:567', {
# Can't prepare a To address using this expression # Can't prepare a To address using this expression
'exception': TypeError, 'instance': TypeError,
}), }),
# Pre-Configured Email Services # Pre-Configured Email Services
@ -115,27 +116,27 @@ TEST_URLS = (
}), }),
# Invalid From Address # Invalid From Address
('mailtos://user:pass@nuxref.com?from=@', { ('mailtos://user:pass@nuxref.com?from=@', {
'exception': TypeError, 'instance': TypeError,
}), }),
# Invalid From Address # Invalid From Address
('mailtos://nuxref.com?user=&pass=.', { ('mailtos://nuxref.com?user=&pass=.', {
'exception': TypeError, 'instance': TypeError,
}), }),
# Invalid To Address # Invalid To Address
('mailtos://user:pass@nuxref.com?to=@', { ('mailtos://user:pass@nuxref.com?to=@', {
'exception': TypeError, 'instance': TypeError,
}), }),
# Valid URL, but can't structure a proper email # Valid URL, but can't structure a proper email
('mailtos://nuxref.com?user=%20!&pass=.', { ('mailtos://nuxref.com?user=%20!&pass=.', {
'exception': TypeError, 'instance': TypeError,
}), }),
# Invalid From (and To) Address # Invalid From (and To) Address
('mailtos://nuxref.com?to=test', { ('mailtos://nuxref.com?to=test', {
'exception': TypeError, 'instance': TypeError,
}), }),
# Invalid Secure Mode # Invalid Secure Mode
('mailtos://user:pass@example.com?mode=notamode', { ('mailtos://user:pass@example.com?mode=notamode', {
'exception': TypeError, 'instance': TypeError,
}), }),
# STARTTLS flag checking # STARTTLS flag checking
('mailtos://user:pass@gmail.com?mode=starttls', { ('mailtos://user:pass@gmail.com?mode=starttls', {
@ -165,6 +166,8 @@ def test_email_plugin(mock_smtp, mock_smtpssl):
API: NotifyEmail Plugin() API: NotifyEmail Plugin()
""" """
# Disable Throttling to speed testing
plugins.NotifyBase.NotifyBase.request_rate_per_sec = 0
# iterate over our dictionary and test it out # iterate over our dictionary and test it out
for (url, meta) in TEST_URLS: for (url, meta) in TEST_URLS:
@ -172,9 +175,6 @@ def test_email_plugin(mock_smtp, mock_smtpssl):
# Our expected instance # Our expected instance
instance = meta.get('instance', None) instance = meta.get('instance', None)
# Our expected exception
exception = meta.get('exception', None)
# Our expected server objects # Our expected server objects
self = meta.get('self', None) self = meta.get('self', None)
@ -217,19 +217,37 @@ def test_email_plugin(mock_smtp, mock_smtpssl):
try: try:
obj = Apprise.instantiate(url, suppress_exceptions=False) obj = Apprise.instantiate(url, suppress_exceptions=False)
assert(exception is None)
if obj is None: if obj is None:
# We're done # We're done (assuming this is what we were expecting)
assert instance is None
continue continue
if instance is None: if instance is None:
# Expected None but didn't get it # Expected None but didn't get it
print('%s instantiated %s' % (url, str(obj))) print('%s instantiated %s (but expected None)' % (
url, str(obj)))
assert(False) assert(False)
assert(isinstance(obj, instance)) assert(isinstance(obj, instance))
if isinstance(obj, plugins.NotifyBase.NotifyBase):
# We loaded okay; now lets make sure we can reverse this url
assert(compat_is_basestring(obj.url()) is True)
# Instantiate the exact same object again using the URL from
# the one that was already created properly
obj_cmp = Apprise.instantiate(obj.url())
# Our object should be the same instance as what we had
# originally expected above.
if not isinstance(obj_cmp, plugins.NotifyBase.NotifyBase):
# Assert messages are hard to trace back with the way
# these tests work. Just printing before throwing our
# assertion failure makes things easier to debug later on
print('TEST FAIL: {} regenerated as {}'.format(
url, obj.url()))
assert(False)
if self: if self:
# Iterate over our expected entries inside of our object # Iterate over our expected entries inside of our object
for key, val in self.items(): for key, val in self.items():
@ -256,18 +274,19 @@ def test_email_plugin(mock_smtp, mock_smtpssl):
# Don't mess with these entries # Don't mess with these entries
raise raise
except Exception as e: except Exception:
# We can't handle this exception type # We can't handle this exception type
print('%s / %s' % (url, str(e))) raise
assert False
except AssertionError: except AssertionError:
# Don't mess with these entries # Don't mess with these entries
print('%s AssertionError' % url)
raise raise
except Exception as e: except Exception as e:
# Check that we were expecting this exception to happen # Check that we were expecting this exception to happen
assert isinstance(e, response) if not isinstance(e, response):
raise
except AssertionError: except AssertionError:
# Don't mess with these entries # Don't mess with these entries
@ -276,9 +295,11 @@ def test_email_plugin(mock_smtp, mock_smtpssl):
except Exception as e: except Exception as e:
# Handle our exception # Handle our exception
print('%s / %s' % (url, str(e))) if(instance is None):
assert(exception is not None) raise
assert(isinstance(e, exception))
if not isinstance(e, instance):
raise
@mock.patch('smtplib.SMTP') @mock.patch('smtplib.SMTP')
@ -323,34 +344,23 @@ def test_smtplib_init_fail(mock_smtplib):
API: Test exception handling when calling smtplib.SMTP() API: Test exception handling when calling smtplib.SMTP()
""" """
# Disable Throttling to speed testing
plugins.NotifyBase.NotifyBase.request_rate_per_sec = 0
obj = Apprise.instantiate( obj = Apprise.instantiate(
'mailto://user:pass@gmail.com', suppress_exceptions=False) 'mailto://user:pass@gmail.com', suppress_exceptions=False)
assert(isinstance(obj, plugins.NotifyEmail)) assert(isinstance(obj, plugins.NotifyEmail))
# Support Exception handling of smtplib.SMTP # Support Exception handling of smtplib.SMTP
mock_smtplib.side_effect = TypeError('Test') mock_smtplib.side_effect = RuntimeError('Test')
try: assert obj.notify(
obj.notify( body='body', title='test', notify_type=NotifyType.INFO) is False
title='test', body='body',
notify_type=NotifyType.INFO)
# We should have thrown an exception
assert False
except TypeError:
# Exception thrown as expected
assert True
except Exception:
# Un-Expected
assert False
# A handled and expected exception # A handled and expected exception
mock_smtplib.side_effect = smtplib.SMTPException('Test') mock_smtplib.side_effect = smtplib.SMTPException('Test')
assert obj.notify(title='test', body='body', assert obj.notify(
notify_type=NotifyType.INFO) is False body='body', title='test', notify_type=NotifyType.INFO) is False
@mock.patch('smtplib.SMTP') @mock.patch('smtplib.SMTP')
@ -359,6 +369,8 @@ def test_smtplib_send_okay(mock_smtplib):
API: Test a successfully sent email API: Test a successfully sent email
""" """
# Disable Throttling to speed testing
plugins.NotifyBase.NotifyBase.request_rate_per_sec = 0
# Defaults to HTML # Defaults to HTML
obj = Apprise.instantiate( obj = Apprise.instantiate(
@ -372,7 +384,7 @@ def test_smtplib_send_okay(mock_smtplib):
mock_smtplib.quit.return_value = True mock_smtplib.quit.return_value = True
assert(obj.notify( assert(obj.notify(
title='test', body='body', notify_type=NotifyType.INFO) is True) body='body', title='test', notify_type=NotifyType.INFO) is True)
# Set Text # Set Text
obj = Apprise.instantiate( obj = Apprise.instantiate(
@ -380,4 +392,4 @@ def test_smtplib_send_okay(mock_smtplib):
assert(isinstance(obj, plugins.NotifyEmail)) assert(isinstance(obj, plugins.NotifyEmail))
assert(obj.notify( assert(obj.notify(
title='test', body='body', notify_type=NotifyType.INFO) is True) body='body', title='test', notify_type=NotifyType.INFO) is True)

View File

@ -28,6 +28,7 @@ import mock
import sys import sys
import types import types
import apprise import apprise
from apprise.utils import compat_is_basestring
try: try:
# Python v3.4+ # Python v3.4+
@ -223,6 +224,9 @@ def test_dbus_plugin(mock_mainloop, mock_byte, mock_bytearray,
assert(isinstance(obj, apprise.plugins.NotifyDBus) is True) assert(isinstance(obj, apprise.plugins.NotifyDBus) is True)
obj.duration = 0 obj.duration = 0
# Test url() call
assert(compat_is_basestring(obj.url()) is True)
# Our notification succeeds even though the gi library was not loaded # Our notification succeeds even though the gi library was not loaded
assert(obj.notify(title='title', body='body', assert(obj.notify(title='title', body='body',
notify_type=apprise.NotifyType.INFO) is True) notify_type=apprise.NotifyType.INFO) is True)

View File

@ -28,6 +28,7 @@ import sys
import types import types
import apprise import apprise
from apprise.utils import compat_is_basestring
try: try:
# Python v3.4+ # Python v3.4+
@ -113,6 +114,9 @@ def test_gnome_plugin():
# Check that it found our mocked environments # Check that it found our mocked environments
assert(obj._enabled is True) assert(obj._enabled is True)
# Test url() call
assert(compat_is_basestring(obj.url()) is True)
# test notifications # test notifications
assert(obj.notify(title='title', body='body', assert(obj.notify(title='title', body='body',
notify_type=apprise.NotifyType.INFO) is True) notify_type=apprise.NotifyType.INFO) is True)

View File

@ -26,8 +26,9 @@
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.utils import compat_is_basestring
import mock import mock
import re
TEST_URLS = ( TEST_URLS = (
@ -193,9 +194,8 @@ def test_growl_plugin(mock_gntp):
# This is the response we expect # This is the response we expect
assert True assert True
except Exception as e: except Exception:
# We can't handle this exception type # We can't handle this exception type
print('%s / %s' % (url, str(e)))
assert False assert False
# We're done this part of the test # We're done this part of the test
@ -216,10 +216,27 @@ def test_growl_plugin(mock_gntp):
if instance is None: if instance is None:
# Expected None but didn't get it # Expected None but didn't get it
print('%s instantiated %s' % (url, str(obj)))
assert(False) assert(False)
assert(isinstance(obj, instance)) assert(isinstance(obj, instance) is True)
if isinstance(obj, plugins.NotifyBase.NotifyBase):
# We loaded okay; now lets make sure we can reverse this url
assert(compat_is_basestring(obj.url()) is True)
# Instantiate the exact same object again using the URL from
# the one that was already created properly
obj_cmp = Apprise.instantiate(obj.url())
# Our object should be the same instance as what we had
# originally expected above.
if not isinstance(obj_cmp, plugins.NotifyBase.NotifyBase):
# Assert messages are hard to trace back with the way
# these tests work. Just printing before throwing our
# assertion failure makes things easier to debug later on
print('TEST FAIL: {} regenerated as {}'.format(
url, obj.url()))
assert(False)
if self: if self:
# Iterate over our expected entries inside of our object # Iterate over our expected entries inside of our object

View File

@ -23,6 +23,9 @@
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE. # THE SOFTWARE.
from datetime import datetime
from datetime import timedelta
from apprise.plugins.NotifyBase import NotifyBase from apprise.plugins.NotifyBase import NotifyBase
from apprise import NotifyType from apprise import NotifyType
from apprise import NotifyImageSize from apprise import NotifyImageSize
@ -45,6 +48,15 @@ def test_notify_base():
except TypeError: except TypeError:
assert(True) assert(True)
# invalid types throw exceptions
try:
nb = NotifyBase(**{'overflow': 'invalid'})
# We should never reach here as an exception should be thrown
assert(False)
except TypeError:
assert(True)
# Bad port information # Bad port information
nb = NotifyBase(port='invalid') nb = NotifyBase(port='invalid')
assert nb.port is None assert nb.port is None
@ -52,9 +64,29 @@ def test_notify_base():
nb = NotifyBase(port=10) nb = NotifyBase(port=10)
assert nb.port == 10 assert nb.port == 10
try:
nb.url()
assert False
except NotImplementedError:
# Each sub-module is that inherits this as a parent is required to
# over-ride this function. So direct calls to this throws a not
# implemented error intentionally
assert True
try:
nb.send('test message')
assert False
except NotImplementedError:
# Each sub-module is that inherits this as a parent is required to
# over-ride this function. So direct calls to this throws a not
# implemented error intentionally
assert True
# Throttle overrides.. # Throttle overrides..
nb = NotifyBase() nb = NotifyBase()
nb.throttle_attempt = 0.0 nb.request_rate_per_sec = 0.0
start_time = default_timer() start_time = default_timer()
nb.throttle() nb.throttle()
elapsed = default_timer() - start_time elapsed = default_timer() - start_time
@ -63,13 +95,57 @@ def test_notify_base():
# then other # then other
assert elapsed < 0.5 assert elapsed < 0.5
# Concurrent calls should achieve the same response
start_time = default_timer() start_time = default_timer()
nb.throttle(1.0) nb.throttle()
elapsed = default_timer() - start_time
assert elapsed < 0.5
nb = NotifyBase()
nb.request_rate_per_sec = 1.0
# Set our time to now
start_time = default_timer()
nb.throttle()
elapsed = default_timer() - start_time
# A first call to throttle (Without telling it a time previously ran) does
# not block for any length of time; it just merely sets us up for
# concurrent calls to block
assert elapsed < 0.5
# Concurrent calls could take up to the rate_per_sec though...
start_time = default_timer()
nb.throttle(last_io=datetime.now())
elapsed = default_timer() - start_time
assert elapsed > 0.5 and elapsed < 1.5
nb = NotifyBase()
nb.request_rate_per_sec = 1.0
# Set our time to now
start_time = default_timer()
nb.throttle(last_io=datetime.now())
elapsed = default_timer() - start_time
# because we told it that we had already done a previous action (now)
# the throttle holds out until the right time has passed
assert elapsed > 0.5 and elapsed < 1.5
# Concurrent calls could take up to the rate_per_sec though...
start_time = default_timer()
nb.throttle(last_io=datetime.now())
elapsed = default_timer() - start_time
assert elapsed > 0.5 and elapsed < 1.5
nb = NotifyBase()
start_time = default_timer()
nb.request_rate_per_sec = 1.0
# Force a time in the past
nb.throttle(last_io=(datetime.now() - timedelta(seconds=20)))
elapsed = default_timer() - start_time elapsed = default_timer() - start_time
# Should be a very fast response time since we set it to zero but we'll # Should be a very fast response time since we set it to zero but we'll
# check for less then 500 to be fair as some testing systems may be slower # check for less then 500 to be fair as some testing systems may be slower
# then other # then other
assert elapsed < 1.5 assert elapsed < 0.5
# our NotifyBase wasn't initialized with an ImageSize so this will fail # our NotifyBase wasn't initialized with an ImageSize so this will fail
assert nb.image_url(notify_type=NotifyType.INFO) is None assert nb.image_url(notify_type=NotifyType.INFO) is None
@ -166,11 +242,30 @@ def test_notify_base_urls():
assert 'password' in results assert 'password' in results
assert results['password'] == "newpassword" assert results['password'] == "newpassword"
# pass headers # Options
results = NotifyBase.parse_url( results = NotifyBase.parse_url('https://localhost?format=invalid')
'https://localhost:8080?-HeaderKey=HeaderValue') assert 'format' not in results
assert 'headerkey' in results['headers'] results = NotifyBase.parse_url('https://localhost?format=text')
assert results['headers']['headerkey'] == 'HeaderValue' assert 'format' in results
assert results['format'] == 'text'
results = NotifyBase.parse_url('https://localhost?format=markdown')
assert 'format' in results
assert results['format'] == 'markdown'
results = NotifyBase.parse_url('https://localhost?format=html')
assert 'format' in results
assert results['format'] == 'html'
results = NotifyBase.parse_url('https://localhost?overflow=invalid')
assert 'overflow' not in results
results = NotifyBase.parse_url('https://localhost?overflow=upstream')
assert 'overflow' in results
assert results['overflow'] == 'upstream'
results = NotifyBase.parse_url('https://localhost?overflow=split')
assert 'overflow' in results
assert results['overflow'] == 'split'
results = NotifyBase.parse_url('https://localhost?overflow=truncate')
assert 'overflow' in results
assert results['overflow'] == 'truncate'
# User Handling # User Handling

View File

@ -26,6 +26,8 @@
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.utils import compat_is_basestring
import mock import mock
TEST_URLS = ( TEST_URLS = (
@ -104,13 +106,37 @@ def test_plugin(mock_refresh, mock_send):
try: try:
obj = Apprise.instantiate(url, suppress_exceptions=False) obj = Apprise.instantiate(url, suppress_exceptions=False)
if instance is None: if obj is None:
# Check that we got what we came for # We're done (assuming this is what we were expecting)
assert obj is instance assert instance is None
continue continue
if instance is None:
# Expected None but didn't get it
print('%s instantiated %s (but expected None)' % (
url, str(obj)))
assert(False)
assert(isinstance(obj, instance)) assert(isinstance(obj, instance))
if isinstance(obj, plugins.NotifyBase.NotifyBase):
# We loaded okay; now lets make sure we can reverse this url
assert(compat_is_basestring(obj.url()) is True)
# Instantiate the exact same object again using the URL from
# the one that was already created properly
obj_cmp = Apprise.instantiate(obj.url())
# Our object should be the same instance as what we had
# originally expected above.
if not isinstance(obj_cmp, plugins.NotifyBase.NotifyBase):
# Assert messages are hard to trace back with the way
# these tests work. Just printing before throwing our
# assertion failure makes things easier to debug later on
print('TEST FAIL: {} regenerated as {}'.format(
url, obj.url()))
assert(False)
if self: if self:
# Iterate over our expected entries inside of our object # Iterate over our expected entries inside of our object
for key, val in self.items(): for key, val in self.items():
@ -142,23 +168,29 @@ def test_plugin(mock_refresh, mock_send):
# Don't mess with these entries # Don't mess with these entries
raise raise
except Exception as e: except Exception:
# We can't handle this exception type # We can't handle this exception type
assert False raise
except AssertionError: except AssertionError:
# Don't mess with these entries # Don't mess with these entries
print('%s AssertionError' % url)
raise raise
except Exception as e: except Exception as e:
# Check that we were expecting this exception to happen # Check that we were expecting this exception to happen
assert isinstance(e, response) if not isinstance(e, response):
raise
except AssertionError: except AssertionError:
# Don't mess with these entries # Don't mess with these entries
print('%s AssertionError' % url)
raise raise
except Exception as e: except Exception as e:
# Handle our exception # Handle our exception
assert(instance is not None) if(instance is None):
assert(isinstance(e, instance)) raise
if not isinstance(e, instance):
raise

File diff suppressed because it is too large Load Diff

View File

@ -303,6 +303,8 @@ def test_aws_topic_handling(mock_post):
API: NotifySNS Plugin() AWS Topic Handling API: NotifySNS Plugin() AWS Topic Handling
""" """
# Disable Throttling to speed testing
plugins.NotifySNS.request_rate_per_sec = 0
arn_response = \ arn_response = \
""" """
@ -336,9 +338,6 @@ def test_aws_topic_handling(mock_post):
# Assign ourselves a new function # Assign ourselves a new function
mock_post.side_effect = post mock_post.side_effect = post
# Disable Throttling to speed testing
plugins.NotifyBase.NotifyBase.throttle_attempt = 0
# Create our object # Create our object
a = Apprise() a = Apprise()

View File

@ -35,6 +35,26 @@ except ImportError:
from apprise import utils from apprise import utils
def test_parse_qsd():
"utils: parse_qsd() testing """
result = utils.parse_qsd('a=1&b=&c&d=abcd')
assert(isinstance(result, dict) is True)
assert(len(result) == 3)
assert 'qsd' in result
assert 'qsd+' in result
assert 'qsd-' in result
assert(len(result['qsd']) == 4)
assert 'a' in result['qsd']
assert 'b' in result['qsd']
assert 'c' in result['qsd']
assert 'd' in result['qsd']
assert(len(result['qsd-']) == 0)
assert(len(result['qsd+']) == 0)
def test_parse_url(): def test_parse_url():
"utils: parse_url() testing """ "utils: parse_url() testing """
@ -49,6 +69,8 @@ def test_parse_url():
assert(result['query'] is None) assert(result['query'] is None)
assert(result['url'] == 'http://hostname') assert(result['url'] == 'http://hostname')
assert(result['qsd'] == {}) assert(result['qsd'] == {})
assert(result['qsd-'] == {})
assert(result['qsd+'] == {})
result = utils.parse_url('http://hostname/') result = utils.parse_url('http://hostname/')
assert(result['schema'] == 'http') assert(result['schema'] == 'http')
@ -61,6 +83,8 @@ def test_parse_url():
assert(result['query'] is None) assert(result['query'] is None)
assert(result['url'] == 'http://hostname/') assert(result['url'] == 'http://hostname/')
assert(result['qsd'] == {}) assert(result['qsd'] == {})
assert(result['qsd-'] == {})
assert(result['qsd+'] == {})
result = utils.parse_url('hostname') result = utils.parse_url('hostname')
assert(result['schema'] == 'http') assert(result['schema'] == 'http')
@ -73,6 +97,61 @@ def test_parse_url():
assert(result['query'] is None) assert(result['query'] is None)
assert(result['url'] == 'http://hostname') assert(result['url'] == 'http://hostname')
assert(result['qsd'] == {}) assert(result['qsd'] == {})
assert(result['qsd-'] == {})
assert(result['qsd+'] == {})
result = utils.parse_url('http://hostname/?-KeY=Value')
assert(result['schema'] == 'http')
assert(result['host'] == 'hostname')
assert(result['port'] is None)
assert(result['user'] is None)
assert(result['password'] is None)
assert(result['fullpath'] == '/')
assert(result['path'] == '/')
assert(result['query'] is None)
assert(result['url'] == 'http://hostname/')
assert('-key' in result['qsd'])
assert(unquote(result['qsd']['-key']) == 'Value')
assert('KeY' in result['qsd-'])
assert(unquote(result['qsd-']['KeY']) == 'Value')
assert(result['qsd+'] == {})
result = utils.parse_url('http://hostname/?+KeY=Value')
assert(result['schema'] == 'http')
assert(result['host'] == 'hostname')
assert(result['port'] is None)
assert(result['user'] is None)
assert(result['password'] is None)
assert(result['fullpath'] == '/')
assert(result['path'] == '/')
assert(result['query'] is None)
assert(result['url'] == 'http://hostname/')
assert('+key' in result['qsd'])
assert('KeY' in result['qsd+'])
assert(result['qsd+']['KeY'] == 'Value')
assert(result['qsd-'] == {})
result = utils.parse_url(
'http://hostname/?+KeY=ValueA&-kEy=ValueB&KEY=Value%20+C')
assert(result['schema'] == 'http')
assert(result['host'] == 'hostname')
assert(result['port'] is None)
assert(result['user'] is None)
assert(result['password'] is None)
assert(result['fullpath'] == '/')
assert(result['path'] == '/')
assert(result['query'] is None)
assert(result['url'] == 'http://hostname/')
assert('+key' in result['qsd'])
assert('-key' in result['qsd'])
assert('key' in result['qsd'])
assert('KeY' in result['qsd+'])
assert(result['qsd+']['KeY'] == 'ValueA')
assert('kEy' in result['qsd-'])
assert(result['qsd-']['kEy'] == 'ValueB')
assert(result['qsd']['key'] == 'Value C')
assert(result['qsd']['+key'] == result['qsd+']['KeY'])
assert(result['qsd']['-key'] == result['qsd-']['kEy'])
result = utils.parse_url('http://hostname////') result = utils.parse_url('http://hostname////')
assert(result['schema'] == 'http') assert(result['schema'] == 'http')
@ -85,6 +164,8 @@ def test_parse_url():
assert(result['query'] is None) assert(result['query'] is None)
assert(result['url'] == 'http://hostname/') assert(result['url'] == 'http://hostname/')
assert(result['qsd'] == {}) assert(result['qsd'] == {})
assert(result['qsd-'] == {})
assert(result['qsd+'] == {})
result = utils.parse_url('http://hostname:40////') result = utils.parse_url('http://hostname:40////')
assert(result['schema'] == 'http') assert(result['schema'] == 'http')
@ -97,6 +178,8 @@ def test_parse_url():
assert(result['query'] is None) assert(result['query'] is None)
assert(result['url'] == 'http://hostname:40/') assert(result['url'] == 'http://hostname:40/')
assert(result['qsd'] == {}) assert(result['qsd'] == {})
assert(result['qsd-'] == {})
assert(result['qsd+'] == {})
result = utils.parse_url('HTTP://HoStNaMe:40/test.php') result = utils.parse_url('HTTP://HoStNaMe:40/test.php')
assert(result['schema'] == 'http') assert(result['schema'] == 'http')
@ -109,6 +192,8 @@ def test_parse_url():
assert(result['query'] == 'test.php') assert(result['query'] == 'test.php')
assert(result['url'] == 'http://HoStNaMe:40/test.php') assert(result['url'] == 'http://HoStNaMe:40/test.php')
assert(result['qsd'] == {}) assert(result['qsd'] == {})
assert(result['qsd-'] == {})
assert(result['qsd+'] == {})
result = utils.parse_url('HTTPS://user@hostname/test.py') result = utils.parse_url('HTTPS://user@hostname/test.py')
assert(result['schema'] == 'https') assert(result['schema'] == 'https')
@ -121,6 +206,8 @@ def test_parse_url():
assert(result['query'] == 'test.py') assert(result['query'] == 'test.py')
assert(result['url'] == 'https://user@hostname/test.py') assert(result['url'] == 'https://user@hostname/test.py')
assert(result['qsd'] == {}) assert(result['qsd'] == {})
assert(result['qsd-'] == {})
assert(result['qsd+'] == {})
result = utils.parse_url(' HTTPS://///user@@@hostname///test.py ') result = utils.parse_url(' HTTPS://///user@@@hostname///test.py ')
assert(result['schema'] == 'https') assert(result['schema'] == 'https')
@ -133,6 +220,8 @@ def test_parse_url():
assert(result['query'] == 'test.py') assert(result['query'] == 'test.py')
assert(result['url'] == 'https://user@hostname/test.py') assert(result['url'] == 'https://user@hostname/test.py')
assert(result['qsd'] == {}) assert(result['qsd'] == {})
assert(result['qsd-'] == {})
assert(result['qsd+'] == {})
result = utils.parse_url( result = utils.parse_url(
'HTTPS://user:password@otherHost/full///path/name/', 'HTTPS://user:password@otherHost/full///path/name/',
@ -147,6 +236,8 @@ def test_parse_url():
assert(result['query'] is None) assert(result['query'] is None)
assert(result['url'] == 'https://user:password@otherHost/full/path/name/') assert(result['url'] == 'https://user:password@otherHost/full/path/name/')
assert(result['qsd'] == {}) assert(result['qsd'] == {})
assert(result['qsd-'] == {})
assert(result['qsd+'] == {})
# Handle garbage # Handle garbage
assert(utils.parse_url(None) is None) assert(utils.parse_url(None) is None)
@ -173,6 +264,8 @@ def test_parse_url():
assert(unquote(result['qsd']['from']) == 'test@test.com') assert(unquote(result['qsd']['from']) == 'test@test.com')
assert('format' in result['qsd']) assert('format' in result['qsd'])
assert(unquote(result['qsd']['format']) == 'text') assert(unquote(result['qsd']['format']) == 'text')
assert(result['qsd-'] == {})
assert(result['qsd+'] == {})
# Test Passwords with question marks ?; not supported # Test Passwords with question marks ?; not supported
result = utils.parse_url( result = utils.parse_url(
@ -194,6 +287,8 @@ def test_parse_url():
assert(result['query'] is None) assert(result['query'] is None)
assert(result['url'] == 'http://nuxref.com') assert(result['url'] == 'http://nuxref.com')
assert(result['qsd'] == {}) assert(result['qsd'] == {})
assert(result['qsd-'] == {})
assert(result['qsd+'] == {})
# just host and path # just host and path
result = utils.parse_url( result = utils.parse_url(
@ -209,6 +304,8 @@ def test_parse_url():
assert(result['query'] == 'host') assert(result['query'] == 'host')
assert(result['url'] == 'http://invalid/host') assert(result['url'] == 'http://invalid/host')
assert(result['qsd'] == {}) assert(result['qsd'] == {})
assert(result['qsd-'] == {})
assert(result['qsd+'] == {})
# just all out invalid # just all out invalid
assert(utils.parse_url('?') is None) assert(utils.parse_url('?') is None)
@ -227,6 +324,8 @@ def test_parse_url():
assert(result['query'] is None) assert(result['query'] is None)
assert(result['url'] == 'http://nuxref.com') assert(result['url'] == 'http://nuxref.com')
assert(result['qsd'] == {}) assert(result['qsd'] == {})
assert(result['qsd-'] == {})
assert(result['qsd+'] == {})
def test_parse_bool(): def test_parse_bool():

View File

@ -29,6 +29,7 @@ import types
# Rebuild our Apprise environment # Rebuild our Apprise environment
import apprise import apprise
from apprise.utils import compat_is_basestring
try: try:
# Python v3.4+ # Python v3.4+
@ -107,6 +108,9 @@ def test_windows_plugin():
obj = apprise.Apprise.instantiate('windows://', suppress_exceptions=False) obj = apprise.Apprise.instantiate('windows://', suppress_exceptions=False)
obj.duration = 0 obj.duration = 0
# Test URL functionality
assert(compat_is_basestring(obj.url()) is True)
# Check that it found our mocked environments # Check that it found our mocked environments
assert(obj._enabled is True) assert(obj._enabled is True)

32
tox.ini
View File

@ -10,36 +10,45 @@ setenv =
deps= deps=
-r{toxinidir}/requirements.txt -r{toxinidir}/requirements.txt
-r{toxinidir}/dev-requirements.txt -r{toxinidir}/dev-requirements.txt
commands = python -m pytest {posargs} commands =
coverage run --parallel -m pytest {posargs}
flake8 . --count --show-source --statistics
[testenv:py27] [testenv:py27]
deps= deps=
dbus-python dbus-python
-r{toxinidir}/requirements.txt -r{toxinidir}/requirements.txt
-r{toxinidir}/dev-requirements.txt -r{toxinidir}/dev-requirements.txt
commands = coverage run --parallel -m pytest {posargs} commands =
coverage run --parallel -m pytest {posargs}
flake8 . --count --show-source --statistics
[testenv:py34] [testenv:py34]
deps= deps=
dbus-python dbus-python
-r{toxinidir}/requirements.txt -r{toxinidir}/requirements.txt
-r{toxinidir}/dev-requirements.txt -r{toxinidir}/dev-requirements.txt
commands = coverage run --parallel -m pytest {posargs} commands =
coverage run --parallel -m pytest {posargs}
flake8 . --count --show-source --statistics
[testenv:py35] [testenv:py35]
deps= deps=
dbus-python dbus-python
-r{toxinidir}/requirements.txt -r{toxinidir}/requirements.txt
-r{toxinidir}/dev-requirements.txt -r{toxinidir}/dev-requirements.txt
commands = coverage run --parallel -m pytest {posargs} commands =
coverage run --parallel -m pytest {posargs}
flake8 . --count --show-source --statistics
[testenv:py36] [testenv:py36]
deps= deps=
dbus-python dbus-python
-r{toxinidir}/requirements.txt -r{toxinidir}/requirements.txt
-r{toxinidir}/dev-requirements.txt -r{toxinidir}/dev-requirements.txt
commands = coverage run --parallel -m pytest {posargs} commands =
coverage run --parallel -m pytest {posargs}
flake8 . --count --show-source --statistics
[testenv:py37] [testenv:py37]
deps= deps=
@ -47,21 +56,24 @@ deps=
-r{toxinidir}/requirements.txt -r{toxinidir}/requirements.txt
-r{toxinidir}/dev-requirements.txt -r{toxinidir}/dev-requirements.txt
commands = commands =
flake8 . --count --select=E901,E999,F821,F822,F823 --show-source --statistics
coverage run --parallel -m pytest {posargs} coverage run --parallel -m pytest {posargs}
flake8 . --count --show-source --statistics
[testenv:pypy] [testenv:pypy]
deps= deps=
-r{toxinidir}/requirements.txt -r{toxinidir}/requirements.txt
-r{toxinidir}/dev-requirements.txt -r{toxinidir}/dev-requirements.txt
commands = coverage run --parallel -m pytest {posargs} commands =
coverage run --parallel -m pytest {posargs}
flake8 . --count --show-source --statistics
[testenv:pypy3] [testenv:pypy3]
deps= deps=
-r{toxinidir}/requirements.txt -r{toxinidir}/requirements.txt
-r{toxinidir}/dev-requirements.txt -r{toxinidir}/dev-requirements.txt
commands = coverage run --parallel -m pytest {posargs} commands =
coverage run --parallel -m pytest {posargs}
flake8 . --count --show-source --statistics
[testenv:coverage-report] [testenv:coverage-report]
deps = coverage deps = coverage