diff --git a/.travis.yml b/.travis.yml
index 46f1bcc6..c7d4cc6c 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -26,7 +26,7 @@ matrix:
install:
- pip install .
- - pip install codecov tox
+ - pip install codecov
- pip install -r dev-requirements.txt
- pip install -r requirements.txt
- if [[ $TRAVIS_PYTHON_VERSION != 'pypy'* ]]; then travis_retry pip install dbus-python; fi
diff --git a/README.md b/README.md
index 3127f114..348cc0aa 100644
--- a/README.md
+++ b/README.md
@@ -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
| [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
growl://hostname:portno
growl://password@hostname
growl://password@hostname:port**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
ifttt://webhooksID/EventToTrigger/Value1/Value2/Value3
ifttt://webhooksID/EventToTrigger/?Value3=NewEntry&Value2=AnotherValue
+| [IFTTT](https://github.com/caronc/apprise/wiki/Notify_ifttt) | ifttt:// | (TCP) 443 | ifttt://webhooksID/Event
ifttt://webhooksID/Event1/Event2/EventN
ifttt://webhooksID/Event1/?+Key=Value
ifttt://webhooksID/Event1/?-Key=value1
| [Join](https://github.com/caronc/apprise/wiki/Notify_join) | join:// | (TCP) 443 | join://apikey/device
join://apikey/device1/device2/deviceN/
join://apikey/group
join://apikey/groupA/groupB/groupN
join://apikey/DeviceA/groupA/groupN/DeviceN/
| [KODI](https://github.com/caronc/apprise/wiki/Notify_kodi) | kodi:// or kodis:// | (TCP) 8080 or 443 | kodi://hostname
kodi://user@hostname
kodi://user:password@hostname:port
| [Matrix](https://github.com/caronc/apprise/wiki/Notify_matrix) | matrix:// or matrixs:// | (TCP) 80 or 443 | matrix://token
matrix://user@token
matrixs://token?mode=slack
matrixs://user@token
@@ -110,8 +110,8 @@ apobj.add('pbul://o.gn5kj6nfhv736I7jC3cj3QLRiyhgl98b')
# Then notify these services any time you desire. The below would
# notify all of the services loaded into our Apprise object.
apobj.notify(
- title='my notification title',
body='what a great notification service!',
+ title='my notification title',
)
```
diff --git a/apprise/Apprise.py b/apprise/Apprise.py
index 576f1d54..467826dd 100644
--- a/apprise/Apprise.py
+++ b/apprise/Apprise.py
@@ -171,7 +171,7 @@ class Apprise(object):
# URL information
plugin = SCHEMA_MAP[results['schema']](**results)
- except:
+ except Exception:
# the arguments are invalid or can not be used.
logger.error('Could not load URL: %s' % url)
return None
@@ -238,7 +238,7 @@ class Apprise(object):
"""
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):
"""
Send a notification to all of the plugins previously loaded.
@@ -366,8 +366,8 @@ class Apprise(object):
try:
# Send notification
if not server.notify(
- title=title,
body=conversion_map[server.notify_format],
+ title=title,
notify_type=notify_type):
# Toggle our return status flag
@@ -375,7 +375,6 @@ class Apprise(object):
except TypeError:
# These our our internally thrown notifications
- # TODO: Change this to a custom one such as AppriseNotifyError
status = False
except Exception:
@@ -432,6 +431,33 @@ class Apprise(object):
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):
"""
Returns the number of servers loaded
diff --git a/apprise/__init__.py b/apprise/__init__.py
index cf9e034e..674f68e7 100644
--- a/apprise/__init__.py
+++ b/apprise/__init__.py
@@ -37,6 +37,8 @@ from .common import NotifyImageSize
from .common import NOTIFY_IMAGE_SIZES
from .common import NotifyFormat
from .common import NOTIFY_FORMATS
+from .common import OverflowMode
+from .common import OVERFLOW_MODES
from .plugins.NotifyBase import NotifyBase
from .Apprise import Apprise
@@ -52,6 +54,6 @@ __all__ = [
'Apprise', 'AppriseAsset', 'NotifyBase',
# Reference
- 'NotifyType', 'NotifyImageSize', 'NotifyFormat', 'NOTIFY_TYPES',
- 'NOTIFY_IMAGE_SIZES', 'NOTIFY_FORMATS',
+ 'NotifyType', 'NotifyImageSize', 'NotifyFormat', 'OverflowMode',
+ 'NOTIFY_TYPES', 'NOTIFY_IMAGE_SIZES', 'NOTIFY_FORMATS', 'OVERFLOW_MODES',
]
diff --git a/apprise/common.py b/apprise/common.py
index 834c1928..75fcd481 100644
--- a/apprise/common.py
+++ b/apprise/common.py
@@ -77,3 +77,31 @@ NOTIFY_FORMATS = (
NotifyFormat.HTML,
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,
+)
diff --git a/apprise/plugins/NotifyBase.py b/apprise/plugins/NotifyBase.py
index a8471f83..ac6df799 100644
--- a/apprise/plugins/NotifyBase.py
+++ b/apprise/plugins/NotifyBase.py
@@ -26,6 +26,8 @@
import re
import logging
from time import sleep
+from datetime import datetime
+
try:
# Python 2.7
from urllib import unquote as _unquote
@@ -42,9 +44,12 @@ from ..utils import parse_url
from ..utils import parse_bool
from ..utils import parse_list
from ..utils import is_hostname
+from ..common import NotifyType
from ..common import NOTIFY_TYPES
from ..common import NotifyFormat
from ..common import NOTIFY_FORMATS
+from ..common import OverflowMode
+from ..common import OVERFLOW_MODES
from ..AppriseAsset import AppriseAsset
@@ -52,13 +57,6 @@ from ..AppriseAsset import AppriseAsset
from xml.sax.saxutils import escape as sax_escape
-def _escape(text):
- """
- saxutil escape tool
- """
- return sax_escape(text, {"'": "'", "\"": """})
-
-
HTTP_ERROR_MAP = {
400: 'Bad Request - Unsupported Parameters.',
401: 'Verification Failed.',
@@ -113,21 +111,33 @@ class NotifyBase(object):
setup_url = None
# Most Servers do not like more then 1 request per 5 seconds, so 5.5 gives
- # us a safe play range...
- throttle_attempt = 5.5
+ # us a safe play range.
+ request_rate_per_sec = 5.5
# Allows the user to specify the NotifyImageSize object
image_size = None
# 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
+ # 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
notify_format = NotifyFormat.TEXT
+ # Default Overflow Mode
+ overflow_mode = OverflowMode.UPSTREAM
+
# Maintain a set of tags to associate with this specific notification
tags = set()
@@ -162,7 +172,6 @@ class NotifyBase(object):
self.user = kwargs.get('user')
self.password = kwargs.get('password')
- self.headers = kwargs.get('headers')
if 'format' in kwargs:
# Store the specified format if specified
@@ -177,25 +186,64 @@ class NotifyBase(object):
# Provide override
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:
# We want to associate some tags with our notification service.
# the code below gets the 'tag' argument if defined, otherwise
# it just falls back to whatever was already defined globally
self.tags = set(parse_list(kwargs.get('tag', self.tags)))
- 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
"""
- self.logger.debug('Throttling...')
- throttle_time = throttle_time \
- if throttle_time is not None else self.throttle_attempt
+ if last_io is not None:
+ # Assume specified last_io
+ self._last_io_datetime = last_io
- # Perform throttle
- if throttle_time > 0:
- sleep(throttle_time)
+ # Get ourselves a reference time of 'now'
+ reference = datetime.now()
+ 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
def image_url(self, notify_type, logo=False, extension=None):
@@ -260,6 +308,117 @@ class NotifyBase(object):
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):
"""
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
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:
# nothing more to do; return object as is
return html
- escaped = _escape(html)
+ # Escape HTML
+ escaped = sax_escape(html, {"'": "'", "\"": """})
if whitespace:
# Tidy up whitespace too
@@ -311,8 +479,25 @@ class NotifyBase(object):
@staticmethod
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:
return ''
@@ -327,9 +512,25 @@ class NotifyBase(object):
@staticmethod
def quote(content, safe='/', encoding=None, errors=None):
- """
- common quote function
+ """ Replaces single character non-ascii characters and URI specific
+ 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:
return ''
@@ -344,26 +545,60 @@ class NotifyBase(object):
@staticmethod
def urlencode(query, doseq=False, safe='', encoding=None, errors=None):
- """
- common urlencode function
+ """Convert a mapping object or a sequence of two-element tuples
+ 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:
# Python v3.x
return _urlencode(
- query, doseq=doseq, safe=safe, encoding=encoding,
+ _query, doseq=doseq, safe=safe, encoding=encoding,
errors=errors)
except TypeError:
# Python v2.7
- return _urlencode(query)
+ return _urlencode(_query)
@staticmethod
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:
return PATHSPLIT_LIST_DELIM.split(
NotifyBase.unquote(path).lstrip('/'))
@@ -371,26 +606,51 @@ class NotifyBase(object):
@staticmethod
def is_email(address):
- """
- Returns True if specified entry is an email address
+ """Determine if the 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
@staticmethod
def is_hostname(hostname):
- """
- Returns True if specified entry is a hostname
+ """Determine if the 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)
@staticmethod
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(
url, default_schema='unknown', verify_host=verify_host)
@@ -417,6 +677,15 @@ class NotifyBase(object):
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
if 'pass' in results['qsd']:
results['password'] = results['qsd']['pass']
@@ -425,6 +694,4 @@ class NotifyBase(object):
if 'user' in results['qsd']:
results['user'] = results['qsd']['user']
- results['headers'] = {k[1:]: v for k, v in results['qsd'].items()
- if re.match(r'^-.', k)}
return results
diff --git a/apprise/plugins/NotifyBoxcar.py b/apprise/plugins/NotifyBoxcar.py
index 58891b76..cfdb968a 100644
--- a/apprise/plugins/NotifyBoxcar.py
+++ b/apprise/plugins/NotifyBoxcar.py
@@ -23,12 +23,13 @@
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
-from json import dumps
-import requests
import re
-from time import time
+import requests
import hmac
+from json import dumps
+from time import time
from hashlib import sha1
+from itertools import chain
try:
from urlparse import urlparse
@@ -37,7 +38,7 @@ except ImportError:
from .NotifyBase import NotifyBase
from .NotifyBase import HTTP_ERROR_MAP
-
+from ..common import NotifyType
from ..common import NotifyImageSize
from ..utils import compat_is_basestring
@@ -168,7 +169,7 @@ class NotifyBoxcar(NotifyBase):
'(%s) specified.' % recipient,
)
- def notify(self, title, body, notify_type, **kwargs):
+ def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs):
"""
Perform Boxcar Notification
"""
@@ -229,6 +230,9 @@ class NotifyBoxcar(NotifyBase):
))
self.logger.debug('Boxcar Payload: %s' % str(payload))
+ # Always call throttle before any remote server i/o is made
+ self.throttle()
+
try:
r = requests.post(
notify_url,
@@ -272,6 +276,27 @@ class NotifyBoxcar(NotifyBase):
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
def parse_url(url):
"""
diff --git a/apprise/plugins/NotifyDBus.py b/apprise/plugins/NotifyDBus.py
index 0517f1b6..a9740d24 100644
--- a/apprise/plugins/NotifyDBus.py
+++ b/apprise/plugins/NotifyDBus.py
@@ -26,10 +26,9 @@
from __future__ import absolute_import
from __future__ import print_function
-import re
-
from .NotifyBase import NotifyBase
from ..common import NotifyImageSize
+from ..common import NotifyType
from ..utils import GET_SCHEMA_RE
# 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
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
image_size = NotifyImageSize.XY_128
# The number of seconds to keep the message present for
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
# in an environment that simply doesn't have the gnome packages
# 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.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
"""
@@ -249,16 +259,10 @@ class NotifyDBus(NotifyBase):
"Could not load Gnome notification icon ({}): {}"
.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:
+ # Always call throttle() before any remote execution is made
+ self.throttle()
+
dbus_iface.Notify(
# Application Identifier
self.app_id,
@@ -280,13 +284,20 @@ class NotifyDBus(NotifyBase):
self.logger.info('Sent DBus notification.')
- except Exception as e:
+ except Exception:
self.logger.warning('Failed to send DBus notification.')
self.logger.exception('DBus Exception')
return False
return True
+ def url(self):
+ """
+ Returns the URL built dynamically based on specified arguments.
+ """
+
+ return '{schema}://'.format(schema=self.schema)
+
@staticmethod
def parse_url(url):
"""
diff --git a/apprise/plugins/NotifyDiscord.py b/apprise/plugins/NotifyDiscord.py
index 7cdf283c..c2384105 100644
--- a/apprise/plugins/NotifyDiscord.py
+++ b/apprise/plugins/NotifyDiscord.py
@@ -48,6 +48,7 @@ from .NotifyBase import NotifyBase
from .NotifyBase import HTTP_ERROR_MAP
from ..common import NotifyImageSize
from ..common import NotifyFormat
+from ..common import NotifyType
from ..utils import parse_bool
@@ -113,7 +114,7 @@ class NotifyDiscord(NotifyBase):
return
- def notify(self, title, body, notify_type, **kwargs):
+ def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs):
"""
Perform Discord Notification
"""
@@ -180,8 +181,8 @@ class NotifyDiscord(NotifyBase):
else:
# not markdown
- payload['content'] = body if not title \
- else "{}\r\n{}".format(title, body)
+ payload['content'] = \
+ body if not title else "{}\r\n{}".format(title, body)
if self.avatar and image_url:
payload['avatar_url'] = image_url
@@ -201,6 +202,10 @@ class NotifyDiscord(NotifyBase):
notify_url, self.verify_certificate,
))
self.logger.debug('Discord Payload: %s' % str(payload))
+
+ # Always call throttle before any remote server i/o is made
+ self.throttle()
+
try:
r = requests.post(
notify_url,
@@ -241,6 +246,28 @@ class NotifyDiscord(NotifyBase):
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
def parse_url(url):
"""
diff --git a/apprise/plugins/NotifyEmail.py b/apprise/plugins/NotifyEmail.py
index 69d836c8..b3ff37ff 100644
--- a/apprise/plugins/NotifyEmail.py
+++ b/apprise/plugins/NotifyEmail.py
@@ -24,15 +24,14 @@
# THE SOFTWARE.
import re
-
-from datetime import datetime
import smtplib
-from socket import error as SocketError
-
from email.mime.text import MIMEText
+from socket import error as SocketError
+from datetime import datetime
from .NotifyBase import NotifyBase
from ..common import NotifyFormat
+from ..common import NotifyType
class WebBaseLogin(object):
@@ -344,7 +343,7 @@ class NotifyEmail(NotifyBase):
break
- def notify(self, title, body, **kwargs):
+ def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs):
"""
Perform Email Notification
"""
@@ -375,6 +374,10 @@ class NotifyEmail(NotifyBase):
# bind the socket variable to the current namespace
socket = None
+
+ # Always call throttle before any remote server i/o is made
+ self.throttle()
+
try:
self.logger.debug('Connecting to remote SMTP server...')
socket_func = smtplib.SMTP
@@ -421,6 +424,53 @@ class NotifyEmail(NotifyBase):
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
def parse_url(url):
"""
diff --git a/apprise/plugins/NotifyEmby.py b/apprise/plugins/NotifyEmby.py
index 02093078..fff5417c 100644
--- a/apprise/plugins/NotifyEmby.py
+++ b/apprise/plugins/NotifyEmby.py
@@ -37,6 +37,7 @@ from json import loads
from .NotifyBase import NotifyBase
from .NotifyBase import HTTP_ERROR_MAP
from ..utils import parse_bool
+from ..common import NotifyType
from .. import __version__ as VERSION
@@ -445,7 +446,7 @@ class NotifyEmby(NotifyBase):
self.user_id = None
return True
- def notify(self, title, body, notify_type, **kwargs):
+ def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs):
"""
Perform Emby Notification
"""
@@ -494,6 +495,10 @@ class NotifyEmby(NotifyBase):
session_url, self.verify_certificate,
))
self.logger.debug('Emby Payload: %s' % str(payload))
+
+ # Always call throttle before the requests are made
+ self.throttle()
+
try:
r = requests.post(
session_url,
@@ -535,6 +540,39 @@ class NotifyEmby(NotifyBase):
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
def is_authenticated(self):
"""
diff --git a/apprise/plugins/NotifyFaast.py b/apprise/plugins/NotifyFaast.py
index cabee82b..5fd2d968 100644
--- a/apprise/plugins/NotifyFaast.py
+++ b/apprise/plugins/NotifyFaast.py
@@ -27,6 +27,7 @@ import requests
from .NotifyBase import NotifyBase
from .NotifyBase import HTTP_ERROR_MAP
from ..common import NotifyImageSize
+from ..common import NotifyType
class NotifyFaast(NotifyBase):
@@ -60,7 +61,7 @@ class NotifyFaast(NotifyBase):
self.authtoken = authtoken
- def notify(self, title, body, notify_type, **kwargs):
+ def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs):
"""
Perform Faast Notification
"""
@@ -85,6 +86,10 @@ class NotifyFaast(NotifyBase):
self.notify_url, self.verify_certificate,
))
self.logger.debug('Faast Payload: %s' % str(payload))
+
+ # Always call throttle before any remote server i/o is made
+ self.throttle()
+
try:
r = requests.post(
self.notify_url,
@@ -124,6 +129,23 @@ class NotifyFaast(NotifyBase):
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
def parse_url(url):
"""
diff --git a/apprise/plugins/NotifyGnome.py b/apprise/plugins/NotifyGnome.py
index 7f9747e5..c1f1cd5b 100644
--- a/apprise/plugins/NotifyGnome.py
+++ b/apprise/plugins/NotifyGnome.py
@@ -26,10 +26,9 @@
from __future__ import absolute_import
from __future__ import print_function
-import re
-
from .NotifyBase import NotifyBase
from ..common import NotifyImageSize
+from ..common import NotifyType
# Default our global support flag
NOTIFY_GNOME_SUPPORT_ENABLED = False
@@ -86,6 +85,18 @@ class NotifyGnome(NotifyBase):
# Allows the user to specify the NotifyImageSize object
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
# in an environment that simply doesn't have the gnome packages
# available to us. It also allows us to handle situations where the
@@ -109,7 +120,7 @@ class NotifyGnome(NotifyBase):
else:
self.urgency = urgency
- def notify(self, title, body, notify_type, **kwargs):
+ def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs):
"""
Perform Gnome Notification
"""
@@ -119,15 +130,6 @@ class NotifyGnome(NotifyBase):
"Gnome Notifications are not supported by this system.")
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:
# App initialization
Notify.init(self.app_id)
@@ -141,6 +143,9 @@ class NotifyGnome(NotifyBase):
# Assign urgency
notification.set_urgency(self.urgency)
+ # Always call throttle before any remote server i/o is made
+ self.throttle()
+
try:
# Use Pixbuf to create the proper image type
image = GdkPixbuf.Pixbuf.new_from_file(icon_path)
@@ -157,13 +162,20 @@ class NotifyGnome(NotifyBase):
notification.show()
self.logger.info('Sent Gnome notification.')
- except Exception as e:
+ except Exception:
self.logger.warning('Failed to send Gnome notification.')
self.logger.exception('Gnome Exception')
return False
return True
+ def url(self):
+ """
+ Returns the URL built dynamically based on specified arguments.
+ """
+
+ return '{schema}://'.format(schema=self.protocol)
+
@staticmethod
def parse_url(url):
"""
diff --git a/apprise/plugins/NotifyGrowl/NotifyGrowl.py b/apprise/plugins/NotifyGrowl/NotifyGrowl.py
index e478998b..86adee1b 100644
--- a/apprise/plugins/NotifyGrowl/NotifyGrowl.py
+++ b/apprise/plugins/NotifyGrowl/NotifyGrowl.py
@@ -23,12 +23,11 @@
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
-import re
-
from .gntp import notifier
from .gntp import errors
from ..NotifyBase import NotifyBase
from ...common import NotifyImageSize
+from ...common import NotifyType
# Priorities
@@ -69,12 +68,24 @@ class NotifyGrowl(NotifyBase):
# A URL that takes you to the setup/help of the specific protocol
setup_url = 'https://github.com/caronc/apprise/wiki/Notify_growl'
- # Default Growl Port
- default_port = 23053
-
# Allows the user to specify the NotifyImageSize object
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):
"""
Initialize Growl Object
@@ -143,17 +154,11 @@ class NotifyGrowl(NotifyBase):
return
- def notify(self, title, body, notify_type, **kwargs):
+ def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs):
"""
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
if self.version >= 2:
# URL Based
@@ -178,6 +183,9 @@ class NotifyGrowl(NotifyBase):
# print the binary contents of an image
payload['icon'] = icon
+ # Always call throttle before any remote server i/o is made
+ self.throttle()
+
try:
response = self.growl.notify(**payload)
if not isinstance(response, bool):
@@ -207,6 +215,44 @@ class NotifyGrowl(NotifyBase):
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
def parse_url(url):
"""
@@ -239,15 +285,10 @@ class NotifyGrowl(NotifyBase):
if 'priority' in results['qsd'] and len(results['qsd']['priority']):
_map = {
'l': GrowlPriority.LOW,
- '-2': GrowlPriority.LOW,
'm': GrowlPriority.MODERATE,
- '-1': GrowlPriority.MODERATE,
'n': GrowlPriority.NORMAL,
- '0': GrowlPriority.NORMAL,
'h': GrowlPriority.HIGH,
- '1': GrowlPriority.HIGH,
'e': GrowlPriority.EMERGENCY,
- '2': GrowlPriority.EMERGENCY,
}
try:
results['priority'] = \
diff --git a/apprise/plugins/NotifyIFTTT.py b/apprise/plugins/NotifyIFTTT.py
index 45a7782b..b0b006fa 100644
--- a/apprise/plugins/NotifyIFTTT.py
+++ b/apprise/plugins/NotifyIFTTT.py
@@ -34,16 +34,18 @@
# URL. For example, it might look like this:
# 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
#
# For each event you create you will assign it a name (this will be known as
# the {event} when building your URL.
import requests
-
from json import dumps
+
from .NotifyBase import NotifyBase
from .NotifyBase import HTTP_ERROR_MAP
+from ..common import NotifyType
+from ..utils import parse_list
class NotifyIFTTT(NotifyBase):
@@ -59,7 +61,7 @@ class NotifyIFTTT(NotifyBase):
service_url = 'https://ifttt.com/'
# The default protocol
- protocol = 'ifttt'
+ secure_protocol = 'ifttt'
# A URL that takes you to the setup/help of the specific protocol
setup_url = 'https://github.com/caronc/apprise/wiki/Notify_ifttt'
@@ -87,37 +89,55 @@ class NotifyIFTTT(NotifyBase):
ifttt_default_type_key = 'value3'
# 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
+ 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)
- if not apikey:
- raise TypeError('You must specify the Webhooks apikey.')
+ if not webhook_id:
+ raise TypeError('You must specify the Webhooks webhook_id.')
- if not event:
- raise TypeError('You must specify the Event you wish to trigger.')
+ # Store our Events we wish to trigger
+ self.events = parse_list(events)
+
+ if not self.events:
+ raise TypeError(
+ 'You must specify at least one event you wish to trigger on.')
# Store our APIKey
- self.apikey = apikey
+ self.webhook_id = webhook_id
- # Store our Event we wish to trigger
- self.event = event
+ # Tokens to include in post
+ self.add_tokens = {}
+ if add_tokens:
+ self.add_tokens.update(add_tokens)
- if isinstance(event_args, dict):
- # Make a copy of the arguments so that they can't change
- # outside of this plugin
- self.event_args = event_args.copy()
+ # Tokens to remove
+ self.del_tokens = []
+ if del_tokens is not None:
+ if isinstance(del_tokens, (list, tuple, set)):
+ self.del_tokens = del_tokens
- else:
- # Force a dictionary
- self.event_args = dict()
+ else:
+ raise TypeError(
+ '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
"""
@@ -134,72 +154,106 @@ class NotifyIFTTT(NotifyBase):
self.ifttt_default_type_key: notify_type,
}
- # Update our payload using any other event_args specified
- payload.update(self.event_args)
+ # Add any new tokens expected (this can also potentially override
+ # any entries defined above)
+ payload.update(self.add_tokens)
- # Eliminate empty fields; users wishing to cancel the use of the
- # self.ifttt_default_ entries can preset these keys to being
- # empty so that they get caught here and removed.
- payload = {x: y for x, y in payload.items() if y}
+ # Eliminate fields flagged for removal
+ payload = {x: y for x, y in payload.items()
+ if x not in self.del_tokens}
- # URL to transmit content via
- url = self.notify_url.format(
- apikey=self.apikey,
- event=self.event,
+ # Track our failures
+ error_count = 0
+
+ # 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
def parse_url(url):
"""
@@ -214,22 +268,14 @@ class NotifyIFTTT(NotifyBase):
return results
# Our Event
- results['event'] = results['host']
+ results['events'] = list()
+ results['events'].append(results['host'])
# Our API Key
- results['apikey'] = results['user']
+ results['webhook_id'] = results['user']
- # Store ValueX entries based on each entry past the host
- results['event_args'] = {
- '{0}{1}'.format(NotifyIFTTT.ifttt_default_key_prefix, n + 1):
- 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()})
+ # Now fetch the remaining tokens
+ results['events'].extend([x for x in filter(
+ bool, NotifyBase.split_path(results['fullpath']))][0:])
return results
diff --git a/apprise/plugins/NotifyJSON.py b/apprise/plugins/NotifyJSON.py
index ba160e92..466a6fa1 100644
--- a/apprise/plugins/NotifyJSON.py
+++ b/apprise/plugins/NotifyJSON.py
@@ -29,6 +29,7 @@ from json import dumps
from .NotifyBase import NotifyBase
from .NotifyBase import HTTP_ERROR_MAP
from ..common import NotifyImageSize
+from ..common import NotifyType
from ..utils import compat_is_basestring
@@ -52,9 +53,17 @@ class NotifyJSON(NotifyBase):
# Allows the user to specify the NotifyImageSize object
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
+
+ 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)
@@ -68,9 +77,51 @@ class NotifyJSON(NotifyBase):
if not compat_is_basestring(self.fullpath):
self.fullpath = '/'
+ self.headers = {}
+ if headers:
+ # Store our extra headers
+ self.headers.update(headers)
+
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
"""
@@ -91,8 +142,8 @@ class NotifyJSON(NotifyBase):
'Content-Type': 'application/json'
}
- if self.headers:
- headers.update(self.headers)
+ # Apply any/all header over-rides defined
+ headers.update(self.headers)
auth = None
if self.user:
@@ -108,6 +159,10 @@ class NotifyJSON(NotifyBase):
url, self.verify_certificate,
))
self.logger.debug('JSON Payload: %s' % str(payload))
+
+ # Always call throttle before any remote server i/o is made
+ self.throttle()
+
try:
r = requests.post(
url,
@@ -145,3 +200,23 @@ class NotifyJSON(NotifyBase):
return False
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
diff --git a/apprise/plugins/NotifyJoin.py b/apprise/plugins/NotifyJoin.py
index 67b75ab9..73824d99 100644
--- a/apprise/plugins/NotifyJoin.py
+++ b/apprise/plugins/NotifyJoin.py
@@ -39,6 +39,7 @@ import requests
from .NotifyBase import NotifyBase
from .NotifyBase import HTTP_ERROR_MAP
from ..common import NotifyImageSize
+from ..common import NotifyType
from ..utils import compat_is_basestring
# Token required as part of the API request
@@ -78,7 +79,7 @@ class NotifyJoin(NotifyBase):
service_url = 'https://joaoapps.com/join/'
# The default protocol
- protocol = 'join'
+ secure_protocol = 'join'
# A URL that takes you to the setup/help of the specific protocol
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
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
body_maxlen = 1000
@@ -126,22 +131,11 @@ class NotifyJoin(NotifyBase):
# Default to everyone
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
"""
- 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 = {
'User-Agent': self.app_id,
'Content-Type': 'application/x-www-form-urlencoded',
@@ -188,6 +182,9 @@ class NotifyJoin(NotifyBase):
))
self.logger.debug('Join Payload: %s' % str(payload))
+ # Always call throttle before any remote server i/o is made
+ self.throttle()
+
try:
r = requests.post(
url,
@@ -227,12 +224,25 @@ class NotifyJoin(NotifyBase):
self.logger.debug('Socket Exception: %s' % str(e))
return_status = False
- if len(devices):
- # Prevent thrashing requests
- self.throttle()
-
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
def parse_url(url):
"""
diff --git a/apprise/plugins/NotifyMatrix.py b/apprise/plugins/NotifyMatrix.py
index 53c38942..f530ebe1 100644
--- a/apprise/plugins/NotifyMatrix.py
+++ b/apprise/plugins/NotifyMatrix.py
@@ -30,6 +30,7 @@ from time import time
from .NotifyBase import NotifyBase
from .NotifyBase import HTTP_ERROR_MAP
+from ..common import NotifyType
# Token required as part of the API request
VALIDATE_TOKEN = re.compile(r'[A-Za-z0-9]{64}')
@@ -112,7 +113,6 @@ class NotifyMatrix(NotifyBase):
if not self.user:
self.logger.warning(
'No user was specified; using %s.' % MATRIX_DEFAULT_USER)
- self.user = MATRIX_DEFAULT_USER
if mode not in MATRIX_NOTIFICATION_MODES:
self.logger.warning('The mode specified (%s) is invalid.' % mode)
@@ -135,7 +135,7 @@ class NotifyMatrix(NotifyBase):
re.IGNORECASE,
)
- def notify(self, title, body, notify_type, **kwargs):
+ def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs):
"""
Perform Matrix Notification
"""
@@ -170,6 +170,10 @@ class NotifyMatrix(NotifyBase):
url, self.verify_certificate,
))
self.logger.debug('Matrix Payload: %s' % str(payload))
+
+ # Always call throttle before any remote server i/o is made
+ self.throttle()
+
try:
r = requests.post(
url,
@@ -209,7 +213,7 @@ class NotifyMatrix(NotifyBase):
def __slack_mode_payload(self, title, body, notify_type):
# prepare JSON Object
payload = {
- 'username': self.user,
+ 'username': self.user if self.user else MATRIX_DEFAULT_USER,
# Use Markdown language
'mrkdwn': True,
'attachments': [{
@@ -230,13 +234,44 @@ class NotifyMatrix(NotifyBase):
msg = '
%s
%s
' % (title, body)
payload = {
- 'displayName': self.user,
+ 'displayName': self.user if self.user else MATRIX_DEFAULT_USER,
'format': 'html',
'text': msg,
}
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
def parse_url(url):
"""
diff --git a/apprise/plugins/NotifyMatterMost.py b/apprise/plugins/NotifyMatterMost.py
index e92b8987..1d79bb8b 100644
--- a/apprise/plugins/NotifyMatterMost.py
+++ b/apprise/plugins/NotifyMatterMost.py
@@ -30,6 +30,7 @@ from json import dumps
from .NotifyBase import NotifyBase
from .NotifyBase import HTTP_ERROR_MAP
from ..common import NotifyImageSize
+from ..common import NotifyType
# Some Reference Locations:
# - 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
body_maxlen = 4000
+ # Mattermost does not have a title
+ title_maxlen = 0
+
def __init__(self, authtoken, channel=None, **kwargs):
"""
Initialize MatterMost Object
@@ -108,7 +112,7 @@ class NotifyMatterMost(NotifyBase):
return
- def notify(self, title, body, notify_type, **kwargs):
+ def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs):
"""
Perform MatterMost Notification
"""
@@ -120,7 +124,7 @@ class NotifyMatterMost(NotifyBase):
# prepare JSON Object
payload = {
- 'text': '###### %s\n%s' % (title, body),
+ 'text': body,
'icon_url': self.image_url(notify_type),
}
@@ -140,6 +144,10 @@ class NotifyMatterMost(NotifyBase):
url, self.verify_certificate,
))
self.logger.debug('MatterMost Payload: %s' % str(payload))
+
+ # Always call throttle before any remote server i/o is made
+ self.throttle()
+
try:
r = requests.post(
url,
@@ -179,6 +187,29 @@ class NotifyMatterMost(NotifyBase):
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
def parse_url(url):
"""
diff --git a/apprise/plugins/NotifyProwl.py b/apprise/plugins/NotifyProwl.py
index 7ddb6f11..2bd8235e 100644
--- a/apprise/plugins/NotifyProwl.py
+++ b/apprise/plugins/NotifyProwl.py
@@ -28,6 +28,7 @@ import requests
from .NotifyBase import NotifyBase
from .NotifyBase import HTTP_ERROR_MAP
+from ..common import NotifyType
# Used to validate API Key
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
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
body_maxlen = 10000
@@ -124,7 +129,7 @@ class NotifyProwl(NotifyBase):
# Store the Provider Key
self.providerkey = providerkey
- def notify(self, title, body, **kwargs):
+ def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs):
"""
Perform Prowl Notification
"""
@@ -150,6 +155,10 @@ class NotifyProwl(NotifyBase):
self.notify_url, self.verify_certificate,
))
self.logger.debug('Prowl Payload: %s' % str(payload))
+
+ # Always call throttle before any remote server i/o is made
+ self.throttle()
+
try:
r = requests.post(
self.notify_url,
@@ -190,6 +199,35 @@ class NotifyProwl(NotifyBase):
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
def parse_url(url):
"""
@@ -216,15 +254,10 @@ class NotifyProwl(NotifyBase):
if 'priority' in results['qsd'] and len(results['qsd']['priority']):
_map = {
'l': ProwlPriority.LOW,
- '-2': ProwlPriority.LOW,
'm': ProwlPriority.MODERATE,
- '-1': ProwlPriority.MODERATE,
'n': ProwlPriority.NORMAL,
- '0': ProwlPriority.NORMAL,
'h': ProwlPriority.HIGH,
- '1': ProwlPriority.HIGH,
'e': ProwlPriority.EMERGENCY,
- '2': ProwlPriority.EMERGENCY,
}
try:
results['priority'] = \
diff --git a/apprise/plugins/NotifyPushBullet.py b/apprise/plugins/NotifyPushBullet.py
index 5eac9a46..e4317afd 100644
--- a/apprise/plugins/NotifyPushBullet.py
+++ b/apprise/plugins/NotifyPushBullet.py
@@ -30,7 +30,7 @@ from json import dumps
from .NotifyBase import NotifyBase
from .NotifyBase import HTTP_ERROR_MAP
from .NotifyBase import IS_EMAIL_RE
-
+from ..common import NotifyType
from ..utils import compat_is_basestring
# Flag used as a placeholder to sending to all devices
@@ -87,7 +87,7 @@ class NotifyPushBullet(NotifyBase):
if len(self.recipients) == 0:
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
"""
@@ -135,6 +135,10 @@ class NotifyPushBullet(NotifyBase):
self.notify_url, self.verify_certificate,
))
self.logger.debug('PushBullet Payload: %s' % str(payload))
+
+ # Always call throttle before any remote server i/o is made
+ self.throttle()
+
try:
r = requests.post(
self.notify_url,
@@ -176,12 +180,31 @@ class NotifyPushBullet(NotifyBase):
self.logger.debug('Socket Exception: %s' % str(e))
has_error = True
- if len(recipients):
- # Prevent thrashing requests
- self.throttle()
-
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
def parse_url(url):
"""
diff --git a/apprise/plugins/NotifyPushed.py b/apprise/plugins/NotifyPushed.py
index b925ba63..7d7f21b4 100644
--- a/apprise/plugins/NotifyPushed.py
+++ b/apprise/plugins/NotifyPushed.py
@@ -26,9 +26,11 @@
import re
import requests
from json import dumps
+from itertools import chain
from .NotifyBase import NotifyBase
from .NotifyBase import HTTP_ERROR_MAP
+from ..common import NotifyType
from ..utils import compat_is_basestring
# Used to detect and parse channels
@@ -127,7 +129,7 @@ class NotifyPushed(NotifyBase):
return
- def notify(self, title, body, notify_type, **kwargs):
+ def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs):
"""
Perform Pushed Notification
"""
@@ -152,7 +154,7 @@ class NotifyPushed(NotifyBase):
if len(self.channels) + len(self.users) == 0:
# Just notify the app
- return self.send_notification(
+ return self._send(
payload=payload, notify_type=notify_type, **kwargs)
# If our code reaches here, we want to target channels and users (by
@@ -170,16 +172,12 @@ class NotifyPushed(NotifyBase):
# Get Channel
_payload['target_alias'] = channels.pop(0)
- if not self.send_notification(
+ if not self._send(
payload=_payload, notify_type=notify_type, **kwargs):
# toggle flag
has_error = True
- if len(channels) + len(users) > 0:
- # Prevent thrashing requests
- self.throttle()
-
# Copy our payload
_payload = dict(payload)
_payload['target_type'] = 'pushed_id'
@@ -188,23 +186,20 @@ class NotifyPushed(NotifyBase):
while len(users):
# Get User's Pushed ID
_payload['pushed_id'] = users.pop(0)
- if not self.send_notification(
+
+ if not self._send(
payload=_payload, notify_type=notify_type, **kwargs):
# toggle flag
has_error = True
- if len(users) > 0:
- # Prevent thrashing requests
- self.throttle()
-
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
Notification servers. This should never be called directly; it is
- referenced automatically through the notify() function.
+ referenced automatically through the send() function.
"""
headers = {
@@ -216,6 +211,10 @@ class NotifyPushed(NotifyBase):
self.notify_url, self.verify_certificate,
))
self.logger.debug('Pushed Payload: %s' % str(payload))
+
+ # Always call throttle before any remote server i/o is made
+ self.throttle()
+
try:
r = requests.post(
self.notify_url,
@@ -256,6 +255,30 @@ class NotifyPushed(NotifyBase):
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
def parse_url(url):
"""
diff --git a/apprise/plugins/NotifyPushjet/NotifyPushjet.py b/apprise/plugins/NotifyPushjet/NotifyPushjet.py
index cb9ddc94..dafd8214 100644
--- a/apprise/plugins/NotifyPushjet/NotifyPushjet.py
+++ b/apprise/plugins/NotifyPushjet/NotifyPushjet.py
@@ -28,6 +28,7 @@ from .pushjet import errors
from .pushjet import pushjet
from ..NotifyBase import NotifyBase
+from ...common import NotifyType
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)
@@ -52,27 +53,35 @@ class NotifyPushjet(NotifyBase):
# A URL that takes you to the setup/help of the specific protocol
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
"""
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
"""
+ # 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:
- server = "http://"
- if self.secure:
- server = "https://"
-
- server += self.host
- if self.port:
- server += ":" + str(self.port)
-
api = pushjet.Api(server)
- service = api.Service(secret_key=self.user)
+ service = api.Service(secret_key=self.secret_key)
service.send(body, title)
self.logger.info('Sent Pushjet notification.')
@@ -84,6 +93,28 @@ class NotifyPushjet(NotifyBase):
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
def parse_url(url):
"""
@@ -91,10 +122,10 @@ class NotifyPushjet(NotifyBase):
us to substantiate this object.
Syntax:
- pjet://secret@hostname
- pjet://secret@hostname:port
- pjets://secret@hostname
- pjets://secret@hostname:port
+ pjet://secret_key@hostname
+ pjet://secret_key@hostname:port
+ pjets://secret_key@hostname
+ pjets://secret_key@hostname:port
"""
results = NotifyBase.parse_url(url)
@@ -107,4 +138,7 @@ class NotifyPushjet(NotifyBase):
# a username is required
return None
+ # Store it as it's value
+ results['secret_key'] = results.get('user')
+
return results
diff --git a/apprise/plugins/NotifyPushover.py b/apprise/plugins/NotifyPushover.py
index fee093d7..99a78dd3 100644
--- a/apprise/plugins/NotifyPushover.py
+++ b/apprise/plugins/NotifyPushover.py
@@ -26,9 +26,10 @@
import re
import requests
-from ..utils import compat_is_basestring
from .NotifyBase import NotifyBase
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
PUSHOVER_SEND_TO_ALL = 'ALL_DEVICES'
@@ -149,7 +150,7 @@ class NotifyPushover(NotifyBase):
'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
"""
@@ -189,6 +190,10 @@ class NotifyPushover(NotifyBase):
self.notify_url, self.verify_certificate,
))
self.logger.debug('Pushover Payload: %s' % str(payload))
+
+ # Always call throttle before any remote server i/o is made
+ self.throttle()
+
try:
r = requests.post(
self.notify_url,
@@ -231,12 +236,44 @@ class NotifyPushover(NotifyBase):
self.logger.debug('Socket Exception: %s' % str(e))
has_error = True
- if len(devices):
- # Prevent thrashing requests
- self.throttle()
-
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
def parse_url(url):
"""
diff --git a/apprise/plugins/NotifyRocketChat.py b/apprise/plugins/NotifyRocketChat.py
index cb86b84d..2ede6ad2 100644
--- a/apprise/plugins/NotifyRocketChat.py
+++ b/apprise/plugins/NotifyRocketChat.py
@@ -26,9 +26,11 @@
import re
import requests
from json import loads
+from itertools import chain
from .NotifyBase import NotifyBase
from .NotifyBase import HTTP_ERROR_MAP
+from ..common import NotifyType
from ..utils import compat_is_basestring
IS_CHANNEL = re.compile(r'^#(?P[A-Za-z0-9]+)$')
@@ -66,8 +68,11 @@ class NotifyRocketChat(NotifyBase):
# A URL that takes you to the setup/help of the specific protocol
setup_url = 'https://github.com/caronc/apprise/wiki/Notify_rocketchat'
- # Defines the maximum allowable characters in the title
- title_maxlen = 200
+ # The title is not used
+ title_maxlen = 0
+
+ # The maximum size of the message
+ body_maxlen = 200
def __init__(self, recipients=None, **kwargs):
"""
@@ -106,6 +111,12 @@ class NotifyRocketChat(NotifyBase):
elif not isinstance(recipients, (set, tuple, list)):
recipients = []
+ if not (self.user and self.password):
+ # Username & Password is required for Rocket Chat to work
+ raise TypeError(
+ 'No Rocket.Chat user/pass combo specified.'
+ )
+
# Validate recipients and drop bad ones:
for recipient in recipients:
result = IS_CHANNEL.match(recipient)
@@ -133,9 +144,44 @@ class NotifyRocketChat(NotifyBase):
# Used to track token headers upon authentication (if successful)
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
@@ -143,8 +189,8 @@ class NotifyRocketChat(NotifyBase):
if not self.login():
return False
- # Prepare our message
- text = '*%s*\r\n%s' % (title.replace('*', '\\*'), body)
+ # Prepare our message using the body only
+ text = body
# Initiaize our error tracking
has_error = False
@@ -157,7 +203,7 @@ class NotifyRocketChat(NotifyBase):
# Get Channel
channel = channels.pop(0)
- if not self.send_notification(
+ if not self._send(
{
'text': text,
'channel': channel,
@@ -166,16 +212,12 @@ class NotifyRocketChat(NotifyBase):
# toggle flag
has_error = True
- if len(channels) + len(rooms) > 0:
- # Prevent thrashing requests
- self.throttle()
-
# Send all our defined room id's
while len(rooms):
# Get Room
room = rooms.pop(0)
- if not self.send_notification(
+ if not self._send(
{
'text': text,
'roomId': room,
@@ -184,16 +226,12 @@ class NotifyRocketChat(NotifyBase):
# toggle flag
has_error = True
- if len(rooms) > 0:
- # Prevent thrashing requests
- self.throttle()
-
# logout
self.logout()
return not has_error
- def send_notification(self, payload, notify_type, **kwargs):
+ def _send(self, payload, notify_type, **kwargs):
"""
Perform Notify Rocket.Chat Notification
"""
@@ -202,6 +240,10 @@ class NotifyRocketChat(NotifyBase):
self.api_url + 'chat.postMessage', self.verify_certificate,
))
self.logger.debug('Rocket.Chat Payload: %s' % str(payload))
+
+ # Always call throttle before any remote server i/o is made
+ self.throttle()
+
try:
r = requests.post(
self.api_url + 'chat.postMessage',
diff --git a/apprise/plugins/NotifyRyver.py b/apprise/plugins/NotifyRyver.py
index c0f6b81c..5139e7b8 100644
--- a/apprise/plugins/NotifyRyver.py
+++ b/apprise/plugins/NotifyRyver.py
@@ -38,6 +38,7 @@ from json import dumps
from .NotifyBase import NotifyBase
from .NotifyBase import HTTP_ERROR_MAP
from ..common import NotifyImageSize
+from ..common import NotifyType
# Token required as part of the API request
VALIDATE_TOKEN = re.compile(r'[A-Za-z0-9]{15}')
@@ -141,7 +142,7 @@ class NotifyRyver(NotifyBase):
re.IGNORECASE,
)
- def notify(self, title, body, notify_type, **kwargs):
+ def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs):
"""
Perform Ryver Notification
"""
@@ -178,6 +179,10 @@ class NotifyRyver(NotifyBase):
url, self.verify_certificate,
))
self.logger.debug('Ryver Payload: %s' % str(payload))
+
+ # Always call throttle before any remote server i/o is made
+ self.throttle()
+
try:
r = requests.post(
url,
@@ -221,6 +226,33 @@ class NotifyRyver(NotifyBase):
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
def parse_url(url):
"""
diff --git a/apprise/plugins/NotifySNS.py b/apprise/plugins/NotifySNS.py
index abc46f42..b04be920 100644
--- a/apprise/plugins/NotifySNS.py
+++ b/apprise/plugins/NotifySNS.py
@@ -24,16 +24,17 @@
# THE SOFTWARE.
import re
-
import hmac
import requests
from hashlib import sha256
from datetime import datetime
from collections import OrderedDict
from xml.etree import ElementTree
+from itertools import chain
from .NotifyBase import NotifyBase
from .NotifyBase import HTTP_ERROR_MAP
+from ..common import NotifyType
from ..utils import compat_is_basestring
# 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
# users of this product search though this Access Key Secret and escape all
# of the forward slashes!
-IS_REGION = re.compile(r'^\s*(?P[a-z]{2})-'
- r'(?P[a-z]+)-(?P[0-9]+)\s*$', re.I)
+IS_REGION = re.compile(
+ r'^\s*(?P[a-z]{2})-(?P[a-z]+)-(?P[0-9]+)\s*$', re.I)
# Extend HTTP Error Messages
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
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
# Source: https://docs.aws.amazon.com/sns/latest/api/API_Publish.html
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,
recipients=None, **kwargs):
"""
@@ -185,7 +194,7 @@ class NotifySNS(NotifyBase):
self.logger.warning(
'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
"""
@@ -214,10 +223,6 @@ class NotifySNS(NotifyBase):
if not result:
error_count += 1
- if len(phone) > 0:
- # Prevent thrashing requests
- self.throttle()
-
# Send all our defined topic id's
while len(topics):
@@ -256,21 +261,24 @@ class NotifySNS(NotifyBase):
if not result:
error_count += 1
- if len(topics) > 0:
- # Prevent thrashing requests
- self.throttle()
-
return error_count == 0
def _post(self, payload, to):
"""
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
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
payload = self.urlencode(payload)
@@ -282,6 +290,7 @@ class NotifySNS(NotifyBase):
self.notify_url, self.verify_certificate,
))
self.logger.debug('AWS Payload: %s' % str(payload))
+
try:
r = requests.post(
self.notify_url,
@@ -521,6 +530,33 @@ class NotifySNS(NotifyBase):
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
def parse_url(url):
"""
diff --git a/apprise/plugins/NotifySlack.py b/apprise/plugins/NotifySlack.py
index ffa8a71a..7980d908 100644
--- a/apprise/plugins/NotifySlack.py
+++ b/apprise/plugins/NotifySlack.py
@@ -43,6 +43,7 @@ from time import time
from .NotifyBase import NotifyBase
from .NotifyBase import HTTP_ERROR_MAP
from ..common import NotifyImageSize
+from ..common import NotifyType
from ..utils import compat_is_basestring
# Token required as part of the API request
@@ -141,7 +142,6 @@ class NotifySlack(NotifyBase):
if not self.user:
self.logger.warning(
'No user was specified; using %s.' % SLACK_DEFAULT_USER)
- self.user = SLACK_DEFAULT_USER
if compat_is_basestring(channels):
self.channels = [x for x in filter(bool, CHANNEL_LIST_DELIM.split(
@@ -175,7 +175,7 @@ class NotifySlack(NotifyBase):
re.IGNORECASE,
)
- def notify(self, title, body, notify_type, **kwargs):
+ def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs):
"""
Perform Slack Notification
"""
@@ -231,7 +231,7 @@ class NotifySlack(NotifyBase):
# prepare JSON Object
payload = {
'channel': _channel,
- 'username': self.user,
+ 'username': self.user if self.user else SLACK_DEFAULT_USER,
# Use Markdown language
'mrkdwn': True,
'attachments': [{
@@ -251,6 +251,9 @@ class NotifySlack(NotifyBase):
url, self.verify_certificate,
))
self.logger.debug('Slack Payload: %s' % str(payload))
+
+ # Always call throttle before any remote server i/o is made
+ self.throttle()
try:
r = requests.post(
url,
@@ -275,7 +278,7 @@ class NotifySlack(NotifyBase):
channel,
r.status_code))
- # self.logger.debug('Response Details: %s' % r.raw.read())
+ # self.logger.debug('Response Details: %s' % r.content)
# Return; we're done
notify_okay = False
@@ -291,12 +294,38 @@ class NotifySlack(NotifyBase):
self.logger.debug('Socket Exception: %s' % str(e))
notify_okay = False
- if len(channels):
- # Prevent thrashing requests
- self.throttle()
-
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
def parse_url(url):
"""
diff --git a/apprise/plugins/NotifyTelegram.py b/apprise/plugins/NotifyTelegram.py
index 4ad54cc8..7598cced 100644
--- a/apprise/plugins/NotifyTelegram.py
+++ b/apprise/plugins/NotifyTelegram.py
@@ -59,10 +59,11 @@ from json import dumps
from .NotifyBase import NotifyBase
from .NotifyBase import HTTP_ERROR_MAP
+from ..common import NotifyType
from ..common import NotifyImageSize
-from ..utils import compat_is_basestring
-from ..utils import parse_bool
from ..common import NotifyFormat
+from ..utils import parse_bool
+from ..utils import parse_list
TELEGRAM_IMAGE_XY = NotifyImageSize.XY_256
@@ -81,9 +82,6 @@ IS_CHAT_ID_RE = re.compile(
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):
"""
@@ -134,16 +132,8 @@ class NotifyTelegram(NotifyBase):
# Store our Bot Token
self.bot_token = result.group('key')
- if compat_is_basestring(chat_ids):
- self.chat_ids = [x for x in filter(bool, CHAT_ID_LIST_DELIM.split(
- chat_ids,
- ))]
-
- elif isinstance(chat_ids, (set, tuple, list)):
- self.chat_ids = list(chat_ids)
-
- else:
- self.chat_ids = list()
+ # Parse our list
+ self.chat_ids = parse_list(chat_ids)
if self.user:
# Treat this as a channel too
@@ -153,7 +143,7 @@ class NotifyTelegram(NotifyBase):
_id = self.detect_bot_owner()
if _id:
# Store our id
- self.chat_ids = [str(_id)]
+ self.chat_ids.append(str(_id))
if len(self.chat_ids) == 0:
self.logger.warning('No chat_id(s) were specified.')
@@ -336,7 +326,7 @@ class NotifyTelegram(NotifyBase):
return 0
- def notify(self, title, body, notify_type, **kwargs):
+ def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs):
"""
Perform Telegram Notification
"""
@@ -424,14 +414,14 @@ class NotifyTelegram(NotifyBase):
# ID
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:
# Send an image
- if self.send_image(
- 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.send_image(payload['chat_id'], notify_type)
self.logger.debug('Telegram POST URL: %s (cert_verify=%r)' % (
url, self.verify_certificate,
@@ -494,13 +484,28 @@ class NotifyTelegram(NotifyBase):
self.logger.debug('Socket Exception: %s' % str(e))
has_error = True
- finally:
- if len(chat_ids):
- # Prevent thrashing requests
- self.throttle()
-
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
def parse_url(url):
"""
@@ -512,17 +517,17 @@ class NotifyTelegram(NotifyBase):
# tgram:// messages since the bot_token has a colon in it.
# It invalidates an normal URL.
- # This hack searches for this bogus URL and corrects it
- # so we can properly load it further down. The other
- # alternative is to ask users to actually change the colon
- # into a slash (which will work too), but it's more likely
- # to cause confusion... So this is the next best thing
+ # This hack searches for this bogus URL and corrects it so we can
+ # properly load it further down. The other alternative is to ask users
+ # to actually change the colon into a slash (which will work too), but
+ # it's more likely to cause confusion... So this is the next best thing
+ # we also check for %3A (incase the URL is encoded) as %3A == :
try:
tgram = re.match(
- r'(?P%s://)(bot)?(?P([a-z0-9_-]+)'
- r'(:[a-z0-9_-]+)?@)?(?P[0-9]+):+'
- r'(?P.*)$' % NotifyTelegram.secure_protocol,
- url, re.I)
+ r'(?P{schema}://)(bot)?(?P([a-z0-9_-]+)'
+ r'(:[a-z0-9_-]+)?@)?(?P[0-9]+)(:|%3A)+'
+ r'(?P.*)$'.format(
+ schema=NotifyTelegram.secure_protocol), url, re.I)
except (TypeError, AttributeError):
# url is bad; force tgram to be None
@@ -534,14 +539,11 @@ class NotifyTelegram(NotifyBase):
if tgram.group('prefix'):
# Try again
- results = NotifyBase.parse_url(
- '%s%s%s/%s' % (
- tgram.group('protocol'),
- tgram.group('prefix'),
- tgram.group('btoken_a'),
- tgram.group('remaining'),
- ),
- )
+ results = NotifyBase.parse_url('%s%s%s/%s' % (
+ tgram.group('protocol'),
+ tgram.group('prefix'),
+ tgram.group('btoken_a'),
+ tgram.group('remaining')))
else:
# Try again
@@ -562,9 +564,8 @@ class NotifyTelegram(NotifyBase):
bot_token = '%s:%s' % (bot_token_a, bot_token_b)
- chat_ids = ','.join(
- [x for x in filter(
- bool, NotifyBase.split_path(results['fullpath']))][1:])
+ chat_ids = [x for x in filter(
+ bool, NotifyBase.split_path(results['fullpath']))][1:]
# Store our bot token
results['bot_token'] = bot_token
diff --git a/apprise/plugins/NotifyTwitter/NotifyTwitter.py b/apprise/plugins/NotifyTwitter/NotifyTwitter.py
index df7dbdd8..50275d4e 100644
--- a/apprise/plugins/NotifyTwitter/NotifyTwitter.py
+++ b/apprise/plugins/NotifyTwitter/NotifyTwitter.py
@@ -25,6 +25,7 @@
from . import tweepy
from ..NotifyBase import NotifyBase
+from ...common import NotifyType
class NotifyTwitter(NotifyBase):
@@ -50,6 +51,9 @@ class NotifyTwitter(NotifyBase):
# which are limited to 240 characters)
body_maxlen = 4096
+ # Twitter does have titles when creating a message
+ title_maxlen = 0
+
def __init__(self, ckey, csecret, akey, asecret, **kwargs):
"""
Initialize Twitter Object
@@ -90,7 +94,7 @@ class NotifyTwitter(NotifyBase):
return
- def notify(self, title, body, notify_type, **kwargs):
+ def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs):
"""
Perform Twitter Notification
"""
@@ -109,13 +113,16 @@ class NotifyTwitter(NotifyBase):
)
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:
# Get our API
api = tweepy.API(self.auth)
# 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.')
except Exception as e:
diff --git a/apprise/plugins/NotifyWindows.py b/apprise/plugins/NotifyWindows.py
index ea0f0e16..f39a5285 100644
--- a/apprise/plugins/NotifyWindows.py
+++ b/apprise/plugins/NotifyWindows.py
@@ -26,11 +26,11 @@
from __future__ import absolute_import
from __future__ import print_function
-import re
from time import sleep
from .NotifyBase import NotifyBase
from ..common import NotifyImageSize
+from ..common import NotifyType
# Default our global support flag
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
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
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
# in an environment that simply doesn't have the windows packages
# available to us. It also allows us to handle situations where the
@@ -100,7 +108,7 @@ class NotifyWindows(NotifyBase):
return None
- def notify(self, title, body, notify_type, **kwargs):
+ def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs):
"""
Perform Windows Notification
"""
@@ -110,11 +118,8 @@ class NotifyWindows(NotifyBase):
"Windows Notifications are not supported by this system.")
return False
- # 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])
+ # Always call throttle before any remote server i/o is made
+ self.throttle()
try:
# Register destruction callback
@@ -168,13 +173,20 @@ class NotifyWindows(NotifyBase):
self.logger.info('Sent Windows notification.')
- except Exception as e:
+ except Exception:
self.logger.warning('Failed to send Windows notification.')
self.logger.exception('Windows Exception')
return False
return True
+ def url(self):
+ """
+ Returns the URL built dynamically based on specified arguments.
+ """
+
+ return '{schema}://'.format(schema=self.protocol)
+
@staticmethod
def parse_url(url):
"""
diff --git a/apprise/plugins/NotifyXBMC.py b/apprise/plugins/NotifyXBMC.py
index 264e8fad..f4970453 100644
--- a/apprise/plugins/NotifyXBMC.py
+++ b/apprise/plugins/NotifyXBMC.py
@@ -23,7 +23,6 @@
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
-import re
import requests
from json import dumps
@@ -44,15 +43,28 @@ class NotifyXBMC(NotifyBase):
# The services URL
service_url = 'http://kodi.tv/'
+ xbmc_protocol = 'xbmc'
+ xbmc_secure_protocol = 'xbmcs'
+ kodi_protocol = 'kodi'
+ kodi_secure_protocol = 'kodis'
+
# The default protocols
- protocol = ('xbmc', 'kodi')
+ protocol = (xbmc_protocol, kodi_protocol)
# 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
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_default_port = 8080
@@ -149,17 +161,11 @@ class NotifyXBMC(NotifyBase):
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
"""
- # 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:
# XBMC v2.0
(headers, payload) = self._payload_20(
@@ -184,6 +190,10 @@ class NotifyXBMC(NotifyBase):
url, self.verify_certificate,
))
self.logger.debug('XBMC/KODI Payload: %s' % str(payload))
+
+ # Always call throttle before any remote server i/o is made
+ self.throttle()
+
try:
r = requests.post(
url,
@@ -224,6 +234,45 @@ class NotifyXBMC(NotifyBase):
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
def parse_url(url):
"""
diff --git a/apprise/plugins/NotifyXML.py b/apprise/plugins/NotifyXML.py
index 9b2f36b2..2aaba506 100644
--- a/apprise/plugins/NotifyXML.py
+++ b/apprise/plugins/NotifyXML.py
@@ -29,6 +29,7 @@ import requests
from .NotifyBase import NotifyBase
from .NotifyBase import HTTP_ERROR_MAP
from ..common import NotifyImageSize
+from ..common import NotifyType
from ..utils import compat_is_basestring
@@ -52,9 +53,17 @@ class NotifyXML(NotifyBase):
# Allows the user to specify the NotifyImageSize object
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
+
+ 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)
@@ -83,9 +92,51 @@ class NotifyXML(NotifyBase):
if not compat_is_basestring(self.fullpath):
self.fullpath = '/'
+ self.headers = {}
+ if headers:
+ # Store our extra headers
+ self.headers.update(headers)
+
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
"""
@@ -96,8 +147,8 @@ class NotifyXML(NotifyBase):
'Content-Type': 'application/xml'
}
- if self.headers:
- headers.update(self.headers)
+ # Apply any/all header over-rides defined
+ headers.update(self.headers)
re_map = {
'{MESSAGE_TYPE}': NotifyBase.quote(notify_type),
@@ -126,6 +177,10 @@ class NotifyXML(NotifyBase):
url, self.verify_certificate,
))
self.logger.debug('XML Payload: %s' % str(payload))
+
+ # Always call throttle before any remote server i/o is made
+ self.throttle()
+
try:
r = requests.post(
url,
@@ -163,3 +218,23 @@ class NotifyXML(NotifyBase):
return False
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
diff --git a/apprise/utils.py b/apprise/utils.py
index 091c7e88..010fe7d9 100644
--- a/apprise/utils.py
+++ b/apprise/utils.py
@@ -32,14 +32,12 @@ try:
from urllib import unquote
from urllib import quote
from urlparse import urlparse
- from urlparse import parse_qsl
except ImportError:
# Python 3.x
from urllib.parse import unquote
from urllib.parse import quote
from urllib.parse import urlparse
- from urllib.parse import parse_qsl
import logging
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.*)\s*')
+NOTIFY_CUSTOM_DEL_TOKENS = re.compile(r'^-(?P.*)\s*')
+
# Used for attempting to acquire the schema if the URL can't be parsed.
GET_SCHEMA_RE = re.compile(r'\s*(?P[a-z0-9]{2,9})://.*$', re.I)
@@ -143,6 +147,81 @@ def tidy_path(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):
"""A function that greatly simplifies the parsing of a url
specified by the end user.
@@ -190,10 +269,17 @@ def parse_url(url, default_schema='http', verify_host=True):
'schema': None,
# The schema
'url': None,
- # The arguments passed in (the parsed query)
- # This is in a dictionary of {'key': 'val', etc }
+ # 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 = 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 = ''
@@ -220,6 +306,11 @@ def parse_url(url, default_schema='http', verify_host=True):
# No qsdata
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
parsed = urlparse('http://%s' % host)
@@ -231,6 +322,7 @@ def parse_url(url, default_schema='http', verify_host=True):
return None
result['fullpath'] = quote(unquote(tidy_path(parsed[2].strip())))
+
try:
# Handle trailing slashes removed by tidy_path
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
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']:
# Default
result['fullpath'] = None
@@ -397,6 +479,10 @@ def parse_list(*args):
elif isinstance(arg, (set, list, tuple)):
result += parse_list(*arg)
+ elif arg is None:
+ # Ignore
+ continue
+
else:
# Convert whatever it is to a string and work with it
result += parse_list(str(arg))
diff --git a/setup.cfg b/setup.cfg
index f1e329a3..5ae99e9a 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -11,6 +11,12 @@ exclude = .eggs,.tox,gntp,tweepy,pushjet
ignore = E722,W503,W504
statistics = true
+[flake8]
+# We exclude packages we don't maintain
+exclude = .eggs,.tox,gntp,tweepy,pushjet
+ignore = E722,W503,W504
+statistics = true
+
[aliases]
test=pytest
diff --git a/test/test_api.py b/test/test_api.py
index fa6ffada..bf499a65 100644
--- a/test/test_api.py
+++ b/test/test_api.py
@@ -71,15 +71,34 @@ def test_apprise():
a = Apprise(servers=servers)
- # 3 servers loaded
+ # 2 servers loaded
assert(len(a) == 2)
+ # We can retrieve our URLs this way:
+ assert(len(a.urls()) == 2)
+
# We can add another server
assert(
a.add('mmosts://mattermost.server.local/'
'3ccdd113474722377935511fc85d3dd4') is True)
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
a.clear()
assert(len(a) == 0)
diff --git a/test/test_email_plugin.py b/test/test_email_plugin.py
index bbd89c8d..a067b65c 100644
--- a/test/test_email_plugin.py
+++ b/test/test_email_plugin.py
@@ -26,6 +26,7 @@
from apprise import plugins
from apprise import NotifyType
from apprise import Apprise
+from apprise.utils import compat_is_basestring
from apprise.plugins import NotifyEmailBase
import smtplib
@@ -49,7 +50,7 @@ TEST_URLS = (
# No Username
('mailtos://:pass@nuxref.com:567', {
# Can't prepare a To address using this expression
- 'exception': TypeError,
+ 'instance': TypeError,
}),
# Pre-Configured Email Services
@@ -115,27 +116,27 @@ TEST_URLS = (
}),
# Invalid From Address
('mailtos://user:pass@nuxref.com?from=@', {
- 'exception': TypeError,
+ 'instance': TypeError,
}),
# Invalid From Address
('mailtos://nuxref.com?user=&pass=.', {
- 'exception': TypeError,
+ 'instance': TypeError,
}),
# Invalid To Address
('mailtos://user:pass@nuxref.com?to=@', {
- 'exception': TypeError,
+ 'instance': TypeError,
}),
# Valid URL, but can't structure a proper email
('mailtos://nuxref.com?user=%20!&pass=.', {
- 'exception': TypeError,
+ 'instance': TypeError,
}),
# Invalid From (and To) Address
('mailtos://nuxref.com?to=test', {
- 'exception': TypeError,
+ 'instance': TypeError,
}),
# Invalid Secure Mode
('mailtos://user:pass@example.com?mode=notamode', {
- 'exception': TypeError,
+ 'instance': TypeError,
}),
# STARTTLS flag checking
('mailtos://user:pass@gmail.com?mode=starttls', {
@@ -165,6 +166,8 @@ def test_email_plugin(mock_smtp, mock_smtpssl):
API: NotifyEmail Plugin()
"""
+ # Disable Throttling to speed testing
+ plugins.NotifyBase.NotifyBase.request_rate_per_sec = 0
# iterate over our dictionary and test it out
for (url, meta) in TEST_URLS:
@@ -172,9 +175,6 @@ def test_email_plugin(mock_smtp, mock_smtpssl):
# Our expected instance
instance = meta.get('instance', None)
- # Our expected exception
- exception = meta.get('exception', None)
-
# Our expected server objects
self = meta.get('self', None)
@@ -217,19 +217,37 @@ def test_email_plugin(mock_smtp, mock_smtpssl):
try:
obj = Apprise.instantiate(url, suppress_exceptions=False)
- assert(exception is None)
-
if obj is None:
- # We're done
+ # We're done (assuming this is what we were expecting)
+ assert instance is None
continue
if instance is None:
# 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(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:
# Iterate over our expected entries inside of our object
for key, val in self.items():
@@ -256,18 +274,19 @@ def test_email_plugin(mock_smtp, mock_smtpssl):
# Don't mess with these entries
raise
- except Exception as e:
+ except Exception:
# We can't handle this exception type
- print('%s / %s' % (url, str(e)))
- assert False
+ raise
except AssertionError:
# Don't mess with these entries
+ print('%s AssertionError' % url)
raise
except Exception as e:
# Check that we were expecting this exception to happen
- assert isinstance(e, response)
+ if not isinstance(e, response):
+ raise
except AssertionError:
# Don't mess with these entries
@@ -276,9 +295,11 @@ def test_email_plugin(mock_smtp, mock_smtpssl):
except Exception as e:
# Handle our exception
- print('%s / %s' % (url, str(e)))
- assert(exception is not None)
- assert(isinstance(e, exception))
+ if(instance is None):
+ raise
+
+ if not isinstance(e, instance):
+ raise
@mock.patch('smtplib.SMTP')
@@ -323,34 +344,23 @@ def test_smtplib_init_fail(mock_smtplib):
API: Test exception handling when calling smtplib.SMTP()
"""
+ # Disable Throttling to speed testing
+ plugins.NotifyBase.NotifyBase.request_rate_per_sec = 0
obj = Apprise.instantiate(
'mailto://user:pass@gmail.com', suppress_exceptions=False)
assert(isinstance(obj, plugins.NotifyEmail))
# Support Exception handling of smtplib.SMTP
- mock_smtplib.side_effect = TypeError('Test')
+ mock_smtplib.side_effect = RuntimeError('Test')
- try:
- obj.notify(
- 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
+ assert obj.notify(
+ body='body', title='test', notify_type=NotifyType.INFO) is False
# A handled and expected exception
mock_smtplib.side_effect = smtplib.SMTPException('Test')
- assert obj.notify(title='test', body='body',
- notify_type=NotifyType.INFO) is False
+ assert obj.notify(
+ body='body', title='test', notify_type=NotifyType.INFO) is False
@mock.patch('smtplib.SMTP')
@@ -359,6 +369,8 @@ def test_smtplib_send_okay(mock_smtplib):
API: Test a successfully sent email
"""
+ # Disable Throttling to speed testing
+ plugins.NotifyBase.NotifyBase.request_rate_per_sec = 0
# Defaults to HTML
obj = Apprise.instantiate(
@@ -372,7 +384,7 @@ def test_smtplib_send_okay(mock_smtplib):
mock_smtplib.quit.return_value = True
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
obj = Apprise.instantiate(
@@ -380,4 +392,4 @@ def test_smtplib_send_okay(mock_smtplib):
assert(isinstance(obj, plugins.NotifyEmail))
assert(obj.notify(
- title='test', body='body', notify_type=NotifyType.INFO) is True)
+ body='body', title='test', notify_type=NotifyType.INFO) is True)
diff --git a/test/test_glib_plugin.py b/test/test_glib_plugin.py
index 98d5e6b4..c2650f1a 100644
--- a/test/test_glib_plugin.py
+++ b/test/test_glib_plugin.py
@@ -28,6 +28,7 @@ import mock
import sys
import types
import apprise
+from apprise.utils import compat_is_basestring
try:
# 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)
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
assert(obj.notify(title='title', body='body',
notify_type=apprise.NotifyType.INFO) is True)
diff --git a/test/test_gnome_plugin.py b/test/test_gnome_plugin.py
index 150293c9..ef66e8d2 100644
--- a/test/test_gnome_plugin.py
+++ b/test/test_gnome_plugin.py
@@ -28,6 +28,7 @@ import sys
import types
import apprise
+from apprise.utils import compat_is_basestring
try:
# Python v3.4+
@@ -113,6 +114,9 @@ def test_gnome_plugin():
# Check that it found our mocked environments
assert(obj._enabled is True)
+ # Test url() call
+ assert(compat_is_basestring(obj.url()) is True)
+
# test notifications
assert(obj.notify(title='title', body='body',
notify_type=apprise.NotifyType.INFO) is True)
diff --git a/test/test_growl_plugin.py b/test/test_growl_plugin.py
index 289f726f..e92742f2 100644
--- a/test/test_growl_plugin.py
+++ b/test/test_growl_plugin.py
@@ -26,8 +26,9 @@
from apprise import plugins
from apprise import NotifyType
from apprise import Apprise
+from apprise.utils import compat_is_basestring
+
import mock
-import re
TEST_URLS = (
@@ -193,9 +194,8 @@ def test_growl_plugin(mock_gntp):
# This is the response we expect
assert True
- except Exception as e:
+ except Exception:
# We can't handle this exception type
- print('%s / %s' % (url, str(e)))
assert False
# We're done this part of the test
@@ -216,10 +216,27 @@ def test_growl_plugin(mock_gntp):
if instance is None:
# Expected None but didn't get it
- print('%s instantiated %s' % (url, str(obj)))
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:
# Iterate over our expected entries inside of our object
diff --git a/test/test_notify_base.py b/test/test_notify_base.py
index dc3ee937..4ee8de0d 100644
--- a/test/test_notify_base.py
+++ b/test/test_notify_base.py
@@ -23,6 +23,9 @@
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
+from datetime import datetime
+from datetime import timedelta
+
from apprise.plugins.NotifyBase import NotifyBase
from apprise import NotifyType
from apprise import NotifyImageSize
@@ -45,6 +48,15 @@ def test_notify_base():
except TypeError:
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
nb = NotifyBase(port='invalid')
assert nb.port is None
@@ -52,9 +64,29 @@ def test_notify_base():
nb = NotifyBase(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..
nb = NotifyBase()
- nb.throttle_attempt = 0.0
+ nb.request_rate_per_sec = 0.0
start_time = default_timer()
nb.throttle()
elapsed = default_timer() - start_time
@@ -63,13 +95,57 @@ def test_notify_base():
# then other
assert elapsed < 0.5
+ # Concurrent calls should achieve the same response
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
# 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
# then other
- assert elapsed < 1.5
+ assert elapsed < 0.5
# our NotifyBase wasn't initialized with an ImageSize so this will fail
assert nb.image_url(notify_type=NotifyType.INFO) is None
@@ -166,11 +242,30 @@ def test_notify_base_urls():
assert 'password' in results
assert results['password'] == "newpassword"
- # pass headers
- results = NotifyBase.parse_url(
- 'https://localhost:8080?-HeaderKey=HeaderValue')
- assert 'headerkey' in results['headers']
- assert results['headers']['headerkey'] == 'HeaderValue'
+ # Options
+ results = NotifyBase.parse_url('https://localhost?format=invalid')
+ assert 'format' not in results
+ results = NotifyBase.parse_url('https://localhost?format=text')
+ 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
diff --git a/test/test_pushjet_plugin.py b/test/test_pushjet_plugin.py
index 789d4844..29642223 100644
--- a/test/test_pushjet_plugin.py
+++ b/test/test_pushjet_plugin.py
@@ -26,6 +26,8 @@
from apprise import plugins
from apprise import NotifyType
from apprise import Apprise
+from apprise.utils import compat_is_basestring
+
import mock
TEST_URLS = (
@@ -104,13 +106,37 @@ def test_plugin(mock_refresh, mock_send):
try:
obj = Apprise.instantiate(url, suppress_exceptions=False)
- if instance is None:
- # Check that we got what we came for
- assert obj is instance
+ if obj is None:
+ # We're done (assuming this is what we were expecting)
+ assert instance is None
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))
+ 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:
# Iterate over our expected entries inside of our object
for key, val in self.items():
@@ -142,23 +168,29 @@ def test_plugin(mock_refresh, mock_send):
# Don't mess with these entries
raise
- except Exception as e:
+ except Exception:
# We can't handle this exception type
- assert False
+ raise
except AssertionError:
# Don't mess with these entries
+ print('%s AssertionError' % url)
raise
except Exception as e:
# Check that we were expecting this exception to happen
- assert isinstance(e, response)
+ if not isinstance(e, response):
+ raise
except AssertionError:
# Don't mess with these entries
+ print('%s AssertionError' % url)
raise
except Exception as e:
# Handle our exception
- assert(instance is not None)
- assert(isinstance(e, instance))
+ if(instance is None):
+ raise
+
+ if not isinstance(e, instance):
+ raise
diff --git a/test/test_rest_plugins.py b/test/test_rest_plugins.py
index bee5fbea..8128674b 100644
--- a/test/test_rest_plugins.py
+++ b/test/test_rest_plugins.py
@@ -25,12 +25,18 @@
from apprise import plugins
from apprise import NotifyType
+from apprise import NotifyBase
from apprise import Apprise
from apprise import AppriseAsset
from apprise.utils import compat_is_basestring
from apprise.common import NotifyFormat
+from apprise.common import OverflowMode
from json import dumps
+from random import choice
+from string import ascii_uppercase as str_alpha
+from string import digits as str_num
+
import requests
import mock
@@ -263,42 +269,21 @@ TEST_URLS = (
('ifttt://EventID/', {
'instance': TypeError,
}),
- # Value1 gets assigned Entry1
- # Title =
- # Body =
- ('ifttt://WebHookID@EventID/Entry1/', {
- 'instance': plugins.NotifyIFTTT,
- }),
- # Value1, Value2, and Value2, the below assigns:
- # Value1 = Entry1
- # Value2 = AnotherEntry
- # Value3 = ThirdValue
- # Title =
- # Body =
- ('ifttt://WebHookID@EventID/Entry1/AnotherEntry/ThirdValue', {
- 'instance': plugins.NotifyIFTTT,
- }),
- # Mix and match content, the below assigns:
- # Value1 = FirstValue
- # AnotherKey = Hello
- # Value5 = test
- # Title =
- # Body =
- ('ifttt://WebHookID@EventID/FirstValue/?AnotherKey=Hello&Value5=test', {
- 'instance': plugins.NotifyIFTTT,
- }),
- # This would assign:
- # Value1 = FirstValue
- # Title = - disable the one passed by the notify call
- # Body = - disable the one passed by the notify call
- # The idea here is maybe you just want to use the apprise IFTTTT hook
- # to trigger something and not nessisarily pass text along to it
- ('ifttt://WebHookID@EventID/FirstValue/?Title=&Body=', {
- 'instance': plugins.NotifyIFTTT,
- }),
('ifttt://:@/', {
'instance': None,
}),
+ # A nicely formed ifttt url with 1 event and a new key/value store
+ ('ifttt://WebHookID@EventID/?+TemplateKey=TemplateVal', {
+ 'instance': plugins.NotifyIFTTT,
+ }),
+ # Removing certain keys:
+ ('ifttt://WebHookID@EventID/?-Value1=&-Value2', {
+ 'instance': plugins.NotifyIFTTT,
+ }),
+ # A nicely formed ifttt url with 2 events defined:
+ ('ifttt://WebHookID@EventID/EventID2/', {
+ 'instance': plugins.NotifyIFTTT,
+ }),
# Test website connection failures
('ifttt://WebHookID@EventID', {
'instance': plugins.NotifyIFTTT,
@@ -392,6 +377,9 @@ TEST_URLS = (
('json://user:pass@localhost', {
'instance': plugins.NotifyJSON,
}),
+ ('json://user@localhost', {
+ 'instance': plugins.NotifyJSON,
+ }),
('json://localhost:8080', {
'instance': plugins.NotifyJSON,
}),
@@ -529,8 +517,8 @@ TEST_URLS = (
('matrix://user@localhost/%s' % ('a' * 64), {
'instance': plugins.NotifyMatrix,
}),
- # Name, port and token (secure)
- ('matrixs://user@localhost:9000/%s' % ('a' * 64), {
+ # port and token (secure)
+ ('matrixs://localhost:9000/%s' % ('a' * 64), {
'instance': plugins.NotifyMatrix,
}),
# Name, port, token and slack mode
@@ -963,6 +951,14 @@ TEST_URLS = (
('rocket://user:pass@localhost/#/!/@', {
'instance': TypeError,
}),
+ # No user/pass combo
+ ('rocket://user@localhost/room/', {
+ 'instance': TypeError,
+ }),
+ # No user/pass combo
+ ('rocket://localhost/room/', {
+ 'instance': TypeError,
+ }),
# A room and port identifier
('rocket://user:pass@localhost:8080/room/', {
'instance': plugins.NotifyRocketChat,
@@ -1383,7 +1379,7 @@ TEST_URLS = (
('xbmc://user:pass@localhost:8080', {
'instance': plugins.NotifyXBMC,
}),
- ('xbmc://localhost', {
+ ('xbmc://user@localhost', {
'instance': plugins.NotifyXBMC,
# don't include an image by default
'include_image': False,
@@ -1432,6 +1428,9 @@ TEST_URLS = (
('xml://localhost', {
'instance': plugins.NotifyXML,
}),
+ ('xml://user@localhost', {
+ 'instance': plugins.NotifyXML,
+ }),
('xml://user:pass@localhost', {
'instance': plugins.NotifyXML,
}),
@@ -1487,6 +1486,22 @@ def test_rest_plugins(mock_post, mock_get):
API: REST Based Plugins()
"""
+ # Disable Throttling to speed testing
+ plugins.NotifyBase.NotifyBase.request_rate_per_sec = 0
+
+ # Define how many characters exist per line
+ row = 80
+
+ # Some variables we use to control the data we work with
+ body_len = 1024
+ title_len = 1024
+
+ # Create a large body and title with random data
+ body = ''.join(choice(str_alpha + str_num + ' ') for _ in range(body_len))
+ body = '\r\n'.join([body[i: i + row] for i in range(0, len(body), row)])
+
+ # Create our title using random data
+ title = ''.join(choice(str_alpha + str_num) for _ in range(title_len))
# iterate over our dictionary and test it out
for (url, meta) in TEST_URLS:
@@ -1567,10 +1582,31 @@ def test_rest_plugins(mock_post, mock_get):
assert instance is None
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))
- # Disable throttling to speed up unit tests
- obj.throttle_attempt = 0
+ 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:
# Iterate over our expected entries inside of our object
@@ -1584,29 +1620,49 @@ def test_rest_plugins(mock_post, mock_get):
#
try:
if test_requests_exceptions is False:
+ # Disable throttling
+ obj.request_rate_per_sec = 0
+
# check that we're as expected
assert obj.notify(
- title='test', body='body',
+ body=body, title=title,
notify_type=notify_type) == response
+ # check that this doesn't change using different overflow
+ # methods
+ assert obj.notify(
+ body=body, title=title,
+ notify_type=notify_type,
+ overflow=OverflowMode.UPSTREAM) == response
+ assert obj.notify(
+ body=body, title=title,
+ notify_type=notify_type,
+ overflow=OverflowMode.TRUNCATE) == response
+ assert obj.notify(
+ body=body, title=title,
+ notify_type=notify_type,
+ overflow=OverflowMode.SPLIT) == response
+
else:
+ # Disable throttling
+ obj.request_rate_per_sec = 0
+
for _exception in REQUEST_EXCEPTIONS:
mock_post.side_effect = _exception
mock_get.side_effect = _exception
try:
assert obj.notify(
- title='test', body='body',
+ body=body, title=title,
notify_type=NotifyType.INFO) is False
except AssertionError:
# Don't mess with these entries
raise
- except Exception as e:
+ except Exception:
# We can't handle this exception type
- print('%s / %s' % (url, str(e)))
- assert False
+ raise
except AssertionError:
# Don't mess with these entries
@@ -1615,7 +1671,8 @@ def test_rest_plugins(mock_post, mock_get):
except Exception as e:
# Check that we were expecting this exception to happen
- assert isinstance(e, response)
+ if not isinstance(e, response):
+ raise
#
# Stage 2: without title defined
@@ -1624,8 +1681,7 @@ def test_rest_plugins(mock_post, mock_get):
if test_requests_exceptions is False:
# check that we're as expected
assert obj.notify(
- title='', body='body',
- notify_type=notify_type) == response
+ body='body', notify_type=notify_type) == response
else:
for _exception in REQUEST_EXCEPTIONS:
@@ -1634,17 +1690,16 @@ def test_rest_plugins(mock_post, mock_get):
try:
assert obj.notify(
- title='', body='body',
+ body=body,
notify_type=NotifyType.INFO) is False
except AssertionError:
# Don't mess with these entries
raise
- except Exception as e:
+ except Exception:
# We can't handle this exception type
- print('%s / %s' % (url, str(e)))
- assert False
+ raise
except AssertionError:
# Don't mess with these entries
@@ -1653,7 +1708,8 @@ def test_rest_plugins(mock_post, mock_get):
except Exception as e:
# Check that we were expecting this exception to happen
- assert isinstance(e, response)
+ if not isinstance(e, response):
+ raise
except AssertionError:
# Don't mess with these entries
@@ -1662,9 +1718,11 @@ def test_rest_plugins(mock_post, mock_get):
except Exception as e:
# Handle our exception
- print('%s / %s' % (url, str(e)))
- assert(instance is not None)
- assert(isinstance(e, instance))
+ if(instance is None):
+ raise
+
+ if not isinstance(e, instance):
+ raise
@mock.patch('requests.get')
@@ -1674,6 +1732,9 @@ def test_notify_boxcar_plugin(mock_post, mock_get):
API: NotifyBoxcar() Extra Checks
"""
+ # Disable Throttling to speed testing
+ plugins.NotifyBase.NotifyBase.request_rate_per_sec = 0
+
# Generate some generic message types
device = 'A' * 64
tag = '@B' * 63
@@ -1724,9 +1785,6 @@ def test_notify_boxcar_plugin(mock_post, mock_get):
# Test notifications without a body or a title
p = plugins.NotifyBoxcar(access=access, secret=secret, recipients=None)
- # Disable throttling to speed up unit tests
- p.throttle_attempt = 0
-
p.notify(body=None, title=None, notify_type=NotifyType.INFO) is True
@@ -1737,6 +1795,8 @@ def test_notify_discord_plugin(mock_post, mock_get):
API: NotifyDiscord() Extra Checks
"""
+ # Disable Throttling to speed testing
+ plugins.NotifyBase.NotifyBase.request_rate_per_sec = 0
# Initialize some generic (but valid) tokens
webhook_id = 'A' * 24
@@ -1762,12 +1822,9 @@ def test_notify_discord_plugin(mock_post, mock_get):
webhook_token=webhook_token,
footer=True, thumbnail=False)
- # Disable throttling to speed up unit tests
- obj.throttle_attempt = 0
-
# This call includes an image with it's payload:
- assert obj.notify(title='title', body='body',
- notify_type=NotifyType.INFO) is True
+ assert obj.notify(
+ body='body', title='title', notify_type=NotifyType.INFO) is True
# Test our header parsing
test_markdown = "## Heading one\nbody body\n\n" + \
@@ -1785,8 +1842,8 @@ def test_notify_discord_plugin(mock_post, mock_get):
assert(len(results) == 5)
# Use our test markdown string during a notification
- assert obj.notify(title='title', body=test_markdown,
- notify_type=NotifyType.INFO) is True
+ assert obj.notify(
+ body=test_markdown, title='title', notify_type=NotifyType.INFO) is True
# Create an apprise instance
a = Apprise()
@@ -1800,18 +1857,18 @@ def test_notify_discord_plugin(mock_post, mock_get):
webhook_token=webhook_token)) is True
# This call includes an image with it's payload:
- assert a.notify(title='title', body=test_markdown,
+ assert a.notify(body=test_markdown, title='title',
notify_type=NotifyType.INFO,
body_format=NotifyFormat.TEXT) is True
- assert a.notify(title='title', body=test_markdown,
+ assert a.notify(body=test_markdown, title='title',
notify_type=NotifyType.INFO,
body_format=NotifyFormat.MARKDOWN) is True
# Toggle our logo availability
a.asset.image_url_logo = None
- assert a.notify(title='title', body='body',
- notify_type=NotifyType.INFO) is True
+ assert a.notify(
+ body='body', title='title', notify_type=NotifyType.INFO) is True
@mock.patch('requests.get')
@@ -1821,6 +1878,8 @@ def test_notify_emby_plugin_login(mock_post, mock_get):
API: NotifyEmby.login()
"""
+ # Disable Throttling to speed testing
+ plugins.NotifyBase.NotifyBase.request_rate_per_sec = 0
# Prepare Mock
mock_get.return_value = requests.Request()
@@ -1946,6 +2005,8 @@ def test_notify_emby_plugin_sessions(mock_post, mock_get, mock_logout,
API: NotifyEmby.sessions()
"""
+ # Disable Throttling to speed testing
+ plugins.NotifyBase.NotifyBase.request_rate_per_sec = 0
# Prepare Mock
mock_get.return_value = requests.Request()
@@ -2047,6 +2108,8 @@ def test_notify_emby_plugin_logout(mock_post, mock_get, mock_login):
API: NotifyEmby.sessions()
"""
+ # Disable Throttling to speed testing
+ plugins.NotifyBase.NotifyBase.request_rate_per_sec = 0
# Prepare Mock
mock_get.return_value = requests.Request()
@@ -2115,6 +2178,8 @@ def test_notify_emby_plugin_notify(mock_post, mock_get, mock_logout,
API: NotifyEmby.notify()
"""
+ # Disable Throttling to speed testing
+ plugins.NotifyBase.NotifyBase.request_rate_per_sec = 0
# Prepare Mock
mock_get.return_value = requests.Request()
@@ -2190,10 +2255,12 @@ def test_notify_ifttt_plugin(mock_post, mock_get):
API: NotifyIFTTT() Extra Checks
"""
+ # Disable Throttling to speed testing
+ plugins.NotifyBase.NotifyBase.request_rate_per_sec = 0
# Initialize some generic (but valid) tokens
- apikey = 'webhookid'
- event = 'event'
+ webhook_id = 'webhookid'
+ events = ['event1', 'event2']
# Prepare Mock
mock_get.return_value = requests.Request()
@@ -2204,7 +2271,7 @@ def test_notify_ifttt_plugin(mock_post, mock_get):
mock_post.return_value.content = '{}'
try:
- obj = plugins.NotifyIFTTT(apikey=apikey, event=None, event_args=None)
+ obj = plugins.NotifyIFTTT(webhook_id=webhook_id, events=None)
# No token specified
assert(False)
@@ -2212,15 +2279,56 @@ def test_notify_ifttt_plugin(mock_post, mock_get):
# Exception should be thrown about the fact no token was specified
assert(True)
- obj = plugins.NotifyIFTTT(apikey=apikey, event=event, event_args=None)
+ obj = plugins.NotifyIFTTT(webhook_id=webhook_id, events=events)
assert(isinstance(obj, plugins.NotifyIFTTT))
- assert(len(obj.event_args) == 0)
- # Disable throttling to speed up unit tests
- obj.throttle_attempt = 0
+ assert obj.notify(
+ body='body', title='title', notify_type=NotifyType.INFO) is True
- assert obj.notify(title='title', body='body',
- notify_type=NotifyType.INFO) is True
+ # Test the addition of tokens
+ obj = plugins.NotifyIFTTT(
+ webhook_id=webhook_id, events=events,
+ add_tokens={'Test': 'ValueA', 'Test2': 'ValueB'})
+
+ assert(isinstance(obj, plugins.NotifyIFTTT))
+
+ assert obj.notify(
+ body='body', title='title', notify_type=NotifyType.INFO) is True
+
+ try:
+ # Invalid del_tokens entry
+ obj = plugins.NotifyIFTTT(
+ webhook_id=webhook_id, events=events,
+ del_tokens=plugins.NotifyIFTTT.ifttt_default_title_key)
+
+ # we shouldn't reach here
+ assert False
+
+ except TypeError:
+ # del_tokens must be a list, so passing a string will throw
+ # an exception.
+ assert True
+
+ assert(isinstance(obj, plugins.NotifyIFTTT))
+
+ assert obj.notify(
+ body='body', title='title', notify_type=NotifyType.INFO) is True
+
+ # Test removal of tokens by a list
+ obj = plugins.NotifyIFTTT(
+ webhook_id=webhook_id, events=events,
+ add_tokens={
+ 'MyKey': 'MyValue'
+ },
+ del_tokens=(
+ plugins.NotifyIFTTT.ifttt_default_title_key,
+ plugins.NotifyIFTTT.ifttt_default_body_key,
+ plugins.NotifyIFTTT.ifttt_default_type_key))
+
+ assert(isinstance(obj, plugins.NotifyIFTTT))
+
+ assert obj.notify(
+ body='body', title='title', notify_type=NotifyType.INFO) is True
@mock.patch('requests.get')
@@ -2230,6 +2338,9 @@ def test_notify_join_plugin(mock_post, mock_get):
API: NotifyJoin() Extra Checks
"""
+ # Disable Throttling to speed testing
+ plugins.NotifyBase.NotifyBase.request_rate_per_sec = 0
+
# Generate some generic message types
device = 'A' * 32
group = 'group.chrome'
@@ -2250,9 +2361,6 @@ def test_notify_join_plugin(mock_post, mock_get):
mock_post.return_value.status_code = requests.codes.created
mock_get.return_value.status_code = requests.codes.created
- # Disable throttling to speed up unit tests
- p.throttle_attempt = 0
-
# Test notifications without a body or a title; nothing to send
# so we return False
p.notify(body=None, title=None, notify_type=NotifyType.INFO) is False
@@ -2265,6 +2373,8 @@ def test_notify_slack_plugin(mock_post, mock_get):
API: NotifySlack() Extra Checks
"""
+ # Disable Throttling to speed testing
+ plugins.NotifyBase.NotifyBase.request_rate_per_sec = 0
# Initialize some generic (but valid) tokens
token_a = 'A' * 9
@@ -2300,12 +2410,9 @@ def test_notify_slack_plugin(mock_post, mock_get):
token_a=token_a, token_b=token_b, token_c=token_c, channels=channels,
include_image=True)
- # Disable throttling to speed up unit tests
- obj.throttle_attempt = 0
-
# This call includes an image with it's payload:
- assert obj.notify(title='title', body='body',
- notify_type=NotifyType.INFO) is True
+ assert obj.notify(
+ body='body', title='title', notify_type=NotifyType.INFO) is True
@mock.patch('requests.get')
@@ -2315,6 +2422,8 @@ def test_notify_pushbullet_plugin(mock_post, mock_get):
API: NotifyPushBullet() Extra Checks
"""
+ # Disable Throttling to speed testing
+ plugins.NotifyBase.NotifyBase.request_rate_per_sec = 0
# Initialize some generic (but valid) tokens
accesstoken = 'a' * 32
@@ -2358,6 +2467,9 @@ def test_notify_pushed_plugin(mock_post, mock_get):
API: NotifyPushed() Extra Checks
"""
+ # Disable Throttling to speed testing
+ plugins.NotifyBase.NotifyBase.request_rate_per_sec = 0
+
# Chat ID
recipients = '@ABCDEFG, @DEFGHIJ, #channel, #channel2'
@@ -2436,9 +2548,6 @@ def test_notify_pushed_plugin(mock_post, mock_get):
assert(len(obj.channels) == 2)
assert(len(obj.users) == 2)
- # Disable throttling to speed up unit tests
- obj.throttle_attempt = 0
-
# Support the handling of an empty and invalid URL strings
assert plugins.NotifyPushed.parse_url(None) is None
assert plugins.NotifyPushed.parse_url('') is None
@@ -2458,6 +2567,8 @@ def test_notify_pushover_plugin(mock_post, mock_get):
API: NotifyPushover() Extra Checks
"""
+ # Disable Throttling to speed testing
+ plugins.NotifyBase.NotifyBase.request_rate_per_sec = 0
# Initialize some generic (but valid) tokens
token = 'a' * 30
@@ -2487,12 +2598,9 @@ def test_notify_pushover_plugin(mock_post, mock_get):
assert(isinstance(obj, plugins.NotifyPushover))
assert(len(obj.devices) == 3)
- # Disable throttling to speed up unit tests
- obj.throttle_attempt = 0
-
# This call fails because there is 1 invalid device
- assert obj.notify(title='title', body='body',
- notify_type=NotifyType.INFO) is False
+ assert obj.notify(
+ body='body', title='title', notify_type=NotifyType.INFO) is False
obj = plugins.NotifyPushover(user=user, token=token)
assert(isinstance(obj, plugins.NotifyPushover))
@@ -2500,12 +2608,9 @@ def test_notify_pushover_plugin(mock_post, mock_get):
# device defined here
assert(len(obj.devices) == 1)
- # Disable throttling to speed up unit tests
- obj.throttle_attempt = 0
-
# This call succeeds because all of the devices are valid
- assert obj.notify(title='title', body='body',
- notify_type=NotifyType.INFO) is True
+ assert obj.notify(
+ body='body', title='title', notify_type=NotifyType.INFO) is True
obj = plugins.NotifyPushover(user=user, token=token, devices=set())
assert(isinstance(obj, plugins.NotifyPushover))
@@ -2526,9 +2631,16 @@ def test_notify_rocketchat_plugin(mock_post, mock_get):
API: NotifyRocketChat() Extra Checks
"""
+ # Disable Throttling to speed testing
+ plugins.NotifyBase.NotifyBase.request_rate_per_sec = 0
+
# Chat ID
recipients = 'l2g, lead2gold, #channel, #channel2'
+ # Authentication
+ user = 'myuser'
+ password = 'mypass'
+
# Prepare Mock
mock_get.return_value = requests.Request()
mock_post.return_value = requests.Request()
@@ -2538,7 +2650,8 @@ def test_notify_rocketchat_plugin(mock_post, mock_get):
mock_get.return_value.text = ''
try:
- obj = plugins.NotifyRocketChat(recipients=None)
+ obj = plugins.NotifyRocketChat(
+ user=user, password=password, recipients=None)
# invalid recipients list (None)
assert(False)
@@ -2548,7 +2661,8 @@ def test_notify_rocketchat_plugin(mock_post, mock_get):
assert(True)
try:
- obj = plugins.NotifyRocketChat(recipients=object())
+ obj = plugins.NotifyRocketChat(
+ user=user, password=password, recipients=object())
# invalid recipients list (object)
assert(False)
@@ -2558,7 +2672,8 @@ def test_notify_rocketchat_plugin(mock_post, mock_get):
assert(True)
try:
- obj = plugins.NotifyRocketChat(recipients=set())
+ obj = plugins.NotifyRocketChat(
+ user=user, password=password, recipients=set())
# invalid recipient list/set (no entries)
assert(False)
@@ -2567,14 +2682,12 @@ def test_notify_rocketchat_plugin(mock_post, mock_get):
# specified
assert(True)
- obj = plugins.NotifyRocketChat(recipients=recipients)
+ obj = plugins.NotifyRocketChat(
+ user=user, password=password, recipients=recipients)
assert(isinstance(obj, plugins.NotifyRocketChat))
assert(len(obj.channels) == 2)
assert(len(obj.rooms) == 2)
- # Disable throttling to speed up unit tests
- obj.throttle_attempt = 0
-
#
# Logout
#
@@ -2595,9 +2708,8 @@ def test_notify_rocketchat_plugin(mock_post, mock_get):
# Send Notification
#
assert obj.notify(
- title='title', body='body', notify_type=NotifyType.INFO) is False
- assert obj.send_notification(
- payload='test', notify_type=NotifyType.INFO) is False
+ body='body', title='title', notify_type=NotifyType.INFO) is False
+ assert obj._send(payload='test', notify_type=NotifyType.INFO) is False
#
# Logout
@@ -2612,9 +2724,8 @@ def test_notify_rocketchat_plugin(mock_post, mock_get):
# Send Notification
#
assert obj.notify(
- title='title', body='body', notify_type=NotifyType.INFO) is False
- assert obj.send_notification(
- payload='test', notify_type=NotifyType.INFO) is False
+ body='body', title='title', notify_type=NotifyType.INFO) is False
+ assert obj._send(payload='test', notify_type=NotifyType.INFO) is False
#
# Logout
@@ -2632,14 +2743,13 @@ def test_notify_rocketchat_plugin(mock_post, mock_get):
#
# Send Notification
#
- assert obj.send_notification(
- payload='test', notify_type=NotifyType.INFO) is False
+ assert obj._send(payload='test', notify_type=NotifyType.INFO) is False
# Attempt the check again but fake a successful login
obj.login = mock.Mock()
obj.login.return_value = True
assert obj.notify(
- title='title', body='body', notify_type=NotifyType.INFO) is False
+ body='body', title='title', notify_type=NotifyType.INFO) is False
#
# Logout
#
@@ -2653,6 +2763,9 @@ def test_notify_telegram_plugin(mock_post, mock_get):
API: NotifyTelegram() Extra Checks
"""
+ # Disable Throttling to speed testing
+ plugins.NotifyBase.NotifyBase.request_rate_per_sec = 0
+
# Bot Token
bot_token = '123456789:abcdefg_hijklmnop'
invalid_bot_token = 'abcd:123'
@@ -2710,6 +2823,12 @@ def test_notify_telegram_plugin(mock_post, mock_get):
assert(isinstance(obj, plugins.NotifyTelegram))
assert(len(obj.chat_ids) == 2)
+ # test url call
+ assert(compat_is_basestring(obj.url()))
+ # Test that we can load the string we generate back:
+ obj = plugins.NotifyTelegram(**plugins.NotifyTelegram.parse_url(obj.url()))
+ assert(isinstance(obj, plugins.NotifyTelegram))
+
# Support the handling of an empty and invalid URL strings
assert(plugins.NotifyTelegram.parse_url(None) is None)
assert(plugins.NotifyTelegram.parse_url('') is None)
@@ -2730,10 +2849,6 @@ def test_notify_telegram_plugin(mock_post, mock_get):
nimg_obj = plugins.NotifyTelegram(bot_token=bot_token, chat_ids=chat_ids)
nimg_obj.asset = AppriseAsset(image_path_mask=False, image_url_mask=False)
- # Disable throttling to speed up unit tests
- nimg_obj.throttle_attempt = 0
- obj.throttle_attempt = 0
-
# Test that our default settings over-ride base settings since they are
# not the same as the one specified in the base; this check merely
# ensures our plugin inheritance is working properly
@@ -2745,9 +2860,11 @@ def test_notify_telegram_plugin(mock_post, mock_get):
# This tests erroneous messages involving multiple chat ids
assert obj.notify(
- title='title', body='body', notify_type=NotifyType.INFO) is False
+ body='body', title='title', notify_type=NotifyType.INFO) is False
+ assert obj.notify(
+ body='body', title='title', notify_type=NotifyType.INFO) is False
assert nimg_obj.notify(
- title='title', body='body', notify_type=NotifyType.INFO) is False
+ body='body', title='title', notify_type=NotifyType.INFO) is False
# This tests erroneous messages involving a single chat id
obj = plugins.NotifyTelegram(bot_token=bot_token, chat_ids='l2g')
@@ -2755,9 +2872,9 @@ def test_notify_telegram_plugin(mock_post, mock_get):
nimg_obj.asset = AppriseAsset(image_path_mask=False, image_url_mask=False)
assert obj.notify(
- title='title', body='body', notify_type=NotifyType.INFO) is False
+ body='body', title='title', notify_type=NotifyType.INFO) is False
assert nimg_obj.notify(
- title='title', body='body', notify_type=NotifyType.INFO) is False
+ body='body', title='title', notify_type=NotifyType.INFO) is False
# Bot Token Detection
# Just to make it clear to people reading this code and trying to learn
@@ -2855,7 +2972,7 @@ def test_notify_telegram_plugin(mock_post, mock_get):
# notification without a bot detection by providing at least 1 chat id
obj = plugins.NotifyTelegram(bot_token=bot_token, chat_ids=['@abcd'])
assert nimg_obj.notify(
- title='title', body='body', notify_type=NotifyType.INFO) is False
+ body='body', title='title', notify_type=NotifyType.INFO) is False
# iterate over our exceptions and test them
for _exception in REQUEST_EXCEPTIONS:
@@ -2868,3 +2985,350 @@ def test_notify_telegram_plugin(mock_post, mock_get):
except TypeError:
# Exception should be thrown about the fact no token was specified
assert(True)
+
+
+def test_notify_overflow_truncate():
+ """
+ API: Overflow Truncate Functionality Testing
+
+ """
+ #
+ # A little preparation
+ #
+
+ # Disable Throttling to speed testing
+ plugins.NotifyBase.NotifyBase.request_rate_per_sec = 0
+
+ # Number of characters per line
+ row = 24
+
+ # Some variables we use to control the data we work with
+ body_len = 1024
+ title_len = 1024
+
+ # Create a large body and title with random data
+ body = ''.join(choice(str_alpha + str_num + ' ') for _ in range(body_len))
+ body = '\r\n'.join([body[i: i + row] for i in range(0, len(body), row)])
+
+ # the new lines add a large amount to our body; lets force the content
+ # back to being 1024 characters.
+ body = body[0:1024]
+
+ # Create our title using random data
+ title = ''.join(choice(str_alpha + str_num) for _ in range(title_len))
+
+ #
+ # First Test: Truncated Title
+ #
+ class TestNotification(NotifyBase):
+
+ # Test title max length
+ title_maxlen = 10
+
+ def __init__(self, *args, **kwargs):
+ super(TestNotification, self).__init__(**kwargs)
+
+ def notify(self, *args, **kwargs):
+ # Pretend everything is okay
+ return True
+
+ try:
+ # Load our object
+ obj = TestNotification(overflow='invalid')
+
+ # We should have thrown an exception because our specified overflow
+ # is wrong.
+ assert False
+
+ except TypeError:
+ # Expected to be here
+ assert True
+
+ # Load our object
+ obj = TestNotification(overflow=OverflowMode.TRUNCATE)
+ assert obj is not None
+
+ # Verify that we break the title to a max length of our title_max
+ # and that the body remains untouched
+ chunks = obj._apply_overflow(body=body, title=title, overflow=None)
+ chunks = obj._apply_overflow(
+ body=body, title=title, overflow=OverflowMode.SPLIT)
+ assert len(chunks) == 1
+ assert body == chunks[0].get('body')
+ assert title[0:TestNotification.title_maxlen] == chunks[0].get('title')
+
+ #
+ # Next Test: Line Count Control
+ #
+
+ class TestNotification(NotifyBase):
+
+ # Test title max length
+ title_maxlen = 5
+
+ # Maximum number of lines
+ body_max_line_count = 5
+
+ def __init__(self, *args, **kwargs):
+ super(TestNotification, self).__init__(**kwargs)
+
+ def notify(self, *args, **kwargs):
+ # Pretend everything is okay
+ return True
+
+ # Load our object
+ obj = TestNotification(overflow=OverflowMode.TRUNCATE)
+ assert obj is not None
+
+ # Verify that we break the title to a max length of our title_max
+ # and that the body remains untouched
+ chunks = obj._apply_overflow(body=body, title=title)
+ assert len(chunks) == 1
+ assert len(chunks[0].get('body').split('\n')) == \
+ TestNotification.body_max_line_count
+ assert title[0:TestNotification.title_maxlen] == chunks[0].get('title')
+
+ #
+ # Next Test: Truncated body
+ #
+
+ class TestNotification(NotifyBase):
+
+ # Test title max length
+ title_maxlen = title_len
+
+ # Enforce a body length of just 10
+ body_maxlen = 10
+
+ def __init__(self, *args, **kwargs):
+ super(TestNotification, self).__init__(**kwargs)
+
+ def notify(self, *args, **kwargs):
+ # Pretend everything is okay
+ return True
+
+ # Load our object
+ obj = TestNotification(overflow=OverflowMode.TRUNCATE)
+ assert obj is not None
+
+ # Verify that we break the title to a max length of our title_max
+ # and that the body remains untouched
+ chunks = obj._apply_overflow(body=body, title=title)
+ assert len(chunks) == 1
+ assert body[0:TestNotification.body_maxlen] == chunks[0].get('body')
+ assert title == chunks[0].get('title')
+
+ #
+ # Next Test: Append title to body + Truncated body
+ #
+
+ class TestNotification(NotifyBase):
+
+ # Enforce no title
+ title_maxlen = 0
+
+ # Enforce a body length of just 100
+ body_maxlen = 100
+
+ def __init__(self, *args, **kwargs):
+ super(TestNotification, self).__init__(**kwargs)
+
+ def notify(self, *args, **kwargs):
+ # Pretend everything is okay
+ return True
+
+ # Load our object
+ obj = TestNotification(overflow=OverflowMode.TRUNCATE)
+ assert obj is not None
+
+ # Verify that we break the title to a max length of our title_max
+ # and that the body remains untouched
+ chunks = obj._apply_overflow(body=body, title=title)
+ assert len(chunks) == 1
+
+ # The below line should be read carefully... We're actually testing to see
+ # that our title is matched against our body. Behind the scenes, the title
+ # was appended to the body. The body was then truncated to the maxlen.
+ # The thing is, since the title is so large, all of the body was lost
+ # and a good chunk of the title was too. The message sent will just be a
+ # small portion of the title
+ assert len(chunks[0].get('body')) == TestNotification.body_maxlen
+ assert title[0:TestNotification.body_maxlen] == chunks[0].get('body')
+
+
+def test_notify_overflow_split():
+ """
+ API: Overflow Split Functionality Testing
+
+ """
+
+ #
+ # A little preparation
+ #
+
+ # Disable Throttling to speed testing
+ plugins.NotifyBase.NotifyBase.request_rate_per_sec = 0
+
+ # Number of characters per line
+ row = 24
+
+ # Some variables we use to control the data we work with
+ body_len = 1024
+ title_len = 1024
+
+ # Create a large body and title with random data
+ body = ''.join(choice(str_alpha + str_num) for _ in range(body_len))
+ body = '\r\n'.join([body[i: i + row] for i in range(0, len(body), row)])
+
+ # the new lines add a large amount to our body; lets force the content
+ # back to being 1024 characters.
+ body = body[0:1024]
+
+ # Create our title using random data
+ title = ''.join(choice(str_alpha + str_num) for _ in range(title_len))
+
+ #
+ # First Test: Truncated Title
+ #
+ class TestNotification(NotifyBase):
+
+ # Test title max length
+ title_maxlen = 10
+
+ def __init__(self, *args, **kwargs):
+ super(TestNotification, self).__init__(**kwargs)
+
+ def notify(self, *args, **kwargs):
+ # Pretend everything is okay
+ return True
+
+ # Load our object
+ obj = TestNotification(overflow=OverflowMode.SPLIT)
+ assert obj is not None
+
+ # Verify that we break the title to a max length of our title_max
+ # and that the body remains untouched
+ chunks = obj._apply_overflow(body=body, title=title)
+ assert len(chunks) == 1
+ assert body == chunks[0].get('body')
+ assert title[0:TestNotification.title_maxlen] == chunks[0].get('title')
+
+ #
+ # Next Test: Line Count Control
+ #
+
+ class TestNotification(NotifyBase):
+
+ # Test title max length
+ title_maxlen = 5
+
+ # Maximum number of lines
+ body_max_line_count = 5
+
+ def __init__(self, *args, **kwargs):
+ super(TestNotification, self).__init__(**kwargs)
+
+ def notify(self, *args, **kwargs):
+ # Pretend everything is okay
+ return True
+
+ # Load our object
+ obj = TestNotification(overflow=OverflowMode.SPLIT)
+ assert obj is not None
+
+ # Verify that we break the title to a max length of our title_max
+ # and that the body remains untouched
+ chunks = obj._apply_overflow(body=body, title=title)
+ assert len(chunks) == 1
+ assert len(chunks[0].get('body').split('\n')) == \
+ TestNotification.body_max_line_count
+ assert title[0:TestNotification.title_maxlen] == chunks[0].get('title')
+
+ #
+ # Next Test: Split body
+ #
+
+ class TestNotification(NotifyBase):
+
+ # Test title max length
+ title_maxlen = title_len
+
+ # Enforce a body length
+ # Wrap in int() so Python v3 doesn't convert the response into a float
+ body_maxlen = int(body_len / 4)
+
+ def __init__(self, *args, **kwargs):
+ super(TestNotification, self).__init__(**kwargs)
+
+ def notify(self, *args, **kwargs):
+ # Pretend everything is okay
+ return True
+
+ # Load our object
+ obj = TestNotification(overflow=OverflowMode.SPLIT)
+ assert obj is not None
+
+ # Verify that we break the title to a max length of our title_max
+ # and that the body remains untouched
+ chunks = obj._apply_overflow(body=body, title=title)
+ offset = 0
+ assert len(chunks) == 4
+ for chunk in chunks:
+ # Our title never changes
+ assert title == chunk.get('title')
+
+ # Our body is only broken up; not lost
+ _body = chunk.get('body')
+ assert body[offset: len(_body) + offset] == _body
+ offset += len(_body)
+
+ #
+ # Next Test: Append title to body + split body
+ #
+
+ class TestNotification(NotifyBase):
+
+ # Enforce no title
+ title_maxlen = 0
+
+ # Enforce a body length based on the title
+ # Wrap in int() so Python v3 doesn't convert the response into a float
+ body_maxlen = int(title_len / 4)
+
+ def __init__(self, *args, **kwargs):
+ super(TestNotification, self).__init__(**kwargs)
+
+ def notify(self, *args, **kwargs):
+ # Pretend everything is okay
+ return True
+
+ # Load our object
+ obj = TestNotification(overflow=OverflowMode.SPLIT)
+ assert obj is not None
+
+ # Verify that we break the title to a max length of our title_max
+ # and that the body remains untouched
+ chunks = obj._apply_overflow(body=body, title=title)
+
+ # Our final product is that our title has been appended to our body to
+ # create one great big body. As a result we'll get quite a few lines back
+ # now.
+ offset = 0
+
+ # Our body will look like this in small chunks at the end of the day
+ bulk = title + '\r\n' + body
+
+ # Due to the new line added to the end
+ assert len(chunks) == (
+ # wrap division in int() so Python 3 doesn't convert it to a float on
+ # us
+ int(len(bulk) / TestNotification.body_maxlen) +
+ (1 if len(bulk) % TestNotification.body_maxlen else 0))
+
+ for chunk in chunks:
+ # Our title is empty every time
+ assert chunk.get('title') == ''
+
+ _body = chunk.get('body')
+ assert bulk[offset: len(_body) + offset] == _body
+ offset += len(_body)
diff --git a/test/test_sns_plugin.py b/test/test_sns_plugin.py
index 27b0f23e..7e88bf1c 100644
--- a/test/test_sns_plugin.py
+++ b/test/test_sns_plugin.py
@@ -303,6 +303,8 @@ def test_aws_topic_handling(mock_post):
API: NotifySNS Plugin() AWS Topic Handling
"""
+ # Disable Throttling to speed testing
+ plugins.NotifySNS.request_rate_per_sec = 0
arn_response = \
"""
@@ -336,9 +338,6 @@ def test_aws_topic_handling(mock_post):
# Assign ourselves a new function
mock_post.side_effect = post
- # Disable Throttling to speed testing
- plugins.NotifyBase.NotifyBase.throttle_attempt = 0
-
# Create our object
a = Apprise()
diff --git a/test/test_utils.py b/test/test_utils.py
index 64e5568f..1281c6e4 100644
--- a/test/test_utils.py
+++ b/test/test_utils.py
@@ -35,6 +35,26 @@ except ImportError:
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():
"utils: parse_url() testing """
@@ -49,6 +69,8 @@ def test_parse_url():
assert(result['query'] is None)
assert(result['url'] == 'http://hostname')
assert(result['qsd'] == {})
+ assert(result['qsd-'] == {})
+ assert(result['qsd+'] == {})
result = utils.parse_url('http://hostname/')
assert(result['schema'] == 'http')
@@ -61,6 +83,8 @@ def test_parse_url():
assert(result['query'] is None)
assert(result['url'] == 'http://hostname/')
assert(result['qsd'] == {})
+ assert(result['qsd-'] == {})
+ assert(result['qsd+'] == {})
result = utils.parse_url('hostname')
assert(result['schema'] == 'http')
@@ -73,6 +97,61 @@ def test_parse_url():
assert(result['query'] is None)
assert(result['url'] == 'http://hostname')
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////')
assert(result['schema'] == 'http')
@@ -85,6 +164,8 @@ def test_parse_url():
assert(result['query'] is None)
assert(result['url'] == 'http://hostname/')
assert(result['qsd'] == {})
+ assert(result['qsd-'] == {})
+ assert(result['qsd+'] == {})
result = utils.parse_url('http://hostname:40////')
assert(result['schema'] == 'http')
@@ -97,6 +178,8 @@ def test_parse_url():
assert(result['query'] is None)
assert(result['url'] == 'http://hostname:40/')
assert(result['qsd'] == {})
+ assert(result['qsd-'] == {})
+ assert(result['qsd+'] == {})
result = utils.parse_url('HTTP://HoStNaMe:40/test.php')
assert(result['schema'] == 'http')
@@ -109,6 +192,8 @@ def test_parse_url():
assert(result['query'] == 'test.php')
assert(result['url'] == 'http://HoStNaMe:40/test.php')
assert(result['qsd'] == {})
+ assert(result['qsd-'] == {})
+ assert(result['qsd+'] == {})
result = utils.parse_url('HTTPS://user@hostname/test.py')
assert(result['schema'] == 'https')
@@ -121,6 +206,8 @@ def test_parse_url():
assert(result['query'] == 'test.py')
assert(result['url'] == 'https://user@hostname/test.py')
assert(result['qsd'] == {})
+ assert(result['qsd-'] == {})
+ assert(result['qsd+'] == {})
result = utils.parse_url(' HTTPS://///user@@@hostname///test.py ')
assert(result['schema'] == 'https')
@@ -133,6 +220,8 @@ def test_parse_url():
assert(result['query'] == 'test.py')
assert(result['url'] == 'https://user@hostname/test.py')
assert(result['qsd'] == {})
+ assert(result['qsd-'] == {})
+ assert(result['qsd+'] == {})
result = utils.parse_url(
'HTTPS://user:password@otherHost/full///path/name/',
@@ -147,6 +236,8 @@ def test_parse_url():
assert(result['query'] is None)
assert(result['url'] == 'https://user:password@otherHost/full/path/name/')
assert(result['qsd'] == {})
+ assert(result['qsd-'] == {})
+ assert(result['qsd+'] == {})
# Handle garbage
assert(utils.parse_url(None) is None)
@@ -173,6 +264,8 @@ def test_parse_url():
assert(unquote(result['qsd']['from']) == 'test@test.com')
assert('format' in result['qsd'])
assert(unquote(result['qsd']['format']) == 'text')
+ assert(result['qsd-'] == {})
+ assert(result['qsd+'] == {})
# Test Passwords with question marks ?; not supported
result = utils.parse_url(
@@ -194,6 +287,8 @@ def test_parse_url():
assert(result['query'] is None)
assert(result['url'] == 'http://nuxref.com')
assert(result['qsd'] == {})
+ assert(result['qsd-'] == {})
+ assert(result['qsd+'] == {})
# just host and path
result = utils.parse_url(
@@ -209,6 +304,8 @@ def test_parse_url():
assert(result['query'] == 'host')
assert(result['url'] == 'http://invalid/host')
assert(result['qsd'] == {})
+ assert(result['qsd-'] == {})
+ assert(result['qsd+'] == {})
# just all out invalid
assert(utils.parse_url('?') is None)
@@ -227,6 +324,8 @@ def test_parse_url():
assert(result['query'] is None)
assert(result['url'] == 'http://nuxref.com')
assert(result['qsd'] == {})
+ assert(result['qsd-'] == {})
+ assert(result['qsd+'] == {})
def test_parse_bool():
diff --git a/test/test_windows_plugin.py b/test/test_windows_plugin.py
index 1c577ede..bfe52632 100644
--- a/test/test_windows_plugin.py
+++ b/test/test_windows_plugin.py
@@ -29,6 +29,7 @@ import types
# Rebuild our Apprise environment
import apprise
+from apprise.utils import compat_is_basestring
try:
# Python v3.4+
@@ -107,6 +108,9 @@ def test_windows_plugin():
obj = apprise.Apprise.instantiate('windows://', suppress_exceptions=False)
obj.duration = 0
+ # Test URL functionality
+ assert(compat_is_basestring(obj.url()) is True)
+
# Check that it found our mocked environments
assert(obj._enabled is True)
diff --git a/tox.ini b/tox.ini
index 3ca38ef7..3a5af053 100644
--- a/tox.ini
+++ b/tox.ini
@@ -10,36 +10,45 @@ setenv =
deps=
-r{toxinidir}/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]
deps=
dbus-python
-r{toxinidir}/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]
deps=
dbus-python
-r{toxinidir}/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]
deps=
dbus-python
-r{toxinidir}/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]
deps=
dbus-python
-r{toxinidir}/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]
deps=
@@ -47,21 +56,24 @@ deps=
-r{toxinidir}/requirements.txt
-r{toxinidir}/dev-requirements.txt
commands =
- flake8 . --count --select=E901,E999,F821,F822,F823 --show-source --statistics
coverage run --parallel -m pytest {posargs}
+ flake8 . --count --show-source --statistics
[testenv:pypy]
deps=
-r{toxinidir}/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]
deps=
-r{toxinidir}/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]
deps = coverage