mirror of
https://github.com/morpheus65535/bazarr.git
synced 2025-04-24 14:47:16 -04:00
Updated Apprise to 0.8.8
This commit is contained in:
parent
ae731bb78b
commit
e6b8b1ad19
81 changed files with 2021 additions and 2681 deletions
|
@ -33,7 +33,7 @@ from .common import NotifyFormat
|
|||
from .common import MATCH_ALL_TAG
|
||||
from .utils import is_exclusive_match
|
||||
from .utils import parse_list
|
||||
from .utils import split_urls
|
||||
from .utils import parse_urls
|
||||
from .logger import logger
|
||||
|
||||
from .AppriseAsset import AppriseAsset
|
||||
|
@ -46,13 +46,19 @@ from .plugins.NotifyBase import NotifyBase
|
|||
from . import plugins
|
||||
from . import __version__
|
||||
|
||||
# Python v3+ support code made importable so it can remain backwards
|
||||
# compatible with Python v2
|
||||
from . import py3compat
|
||||
ASYNCIO_SUPPORT = not six.PY2
|
||||
|
||||
|
||||
class Apprise(object):
|
||||
"""
|
||||
Our Notification Manager
|
||||
|
||||
"""
|
||||
def __init__(self, servers=None, asset=None):
|
||||
|
||||
def __init__(self, servers=None, asset=None, debug=False):
|
||||
"""
|
||||
Loads a set of server urls while applying the Asset() module to each
|
||||
if specified.
|
||||
|
@ -78,6 +84,9 @@ class Apprise(object):
|
|||
# Initialize our locale object
|
||||
self.locale = AppriseLocale()
|
||||
|
||||
# Set our debug flag
|
||||
self.debug = debug
|
||||
|
||||
@staticmethod
|
||||
def instantiate(url, asset=None, tag=None, suppress_exceptions=True):
|
||||
"""
|
||||
|
@ -111,14 +120,10 @@ class Apprise(object):
|
|||
# Acquire our url tokens
|
||||
results = plugins.url_to_dict(url)
|
||||
if results is None:
|
||||
# Failed to parse the server URL
|
||||
logger.error('Unparseable URL {}.'.format(url))
|
||||
# Failed to parse the server URL; detailed logging handled
|
||||
# inside url_to_dict - nothing to report here.
|
||||
return None
|
||||
|
||||
logger.trace('URL {} unpacked as:{}{}'.format(
|
||||
url, os.linesep, os.linesep.join(
|
||||
['{}="{}"'.format(k, v) for k, v in results.items()])))
|
||||
|
||||
elif isinstance(url, dict):
|
||||
# We already have our result set
|
||||
results = url
|
||||
|
@ -154,11 +159,14 @@ class Apprise(object):
|
|||
plugin = plugins.SCHEMA_MAP[results['schema']](**results)
|
||||
|
||||
# Create log entry of loaded URL
|
||||
logger.debug('Loaded URL: {}'.format(plugin.url()))
|
||||
logger.debug('Loaded {} URL: {}'.format(
|
||||
plugins.SCHEMA_MAP[results['schema']].service_name,
|
||||
plugin.url()))
|
||||
|
||||
except Exception:
|
||||
# the arguments are invalid or can not be used.
|
||||
logger.error('Could not load URL: %s' % url)
|
||||
logger.error('Could not load {} URL: {}'.format(
|
||||
plugins.SCHEMA_MAP[results['schema']].service_name, url))
|
||||
return None
|
||||
|
||||
else:
|
||||
|
@ -189,7 +197,7 @@ class Apprise(object):
|
|||
|
||||
if isinstance(servers, six.string_types):
|
||||
# build our server list
|
||||
servers = split_urls(servers)
|
||||
servers = parse_urls(servers)
|
||||
if len(servers) == 0:
|
||||
return False
|
||||
|
||||
|
@ -226,7 +234,7 @@ class Apprise(object):
|
|||
# returns None if it fails
|
||||
instance = Apprise.instantiate(_server, asset=asset, tag=tag)
|
||||
if not isinstance(instance, NotifyBase):
|
||||
# No logging is requird as instantiate() handles failure
|
||||
# No logging is required as instantiate() handles failure
|
||||
# and/or success reasons for us
|
||||
return_status = False
|
||||
continue
|
||||
|
@ -327,6 +335,10 @@ class Apprise(object):
|
|||
body_format = self.asset.body_format \
|
||||
if body_format is None else body_format
|
||||
|
||||
# for asyncio support; we track a list of our servers to notify
|
||||
# sequentially
|
||||
coroutines = []
|
||||
|
||||
# Iterate over our loaded plugins
|
||||
for server in self.find(tag):
|
||||
if status is None:
|
||||
|
@ -384,6 +396,18 @@ class Apprise(object):
|
|||
# Store entry directly
|
||||
conversion_map[server.notify_format] = body
|
||||
|
||||
if ASYNCIO_SUPPORT and server.asset.async_mode:
|
||||
# Build a list of servers requiring notification
|
||||
# that will be triggered asynchronously afterwards
|
||||
coroutines.append(server.async_notify(
|
||||
body=conversion_map[server.notify_format],
|
||||
title=title,
|
||||
notify_type=notify_type,
|
||||
attach=attach))
|
||||
|
||||
# We gather at this point and notify at the end
|
||||
continue
|
||||
|
||||
try:
|
||||
# Send notification
|
||||
if not server.notify(
|
||||
|
@ -405,6 +429,12 @@ class Apprise(object):
|
|||
logger.exception("Notification Exception")
|
||||
status = False
|
||||
|
||||
if coroutines:
|
||||
# perform our async notification(s)
|
||||
if not py3compat.asyncio.notify(coroutines, debug=self.debug):
|
||||
# Toggle our status only if we had a failure
|
||||
status = False
|
||||
|
||||
return status
|
||||
|
||||
def details(self, lang=None):
|
||||
|
|
|
@ -99,6 +99,12 @@ class AppriseAsset(object):
|
|||
# will be the default.
|
||||
body_format = None
|
||||
|
||||
# Always attempt to send notifications asynchronous (as the same time
|
||||
# if possible)
|
||||
# This is a Python 3 supported option only. If set to False, then
|
||||
# notifications are sent sequentially (one after another)
|
||||
async_mode = True
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
"""
|
||||
Asset Initialization
|
||||
|
|
|
@ -27,6 +27,7 @@ import six
|
|||
|
||||
from . import config
|
||||
from . import ConfigBase
|
||||
from . import CONFIG_FORMATS
|
||||
from . import URLBase
|
||||
from .AppriseAsset import AppriseAsset
|
||||
|
||||
|
@ -46,7 +47,8 @@ class AppriseConfig(object):
|
|||
|
||||
"""
|
||||
|
||||
def __init__(self, paths=None, asset=None, cache=True, **kwargs):
|
||||
def __init__(self, paths=None, asset=None, cache=True, recursion=0,
|
||||
insecure_includes=False, **kwargs):
|
||||
"""
|
||||
Loads all of the paths specified (if any).
|
||||
|
||||
|
@ -69,6 +71,29 @@ class AppriseConfig(object):
|
|||
|
||||
It's also worth nothing that the cache value is only set to elements
|
||||
that are not already of subclass ConfigBase()
|
||||
|
||||
recursion defines how deep we recursively handle entries that use the
|
||||
`import` keyword. This keyword requires us to fetch more configuration
|
||||
from another source and add it to our existing compilation. If the
|
||||
file we remotely retrieve also has an `import` reference, we will only
|
||||
advance through it if recursion is set to 2 deep. If set to zero
|
||||
it is off. There is no limit to how high you set this value. It would
|
||||
be recommended to keep it low if you do intend to use it.
|
||||
|
||||
insecure includes by default are disabled. When set to True, all
|
||||
Apprise Config files marked to be in STRICT mode are treated as being
|
||||
in ALWAYS mode.
|
||||
|
||||
Take a file:// based configuration for example, only a file:// based
|
||||
configuration can import another file:// based one. because it is set
|
||||
to STRICT mode. If an http:// based configuration file attempted to
|
||||
import a file:// one it woul fail. However this import would be
|
||||
possible if insecure_includes is set to True.
|
||||
|
||||
There are cases where a self hosting apprise developer may wish to load
|
||||
configuration from memory (in a string format) that contains import
|
||||
entries (even file:// based ones). In these circumstances if you want
|
||||
these includes to be honored, this value must be set to True.
|
||||
"""
|
||||
|
||||
# Initialize a server list of URLs
|
||||
|
@ -81,13 +106,20 @@ class AppriseConfig(object):
|
|||
# Set our cache flag
|
||||
self.cache = cache
|
||||
|
||||
# Initialize our recursion value
|
||||
self.recursion = recursion
|
||||
|
||||
# Initialize our insecure_includes flag
|
||||
self.insecure_includes = insecure_includes
|
||||
|
||||
if paths is not None:
|
||||
# Store our path(s)
|
||||
self.add(paths)
|
||||
|
||||
return
|
||||
|
||||
def add(self, configs, asset=None, tag=None, cache=True):
|
||||
def add(self, configs, asset=None, tag=None, cache=True, recursion=None,
|
||||
insecure_includes=None):
|
||||
"""
|
||||
Adds one or more config URLs into our list.
|
||||
|
||||
|
@ -107,6 +139,12 @@ class AppriseConfig(object):
|
|||
|
||||
It's also worth nothing that the cache value is only set to elements
|
||||
that are not already of subclass ConfigBase()
|
||||
|
||||
Optionally override the default recursion value.
|
||||
|
||||
Optionally override the insecure_includes flag.
|
||||
if insecure_includes is set to True then all plugins that are
|
||||
set to a STRICT mode will be a treated as ALWAYS.
|
||||
"""
|
||||
|
||||
# Initialize our return status
|
||||
|
@ -115,6 +153,14 @@ class AppriseConfig(object):
|
|||
# Initialize our default cache value
|
||||
cache = cache if cache is not None else self.cache
|
||||
|
||||
# Initialize our default recursion value
|
||||
recursion = recursion if recursion is not None else self.recursion
|
||||
|
||||
# Initialize our default insecure_includes value
|
||||
insecure_includes = \
|
||||
insecure_includes if insecure_includes is not None \
|
||||
else self.insecure_includes
|
||||
|
||||
if asset is None:
|
||||
# prepare default asset
|
||||
asset = self.asset
|
||||
|
@ -154,7 +200,8 @@ class AppriseConfig(object):
|
|||
# Instantiate ourselves an object, this function throws or
|
||||
# returns None if it fails
|
||||
instance = AppriseConfig.instantiate(
|
||||
_config, asset=asset, tag=tag, cache=cache)
|
||||
_config, asset=asset, tag=tag, cache=cache,
|
||||
recursion=recursion, insecure_includes=insecure_includes)
|
||||
if not isinstance(instance, ConfigBase):
|
||||
return_status = False
|
||||
continue
|
||||
|
@ -165,7 +212,8 @@ class AppriseConfig(object):
|
|||
# Return our status
|
||||
return return_status
|
||||
|
||||
def add_config(self, content, asset=None, tag=None, format=None):
|
||||
def add_config(self, content, asset=None, tag=None, format=None,
|
||||
recursion=None, insecure_includes=None):
|
||||
"""
|
||||
Adds one configuration file in it's raw format. Content gets loaded as
|
||||
a memory based object and only exists for the life of this
|
||||
|
@ -174,8 +222,22 @@ class AppriseConfig(object):
|
|||
If you know the format ('yaml' or 'text') you can specify
|
||||
it for slightly less overhead during this call. Otherwise the
|
||||
configuration is auto-detected.
|
||||
|
||||
Optionally override the default recursion value.
|
||||
|
||||
Optionally override the insecure_includes flag.
|
||||
if insecure_includes is set to True then all plugins that are
|
||||
set to a STRICT mode will be a treated as ALWAYS.
|
||||
"""
|
||||
|
||||
# Initialize our default recursion value
|
||||
recursion = recursion if recursion is not None else self.recursion
|
||||
|
||||
# Initialize our default insecure_includes value
|
||||
insecure_includes = \
|
||||
insecure_includes if insecure_includes is not None \
|
||||
else self.insecure_includes
|
||||
|
||||
if asset is None:
|
||||
# prepare default asset
|
||||
asset = self.asset
|
||||
|
@ -190,7 +252,13 @@ class AppriseConfig(object):
|
|||
|
||||
# Create ourselves a ConfigMemory Object to store our configuration
|
||||
instance = config.ConfigMemory(
|
||||
content=content, format=format, asset=asset, tag=tag)
|
||||
content=content, format=format, asset=asset, tag=tag,
|
||||
recursion=recursion, insecure_includes=insecure_includes)
|
||||
|
||||
if instance.config_format not in CONFIG_FORMATS:
|
||||
logger.warning(
|
||||
"The format of the configuration could not be deteced.")
|
||||
return False
|
||||
|
||||
# Add our initialized plugin to our server listings
|
||||
self.configs.append(instance)
|
||||
|
@ -235,6 +303,7 @@ class AppriseConfig(object):
|
|||
|
||||
@staticmethod
|
||||
def instantiate(url, asset=None, tag=None, cache=None,
|
||||
recursion=0, insecure_includes=False,
|
||||
suppress_exceptions=True):
|
||||
"""
|
||||
Returns the instance of a instantiated configuration plugin based on
|
||||
|
@ -279,6 +348,12 @@ class AppriseConfig(object):
|
|||
# Force an over-ride of the cache value to what we have specified
|
||||
results['cache'] = cache
|
||||
|
||||
# Recursion can never be parsed from the URL
|
||||
results['recursion'] = recursion
|
||||
|
||||
# Insecure includes flag can never be parsed from the URL
|
||||
results['insecure_includes'] = insecure_includes
|
||||
|
||||
if suppress_exceptions:
|
||||
try:
|
||||
# Attempt to create an instance of our plugin using the parsed
|
||||
|
|
|
@ -42,6 +42,7 @@ except ImportError:
|
|||
from urllib.parse import quote as _quote
|
||||
from urllib.parse import urlencode as _urlencode
|
||||
|
||||
from .AppriseLocale import gettext_lazy as _
|
||||
from .AppriseAsset import AppriseAsset
|
||||
from .utils import parse_url
|
||||
from .utils import parse_bool
|
||||
|
@ -98,6 +99,16 @@ class URLBase(object):
|
|||
# Throttle
|
||||
request_rate_per_sec = 0
|
||||
|
||||
# The connect timeout is the number of seconds Requests will wait for your
|
||||
# client to establish a connection to a remote machine (corresponding to
|
||||
# the connect()) call on the socket.
|
||||
socket_connect_timeout = 4.0
|
||||
|
||||
# The read timeout is the number of seconds the client will wait for the
|
||||
# server to send a response.
|
||||
socket_read_timeout = 4.0
|
||||
|
||||
# Handle
|
||||
# Maintain a set of tags to associate with this specific notification
|
||||
tags = set()
|
||||
|
||||
|
@ -107,6 +118,78 @@ class URLBase(object):
|
|||
# Logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Define a default set of template arguments used for dynamically building
|
||||
# details about our individual plugins for developers.
|
||||
|
||||
# Define object templates
|
||||
templates = ()
|
||||
|
||||
# Provides a mapping of tokens, certain entries are fixed and automatically
|
||||
# configured if found (such as schema, host, user, pass, and port)
|
||||
template_tokens = {}
|
||||
|
||||
# Here is where we define all of the arguments we accept on the url
|
||||
# such as: schema://whatever/?cto=5.0&rto=15
|
||||
# These act the same way as tokens except they are optional and/or
|
||||
# have default values set if mandatory. This rule must be followed
|
||||
template_args = {
|
||||
'verify': {
|
||||
'name': _('Verify SSL'),
|
||||
# SSL Certificate Authority Verification
|
||||
'type': 'bool',
|
||||
# Provide a default
|
||||
'default': verify_certificate,
|
||||
# look up default using the following parent class value at
|
||||
# runtime.
|
||||
'_lookup_default': 'verify_certificate',
|
||||
},
|
||||
'rto': {
|
||||
'name': _('Socket Read Timeout'),
|
||||
'type': 'float',
|
||||
# Provide a default
|
||||
'default': socket_read_timeout,
|
||||
# look up default using the following parent class value at
|
||||
# runtime. The variable name identified here (in this case
|
||||
# socket_read_timeout) is checked and it's result is placed
|
||||
# over-top of the 'default'. This is done because once a parent
|
||||
# class inherits this one, the overflow_mode already set as a
|
||||
# default 'could' be potentially over-ridden and changed to a
|
||||
# different value.
|
||||
'_lookup_default': 'socket_read_timeout',
|
||||
},
|
||||
'cto': {
|
||||
'name': _('Socket Connect Timeout'),
|
||||
'type': 'float',
|
||||
# Provide a default
|
||||
'default': socket_connect_timeout,
|
||||
# look up default using the following parent class value at
|
||||
# runtime. The variable name identified here (in this case
|
||||
# socket_connect_timeout) is checked and it's result is placed
|
||||
# over-top of the 'default'. This is done because once a parent
|
||||
# class inherits this one, the overflow_mode already set as a
|
||||
# default 'could' be potentially over-ridden and changed to a
|
||||
# different value.
|
||||
'_lookup_default': 'socket_connect_timeout',
|
||||
},
|
||||
}
|
||||
|
||||
# kwargs are dynamically built because a prefix causes us to parse the
|
||||
# content slightly differently. The prefix is required and can be either
|
||||
# a (+ or -). Below would handle the +key=value:
|
||||
# {
|
||||
# 'headers': {
|
||||
# 'name': _('HTTP Header'),
|
||||
# 'prefix': '+',
|
||||
# 'type': 'string',
|
||||
# },
|
||||
# },
|
||||
#
|
||||
# In a kwarg situation, the 'key' is always presumed to be treated as
|
||||
# a string. When the 'type' is defined, it is being defined to respect
|
||||
# the 'value'.
|
||||
|
||||
template_kwargs = {}
|
||||
|
||||
def __init__(self, asset=None, **kwargs):
|
||||
"""
|
||||
Initialize some general logging and common server arguments that will
|
||||
|
@ -131,6 +214,9 @@ class URLBase(object):
|
|||
self.port = int(self.port)
|
||||
|
||||
except (TypeError, ValueError):
|
||||
self.logger.warning(
|
||||
'Invalid port number specified {}'
|
||||
.format(self.port))
|
||||
self.port = None
|
||||
|
||||
self.user = kwargs.get('user')
|
||||
|
@ -143,6 +229,26 @@ class URLBase(object):
|
|||
# Always unquote the password if it exists
|
||||
self.password = URLBase.unquote(self.password)
|
||||
|
||||
# Store our Timeout Variables
|
||||
if 'socket_read_timeout' in kwargs:
|
||||
try:
|
||||
self.socket_read_timeout = \
|
||||
float(kwargs.get('socket_read_timeout'))
|
||||
except (TypeError, ValueError):
|
||||
self.logger.warning(
|
||||
'Invalid socket read timeout (rto) was specified {}'
|
||||
.format(kwargs.get('socket_read_timeout')))
|
||||
|
||||
if 'socket_connect_timeout' in kwargs:
|
||||
try:
|
||||
self.socket_connect_timeout = \
|
||||
float(kwargs.get('socket_connect_timeout'))
|
||||
|
||||
except (TypeError, ValueError):
|
||||
self.logger.warning(
|
||||
'Invalid socket connect timeout (cto) was specified {}'
|
||||
.format(kwargs.get('socket_connect_timeout')))
|
||||
|
||||
if 'tag' in kwargs:
|
||||
# We want to associate some tags with our notification service.
|
||||
# the code below gets the 'tag' argument if defined, otherwise
|
||||
|
@ -456,15 +562,41 @@ class URLBase(object):
|
|||
|
||||
@property
|
||||
def app_id(self):
|
||||
return self.asset.app_id
|
||||
return self.asset.app_id if self.asset.app_id else ''
|
||||
|
||||
@property
|
||||
def app_desc(self):
|
||||
return self.asset.app_desc
|
||||
return self.asset.app_desc if self.asset.app_desc else ''
|
||||
|
||||
@property
|
||||
def app_url(self):
|
||||
return self.asset.app_url
|
||||
return self.asset.app_url if self.asset.app_url else ''
|
||||
|
||||
@property
|
||||
def request_timeout(self):
|
||||
"""This is primarily used to fullfill the `timeout` keyword argument
|
||||
that is used by requests.get() and requests.put() calls.
|
||||
"""
|
||||
return (self.socket_connect_timeout, self.socket_read_timeout)
|
||||
|
||||
def url_parameters(self, *args, **kwargs):
|
||||
"""
|
||||
Provides a default set of args to work with. This can greatly
|
||||
simplify URL construction in the acommpanied url() function.
|
||||
|
||||
The following property returns a dictionary (of strings) containing
|
||||
all of the parameters that can be set on a URL and managed through
|
||||
this class.
|
||||
"""
|
||||
|
||||
return {
|
||||
# The socket read timeout
|
||||
'rto': str(self.socket_read_timeout),
|
||||
# The request/socket connect timeout
|
||||
'cto': str(self.socket_connect_timeout),
|
||||
# Certificate verification
|
||||
'verify': 'yes' if self.verify_certificate else 'no',
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def parse_url(url, verify_host=True):
|
||||
|
@ -511,6 +643,14 @@ class URLBase(object):
|
|||
if 'user' in results['qsd']:
|
||||
results['user'] = results['qsd']['user']
|
||||
|
||||
# Store our socket read timeout if specified
|
||||
if 'rto' in results['qsd']:
|
||||
results['socket_read_timeout'] = results['qsd']['rto']
|
||||
|
||||
# Store our socket connect timeout if specified
|
||||
if 'cto' in results['qsd']:
|
||||
results['socket_connect_timeout'] = results['qsd']['cto']
|
||||
|
||||
return results
|
||||
|
||||
@staticmethod
|
||||
|
@ -534,3 +674,24 @@ class URLBase(object):
|
|||
response = ''
|
||||
|
||||
return response
|
||||
|
||||
def schemas(self):
|
||||
"""A simple function that returns a set of all schemas associated
|
||||
with this object based on the object.protocol and
|
||||
object.secure_protocol
|
||||
"""
|
||||
|
||||
schemas = set([])
|
||||
|
||||
for key in ('protocol', 'secure_protocol'):
|
||||
schema = getattr(self, key, None)
|
||||
if isinstance(schema, six.string_types):
|
||||
schemas.add(schema)
|
||||
|
||||
elif isinstance(schema, (set, list, tuple)):
|
||||
# Support iterables list types
|
||||
for s in schema:
|
||||
if isinstance(s, six.string_types):
|
||||
schemas.add(s)
|
||||
|
||||
return schemas
|
||||
|
|
|
@ -24,7 +24,7 @@
|
|||
# THE SOFTWARE.
|
||||
|
||||
__title__ = 'apprise'
|
||||
__version__ = '0.8.5'
|
||||
__version__ = '0.8.8'
|
||||
__author__ = 'Chris Caron'
|
||||
__license__ = 'MIT'
|
||||
__copywrite__ = 'Copyright (C) 2020 Chris Caron <lead2gold@gmail.com>'
|
||||
|
@ -41,6 +41,8 @@ from .common import OverflowMode
|
|||
from .common import OVERFLOW_MODES
|
||||
from .common import ConfigFormat
|
||||
from .common import CONFIG_FORMATS
|
||||
from .common import ConfigIncludeMode
|
||||
from .common import CONFIG_INCLUDE_MODES
|
||||
|
||||
from .URLBase import URLBase
|
||||
from .URLBase import PrivacyMode
|
||||
|
@ -66,5 +68,7 @@ __all__ = [
|
|||
# Reference
|
||||
'NotifyType', 'NotifyImageSize', 'NotifyFormat', 'OverflowMode',
|
||||
'NOTIFY_TYPES', 'NOTIFY_IMAGE_SIZES', 'NOTIFY_FORMATS', 'OVERFLOW_MODES',
|
||||
'ConfigFormat', 'CONFIG_FORMATS', 'PrivacyMode',
|
||||
'ConfigFormat', 'CONFIG_FORMATS',
|
||||
'ConfigIncludeMode', 'CONFIG_INCLUDE_MODES',
|
||||
'PrivacyMode',
|
||||
]
|
||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 157 KiB |
|
@ -57,20 +57,20 @@ class AttachFile(AttachBase):
|
|||
Returns the URL built dynamically based on specified arguments.
|
||||
"""
|
||||
|
||||
# Define any arguments set
|
||||
args = {}
|
||||
# Define any URL parameters
|
||||
params = {}
|
||||
|
||||
if self._mimetype:
|
||||
# A mime-type was enforced
|
||||
args['mime'] = self._mimetype
|
||||
params['mime'] = self._mimetype
|
||||
|
||||
if self._name:
|
||||
# A name was enforced
|
||||
args['name'] = self._name
|
||||
params['name'] = self._name
|
||||
|
||||
return 'file://{path}{args}'.format(
|
||||
return 'file://{path}{params}'.format(
|
||||
path=self.quote(self.dirty_path),
|
||||
args='?{}'.format(self.urlencode(args)) if args else '',
|
||||
params='?{}'.format(self.urlencode(params)) if params else '',
|
||||
)
|
||||
|
||||
def download(self, **kwargs):
|
||||
|
|
|
@ -47,10 +47,6 @@ class AttachHTTP(AttachBase):
|
|||
# The default secure protocol
|
||||
secure_protocol = 'https'
|
||||
|
||||
# The maximum number of seconds to wait for a connection to be established
|
||||
# before out-right just giving up
|
||||
connection_timeout_sec = 5.0
|
||||
|
||||
# The number of bytes in memory to read from the remote source at a time
|
||||
chunk_size = 8192
|
||||
|
||||
|
@ -129,7 +125,7 @@ class AttachHTTP(AttachBase):
|
|||
auth=auth,
|
||||
params=self.qsd,
|
||||
verify=self.verify_certificate,
|
||||
timeout=self.connection_timeout_sec,
|
||||
timeout=self.request_timeout,
|
||||
stream=True) as r:
|
||||
|
||||
# Handle Errors
|
||||
|
@ -215,7 +211,7 @@ class AttachHTTP(AttachBase):
|
|||
|
||||
except requests.RequestException as e:
|
||||
self.logger.error(
|
||||
'A Connection error occured retrieving HTTP '
|
||||
'A Connection error occurred retrieving HTTP '
|
||||
'configuration from %s.' % self.host)
|
||||
self.logger.debug('Socket Exception: %s' % str(e))
|
||||
|
||||
|
@ -258,10 +254,8 @@ class AttachHTTP(AttachBase):
|
|||
Returns the URL built dynamically based on specified arguments.
|
||||
"""
|
||||
|
||||
# Define any arguments set
|
||||
args = {
|
||||
'verify': 'yes' if self.verify_certificate else 'no',
|
||||
}
|
||||
# Our URL parameters
|
||||
params = self.url_parameters(privacy=privacy, *args, **kwargs)
|
||||
|
||||
# Prepare our cache value
|
||||
if self.cache is not None:
|
||||
|
@ -271,21 +265,21 @@ class AttachHTTP(AttachBase):
|
|||
cache = int(self.cache)
|
||||
|
||||
# Set our cache value
|
||||
args['cache'] = cache
|
||||
params['cache'] = cache
|
||||
|
||||
if self._mimetype:
|
||||
# A format was enforced
|
||||
args['mime'] = self._mimetype
|
||||
params['mime'] = self._mimetype
|
||||
|
||||
if self._name:
|
||||
# A name was enforced
|
||||
args['name'] = self._name
|
||||
params['name'] = self._name
|
||||
|
||||
# Append our headers into our args
|
||||
args.update({'+{}'.format(k): v for k, v in self.headers.items()})
|
||||
# Append our headers into our parameters
|
||||
params.update({'+{}'.format(k): v for k, v in self.headers.items()})
|
||||
|
||||
# Apply any remaining entries to our URL
|
||||
args.update(self.qsd)
|
||||
params.update(self.qsd)
|
||||
|
||||
# Determine Authentication
|
||||
auth = ''
|
||||
|
@ -302,21 +296,21 @@ class AttachHTTP(AttachBase):
|
|||
|
||||
default_port = 443 if self.secure else 80
|
||||
|
||||
return '{schema}://{auth}{hostname}{port}{fullpath}?{args}'.format(
|
||||
return '{schema}://{auth}{hostname}{port}{fullpath}?{params}'.format(
|
||||
schema=self.secure_protocol if self.secure else self.protocol,
|
||||
auth=auth,
|
||||
hostname=self.quote(self.host, safe=''),
|
||||
port='' if self.port is None or self.port == default_port
|
||||
else ':{}'.format(self.port),
|
||||
fullpath=self.quote(self.fullpath, safe='/'),
|
||||
args=self.urlencode(args),
|
||||
params=self.urlencode(params),
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def parse_url(url):
|
||||
"""
|
||||
Parses the URL and returns enough arguments that can allow
|
||||
us to substantiate this object.
|
||||
us to re-instantiate this object.
|
||||
|
||||
"""
|
||||
results = AttachBase.parse_url(url)
|
||||
|
|
|
@ -32,11 +32,13 @@ from os.path import expanduser
|
|||
from os.path import expandvars
|
||||
|
||||
from . import NotifyType
|
||||
from . import NotifyFormat
|
||||
from . import Apprise
|
||||
from . import AppriseAsset
|
||||
from . import AppriseConfig
|
||||
from .utils import parse_list
|
||||
from .common import NOTIFY_TYPES
|
||||
from .common import NOTIFY_FORMATS
|
||||
from .logger import logger
|
||||
|
||||
from . import __title__
|
||||
|
@ -44,6 +46,10 @@ from . import __version__
|
|||
from . import __license__
|
||||
from . import __copywrite__
|
||||
|
||||
# By default we allow looking 1 level down recursivly in Apprise configuration
|
||||
# files.
|
||||
DEFAULT_RECURSION_DEPTH = 1
|
||||
|
||||
# Defines our click context settings adding -h to the additional options that
|
||||
# can be specified to get the help menu to come up
|
||||
CONTEXT_SETTINGS = dict(help_option_names=['-h', '--help'])
|
||||
|
@ -101,12 +107,19 @@ def print_version_msg():
|
|||
help='Specify one or more configuration locations.')
|
||||
@click.option('--attach', '-a', default=None, type=str, multiple=True,
|
||||
metavar='ATTACHMENT_URL',
|
||||
help='Specify one or more configuration locations.')
|
||||
help='Specify one or more attachment.')
|
||||
@click.option('--notification-type', '-n', default=NotifyType.INFO, type=str,
|
||||
metavar='TYPE',
|
||||
help='Specify the message type (default=info). Possible values'
|
||||
' are "{}", and "{}".'.format(
|
||||
'", "'.join(NOTIFY_TYPES[:-1]), NOTIFY_TYPES[-1]))
|
||||
help='Specify the message type (default={}). '
|
||||
'Possible values are "{}", and "{}".'.format(
|
||||
NotifyType.INFO, '", "'.join(NOTIFY_TYPES[:-1]),
|
||||
NOTIFY_TYPES[-1]))
|
||||
@click.option('--input-format', '-i', default=NotifyFormat.TEXT, type=str,
|
||||
metavar='FORMAT',
|
||||
help='Specify the message input format (default={}). '
|
||||
'Possible values are "{}", and "{}".'.format(
|
||||
NotifyFormat.TEXT, '", "'.join(NOTIFY_FORMATS[:-1]),
|
||||
NOTIFY_FORMATS[-1]))
|
||||
@click.option('--theme', '-T', default='default', type=str, metavar='THEME',
|
||||
help='Specify the default theme.')
|
||||
@click.option('--tag', '-g', default=None, type=str, multiple=True,
|
||||
|
@ -114,19 +127,28 @@ def print_version_msg():
|
|||
'which services to notify. Use multiple --tag (-g) entries to '
|
||||
'"OR" the tags together and comma separated to "AND" them. '
|
||||
'If no tags are specified then all services are notified.')
|
||||
@click.option('--disable-async', '-Da', is_flag=True,
|
||||
help='Send all notifications sequentially')
|
||||
@click.option('--dry-run', '-d', is_flag=True,
|
||||
help='Perform a trial run but only prints the notification '
|
||||
'services to-be triggered to stdout. Notifications are never '
|
||||
'sent using this mode.')
|
||||
@click.option('--recursion-depth', '-R', default=DEFAULT_RECURSION_DEPTH,
|
||||
type=int,
|
||||
help='The number of recursive import entries that can be '
|
||||
'loaded from within Apprise configuration. By default '
|
||||
'this is set to {}.'.format(DEFAULT_RECURSION_DEPTH))
|
||||
@click.option('--verbose', '-v', count=True,
|
||||
help='Makes the operation more talkative. Use multiple v to '
|
||||
'increase the verbosity. I.e.: -vvvv')
|
||||
@click.option('--debug', '-D', is_flag=True, help='Debug mode')
|
||||
@click.option('--version', '-V', is_flag=True,
|
||||
help='Display the apprise version and exit.')
|
||||
@click.argument('urls', nargs=-1,
|
||||
metavar='SERVER_URL [SERVER_URL2 [SERVER_URL3]]',)
|
||||
def main(body, title, config, attach, urls, notification_type, theme, tag,
|
||||
dry_run, verbose, version):
|
||||
input_format, dry_run, recursion_depth, verbose, disable_async,
|
||||
debug, version):
|
||||
"""
|
||||
Send a notification to all of the specified servers identified by their
|
||||
URLs the content provided within the title, body and notification-type.
|
||||
|
@ -138,6 +160,11 @@ def main(body, title, config, attach, urls, notification_type, theme, tag,
|
|||
# want to return a specific error code, you must call sys.exit()
|
||||
# as you will see below.
|
||||
|
||||
debug = True if debug else False
|
||||
if debug:
|
||||
# Verbosity must be a minimum of 3
|
||||
verbose = 3 if verbose < 3 else verbose
|
||||
|
||||
# Logging
|
||||
ch = logging.StreamHandler(sys.stdout)
|
||||
if verbose > 3:
|
||||
|
@ -166,21 +193,55 @@ def main(body, title, config, attach, urls, notification_type, theme, tag,
|
|||
ch.setFormatter(formatter)
|
||||
logger.addHandler(ch)
|
||||
|
||||
# Update our asyncio logger
|
||||
asyncio_logger = logging.getLogger('asyncio')
|
||||
for handler in logger.handlers:
|
||||
asyncio_logger.addHandler(handler)
|
||||
asyncio_logger.setLevel(logger.level)
|
||||
|
||||
if version:
|
||||
print_version_msg()
|
||||
sys.exit(0)
|
||||
|
||||
# Prepare our asset
|
||||
asset = AppriseAsset(theme=theme)
|
||||
# Simple Error Checking
|
||||
notification_type = notification_type.strip().lower()
|
||||
if notification_type not in NOTIFY_TYPES:
|
||||
logger.error(
|
||||
'The --notification-type (-n) value of {} is not supported.'
|
||||
.format(notification_type))
|
||||
# 2 is the same exit code returned by Click if there is a parameter
|
||||
# issue. For consistency, we also return a 2
|
||||
sys.exit(2)
|
||||
|
||||
# Create our object
|
||||
a = Apprise(asset=asset)
|
||||
input_format = input_format.strip().lower()
|
||||
if input_format not in NOTIFY_FORMATS:
|
||||
logger.error(
|
||||
'The --input-format (-i) value of {} is not supported.'
|
||||
.format(input_format))
|
||||
# 2 is the same exit code returned by Click if there is a parameter
|
||||
# issue. For consistency, we also return a 2
|
||||
sys.exit(2)
|
||||
|
||||
# Prepare our asset
|
||||
asset = AppriseAsset(
|
||||
body_format=input_format,
|
||||
theme=theme,
|
||||
# Async mode is only used for Python v3+ and allows a user to send
|
||||
# all of their notifications asyncronously. This was made an option
|
||||
# incase there are problems in the future where it's better that
|
||||
# everything run sequentially/syncronously instead.
|
||||
async_mode=disable_async is not True,
|
||||
)
|
||||
|
||||
# Create our Apprise object
|
||||
a = Apprise(asset=asset, debug=debug)
|
||||
|
||||
# Load our configuration if no URLs or specified configuration was
|
||||
# identified on the command line
|
||||
a.add(AppriseConfig(
|
||||
paths=[f for f in DEFAULT_SEARCH_PATHS if isfile(expanduser(f))]
|
||||
if not (config or urls) else config), asset=asset)
|
||||
if not (config or urls) else config,
|
||||
asset=asset, recursion=recursion_depth))
|
||||
|
||||
# Load our inventory up
|
||||
for url in urls:
|
||||
|
@ -234,7 +295,10 @@ def main(body, title, config, attach, urls, notification_type, theme, tag,
|
|||
# There were no notifications set. This is a result of just having
|
||||
# empty configuration files and/or being to restrictive when filtering
|
||||
# by specific tag(s)
|
||||
sys.exit(2)
|
||||
|
||||
# Exit code 3 is used since Click uses exit code 2 if there is an
|
||||
# error with the parameters specified
|
||||
sys.exit(3)
|
||||
|
||||
elif result is False:
|
||||
# At least 1 notification service failed to send
|
||||
|
|
|
@ -31,15 +31,15 @@ class NotifyType(object):
|
|||
"""
|
||||
INFO = 'info'
|
||||
SUCCESS = 'success'
|
||||
FAILURE = 'failure'
|
||||
WARNING = 'warning'
|
||||
FAILURE = 'failure'
|
||||
|
||||
|
||||
NOTIFY_TYPES = (
|
||||
NotifyType.INFO,
|
||||
NotifyType.SUCCESS,
|
||||
NotifyType.FAILURE,
|
||||
NotifyType.WARNING,
|
||||
NotifyType.FAILURE,
|
||||
)
|
||||
|
||||
|
||||
|
@ -129,6 +129,31 @@ CONFIG_FORMATS = (
|
|||
ConfigFormat.YAML,
|
||||
)
|
||||
|
||||
|
||||
class ConfigIncludeMode(object):
|
||||
"""
|
||||
The different Cofiguration inclusion modes. All Configuration
|
||||
plugins will have one of these associated with it.
|
||||
"""
|
||||
# - Configuration inclusion of same type only; hence a file:// can include
|
||||
# a file://
|
||||
# - Cross file inclusion is not allowed unless insecure_includes (a flag)
|
||||
# is set to True. In these cases STRICT acts as type ALWAYS
|
||||
STRICT = 'strict'
|
||||
|
||||
# This configuration type can never be included
|
||||
NEVER = 'never'
|
||||
|
||||
# File configuration can always be included
|
||||
ALWAYS = 'always'
|
||||
|
||||
|
||||
CONFIG_INCLUDE_MODES = (
|
||||
ConfigIncludeMode.STRICT,
|
||||
ConfigIncludeMode.NEVER,
|
||||
ConfigIncludeMode.ALWAYS,
|
||||
)
|
||||
|
||||
# This is a reserved tag that is automatically assigned to every
|
||||
# Notification Plugin
|
||||
MATCH_ALL_TAG = 'all'
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2019 Chris Caron <lead2gold@gmail.com>
|
||||
# Copyright (C) 2020 Chris Caron <lead2gold@gmail.com>
|
||||
# All rights reserved.
|
||||
#
|
||||
# This code is licensed under the MIT License.
|
||||
|
@ -34,9 +34,12 @@ from ..AppriseAsset import AppriseAsset
|
|||
from ..URLBase import URLBase
|
||||
from ..common import ConfigFormat
|
||||
from ..common import CONFIG_FORMATS
|
||||
from ..common import ConfigIncludeMode
|
||||
from ..utils import GET_SCHEMA_RE
|
||||
from ..utils import parse_list
|
||||
from ..utils import parse_bool
|
||||
from ..utils import parse_urls
|
||||
from . import SCHEMA_MAP
|
||||
|
||||
|
||||
class ConfigBase(URLBase):
|
||||
|
@ -60,7 +63,15 @@ class ConfigBase(URLBase):
|
|||
# anything else. 128KB (131072B)
|
||||
max_buffer_size = 131072
|
||||
|
||||
def __init__(self, cache=True, **kwargs):
|
||||
# By default all configuration is not includable using the 'include'
|
||||
# line found in configuration files.
|
||||
allow_cross_includes = ConfigIncludeMode.NEVER
|
||||
|
||||
# the config path manages the handling of relative include
|
||||
config_path = os.getcwd()
|
||||
|
||||
def __init__(self, cache=True, recursion=0, insecure_includes=False,
|
||||
**kwargs):
|
||||
"""
|
||||
Initialize some general logging and common server arguments that will
|
||||
keep things consistent when working with the configurations that
|
||||
|
@ -76,6 +87,29 @@ class ConfigBase(URLBase):
|
|||
You can alternatively set the cache value to an int identifying the
|
||||
number of seconds the previously retrieved can exist for before it
|
||||
should be considered expired.
|
||||
|
||||
recursion defines how deep we recursively handle entries that use the
|
||||
`include` keyword. This keyword requires us to fetch more configuration
|
||||
from another source and add it to our existing compilation. If the
|
||||
file we remotely retrieve also has an `include` reference, we will only
|
||||
advance through it if recursion is set to 2 deep. If set to zero
|
||||
it is off. There is no limit to how high you set this value. It would
|
||||
be recommended to keep it low if you do intend to use it.
|
||||
|
||||
insecure_include by default are disabled. When set to True, all
|
||||
Apprise Config files marked to be in STRICT mode are treated as being
|
||||
in ALWAYS mode.
|
||||
|
||||
Take a file:// based configuration for example, only a file:// based
|
||||
configuration can include another file:// based one. because it is set
|
||||
to STRICT mode. If an http:// based configuration file attempted to
|
||||
include a file:// one it woul fail. However this include would be
|
||||
possible if insecure_includes is set to True.
|
||||
|
||||
There are cases where a self hosting apprise developer may wish to load
|
||||
configuration from memory (in a string format) that contains 'include'
|
||||
entries (even file:// based ones). In these circumstances if you want
|
||||
these 'include' entries to be honored, this value must be set to True.
|
||||
"""
|
||||
|
||||
super(ConfigBase, self).__init__(**kwargs)
|
||||
|
@ -88,6 +122,12 @@ class ConfigBase(URLBase):
|
|||
# Tracks previously loaded content for speed
|
||||
self._cached_servers = None
|
||||
|
||||
# Initialize our recursion value
|
||||
self.recursion = recursion
|
||||
|
||||
# Initialize our insecure_includes flag
|
||||
self.insecure_includes = insecure_includes
|
||||
|
||||
if 'encoding' in kwargs:
|
||||
# Store the encoding
|
||||
self.encoding = kwargs.get('encoding')
|
||||
|
@ -154,15 +194,110 @@ class ConfigBase(URLBase):
|
|||
# Dynamically load our parse_ function based on our config format
|
||||
fn = getattr(ConfigBase, 'config_parse_{}'.format(config_format))
|
||||
|
||||
# Execute our config parse function which always returns a list
|
||||
self._cached_servers.extend(fn(content=content, asset=asset))
|
||||
# Initialize our asset object
|
||||
asset = asset if isinstance(asset, AppriseAsset) else self.asset
|
||||
|
||||
if len(self._cached_servers):
|
||||
# Execute our config parse function which always returns a tuple
|
||||
# of our servers and our configuration
|
||||
servers, configs = fn(content=content, asset=asset)
|
||||
self._cached_servers.extend(servers)
|
||||
|
||||
# Configuration files were detected; recursively populate them
|
||||
# If we have been configured to do so
|
||||
for url in configs:
|
||||
if self.recursion > 0:
|
||||
|
||||
# Attempt to acquire the schema at the very least to allow
|
||||
# our configuration based urls.
|
||||
schema = GET_SCHEMA_RE.match(url)
|
||||
if schema is None:
|
||||
# Plan B is to assume we're dealing with a file
|
||||
schema = 'file'
|
||||
if not os.path.isabs(url):
|
||||
# We're dealing with a relative path; prepend
|
||||
# our current config path
|
||||
url = os.path.join(self.config_path, url)
|
||||
|
||||
url = '{}://{}'.format(schema, URLBase.quote(url))
|
||||
else:
|
||||
# Ensure our schema is always in lower case
|
||||
schema = schema.group('schema').lower()
|
||||
|
||||
# Some basic validation
|
||||
if schema not in SCHEMA_MAP:
|
||||
ConfigBase.logger.warning(
|
||||
'Unsupported include schema {}.'.format(schema))
|
||||
continue
|
||||
|
||||
# Parse our url details of the server object as dictionary
|
||||
# containing all of the information parsed from our URL
|
||||
results = SCHEMA_MAP[schema].parse_url(url)
|
||||
if not results:
|
||||
# Failed to parse the server URL
|
||||
self.logger.warning(
|
||||
'Unparseable include URL {}'.format(url))
|
||||
continue
|
||||
|
||||
# Handle cross inclusion based on allow_cross_includes rules
|
||||
if (SCHEMA_MAP[schema].allow_cross_includes ==
|
||||
ConfigIncludeMode.STRICT
|
||||
and schema not in self.schemas()
|
||||
and not self.insecure_includes) or \
|
||||
SCHEMA_MAP[schema].allow_cross_includes == \
|
||||
ConfigIncludeMode.NEVER:
|
||||
|
||||
# Prevent the loading if insecure base protocols
|
||||
ConfigBase.logger.warning(
|
||||
'Including {}:// based configuration is prohibited. '
|
||||
'Ignoring URL {}'.format(schema, url))
|
||||
continue
|
||||
|
||||
# Prepare our Asset Object
|
||||
results['asset'] = asset
|
||||
|
||||
# No cache is required because we're just lumping this in
|
||||
# and associating it with the cache value we've already
|
||||
# declared (prior to our recursion)
|
||||
results['cache'] = False
|
||||
|
||||
# Recursion can never be parsed from the URL; we decrement
|
||||
# it one level
|
||||
results['recursion'] = self.recursion - 1
|
||||
|
||||
# Insecure Includes flag can never be parsed from the URL
|
||||
results['insecure_includes'] = self.insecure_includes
|
||||
|
||||
try:
|
||||
# Attempt to create an instance of our plugin using the
|
||||
# parsed URL information
|
||||
cfg_plugin = SCHEMA_MAP[results['schema']](**results)
|
||||
|
||||
except Exception as e:
|
||||
# the arguments are invalid or can not be used.
|
||||
self.logger.warning(
|
||||
'Could not load include URL: {}'.format(url))
|
||||
self.logger.debug('Loading Exception: {}'.format(str(e)))
|
||||
continue
|
||||
|
||||
# if we reach here, we can now add this servers found
|
||||
# in this configuration file to our list
|
||||
self._cached_servers.extend(
|
||||
cfg_plugin.servers(asset=asset))
|
||||
|
||||
# We no longer need our configuration object
|
||||
del cfg_plugin
|
||||
|
||||
else:
|
||||
self.logger.debug(
|
||||
'Recursion limit reached; ignoring Include URL: %s' % url)
|
||||
|
||||
if self._cached_servers:
|
||||
self.logger.info('Loaded {} entries from {}'.format(
|
||||
len(self._cached_servers), self.url()))
|
||||
else:
|
||||
self.logger.warning('Failed to load configuration from {}'.format(
|
||||
self.url()))
|
||||
self.logger.warning(
|
||||
'Failed to load Apprise configuration from {}'.format(
|
||||
self.url()))
|
||||
|
||||
# Set the time our content was cached at
|
||||
self._cached_time = time.time()
|
||||
|
@ -282,7 +417,8 @@ class ConfigBase(URLBase):
|
|||
|
||||
except TypeError:
|
||||
# content was not expected string type
|
||||
ConfigBase.logger.error('Invalid apprise config specified')
|
||||
ConfigBase.logger.error(
|
||||
'Invalid Apprise configuration specified.')
|
||||
return None
|
||||
|
||||
# By default set our return value to None since we don't know
|
||||
|
@ -297,7 +433,7 @@ class ConfigBase(URLBase):
|
|||
if not result:
|
||||
# Invalid syntax
|
||||
ConfigBase.logger.error(
|
||||
'Undetectable apprise configuration found '
|
||||
'Undetectable Apprise configuration found '
|
||||
'based on line {}.'.format(line))
|
||||
# Take an early exit
|
||||
return None
|
||||
|
@ -338,14 +474,14 @@ class ConfigBase(URLBase):
|
|||
if not config_format:
|
||||
# We couldn't detect configuration
|
||||
ConfigBase.logger.error('Could not detect configuration')
|
||||
return list()
|
||||
return (list(), list())
|
||||
|
||||
if config_format not in CONFIG_FORMATS:
|
||||
# Invalid configuration type specified
|
||||
ConfigBase.logger.error(
|
||||
'An invalid configuration format ({}) was specified'.format(
|
||||
config_format))
|
||||
return list()
|
||||
return (list(), list())
|
||||
|
||||
# Dynamically load our parse_ function based on our config format
|
||||
fn = getattr(ConfigBase, 'config_parse_{}'.format(config_format))
|
||||
|
@ -357,9 +493,14 @@ class ConfigBase(URLBase):
|
|||
def config_parse_text(content, asset=None):
|
||||
"""
|
||||
Parse the specified content as though it were a simple text file only
|
||||
containing a list of URLs. Return a list of loaded notification plugins
|
||||
containing a list of URLs.
|
||||
|
||||
Optionally associate an asset with the notification.
|
||||
Return a tuple that looks like (servers, configs) where:
|
||||
- servers contains a list of loaded notification plugins
|
||||
- configs contains a list of additional configuration files
|
||||
referenced.
|
||||
|
||||
You may also optionally associate an asset with the notification.
|
||||
|
||||
The file syntax is:
|
||||
|
||||
|
@ -373,14 +514,25 @@ class ConfigBase(URLBase):
|
|||
# Or you can use this format (no tags associated)
|
||||
<URL>
|
||||
|
||||
# you can also use the keyword 'include' and identify a
|
||||
# configuration location (like this file) which will be included
|
||||
# as additional configuration entries when loaded.
|
||||
include <ConfigURL>
|
||||
|
||||
"""
|
||||
response = list()
|
||||
# A list of loaded Notification Services
|
||||
servers = list()
|
||||
|
||||
# A list of additional configuration files referenced using
|
||||
# the include keyword
|
||||
configs = list()
|
||||
|
||||
# Define what a valid line should look like
|
||||
valid_line_re = re.compile(
|
||||
r'^\s*(?P<line>([;#]+(?P<comment>.*))|'
|
||||
r'(\s*(?P<tags>[^=]+)=|=)?\s*'
|
||||
r'(?P<url>[a-z0-9]{2,9}://.*))?$', re.I)
|
||||
r'(?P<url>[a-z0-9]{2,9}://.*)|'
|
||||
r'include\s+(?P<config>.+))?\s*$', re.I)
|
||||
|
||||
try:
|
||||
# split our content up to read line by line
|
||||
|
@ -388,28 +540,35 @@ class ConfigBase(URLBase):
|
|||
|
||||
except TypeError:
|
||||
# content was not expected string type
|
||||
ConfigBase.logger.error('Invalid apprise text data specified')
|
||||
return list()
|
||||
ConfigBase.logger.error(
|
||||
'Invalid Apprise TEXT based configuration specified.')
|
||||
return (list(), list())
|
||||
|
||||
for line, entry in enumerate(content, start=1):
|
||||
result = valid_line_re.match(entry)
|
||||
if not result:
|
||||
# Invalid syntax
|
||||
ConfigBase.logger.error(
|
||||
'Invalid apprise text format found '
|
||||
'Invalid Apprise TEXT configuration format found '
|
||||
'{} on line {}.'.format(entry, line))
|
||||
|
||||
# Assume this is a file we shouldn't be parsing. It's owner
|
||||
# can read the error printed to screen and take action
|
||||
# otherwise.
|
||||
return list()
|
||||
return (list(), list())
|
||||
|
||||
# Store our url read in
|
||||
url = result.group('url')
|
||||
if not url:
|
||||
url, config = result.group('url'), result.group('config')
|
||||
if not (url or config):
|
||||
# Comment/empty line; do nothing
|
||||
continue
|
||||
|
||||
if config:
|
||||
ConfigBase.logger.debug('Include URL: {}'.format(config))
|
||||
|
||||
# Store our include line
|
||||
configs.append(config.strip())
|
||||
continue
|
||||
|
||||
# Acquire our url tokens
|
||||
results = plugins.url_to_dict(url)
|
||||
if results is None:
|
||||
|
@ -422,11 +581,6 @@ class ConfigBase(URLBase):
|
|||
# notifications if any were set
|
||||
results['tag'] = set(parse_list(result.group('tags')))
|
||||
|
||||
ConfigBase.logger.trace(
|
||||
'URL {} unpacked as:{}{}'.format(
|
||||
url, os.linesep, os.linesep.join(
|
||||
['{}="{}"'.format(k, v) for k, v in results.items()])))
|
||||
|
||||
# Prepare our Asset Object
|
||||
results['asset'] = \
|
||||
asset if isinstance(asset, AppriseAsset) else AppriseAsset()
|
||||
|
@ -448,23 +602,32 @@ class ConfigBase(URLBase):
|
|||
continue
|
||||
|
||||
# if we reach here, we successfully loaded our data
|
||||
response.append(plugin)
|
||||
servers.append(plugin)
|
||||
|
||||
# Return what was loaded
|
||||
return response
|
||||
return (servers, configs)
|
||||
|
||||
@staticmethod
|
||||
def config_parse_yaml(content, asset=None):
|
||||
"""
|
||||
Parse the specified content as though it were a yaml file
|
||||
specifically formatted for apprise. Return a list of loaded
|
||||
notification plugins.
|
||||
specifically formatted for Apprise.
|
||||
|
||||
Optionally associate an asset with the notification.
|
||||
Return a tuple that looks like (servers, configs) where:
|
||||
- servers contains a list of loaded notification plugins
|
||||
- configs contains a list of additional configuration files
|
||||
referenced.
|
||||
|
||||
You may optionally associate an asset with the notification.
|
||||
|
||||
"""
|
||||
|
||||
response = list()
|
||||
# A list of loaded Notification Services
|
||||
servers = list()
|
||||
|
||||
# A list of additional configuration files referenced using
|
||||
# the include keyword
|
||||
configs = list()
|
||||
|
||||
try:
|
||||
# Load our data (safely)
|
||||
|
@ -473,23 +636,24 @@ class ConfigBase(URLBase):
|
|||
except (AttributeError, yaml.error.MarkedYAMLError) as e:
|
||||
# Invalid content
|
||||
ConfigBase.logger.error(
|
||||
'Invalid apprise yaml data specified.')
|
||||
'Invalid Apprise YAML data specified.')
|
||||
ConfigBase.logger.debug(
|
||||
'YAML Exception:{}{}'.format(os.linesep, e))
|
||||
return list()
|
||||
return (list(), list())
|
||||
|
||||
if not isinstance(result, dict):
|
||||
# Invalid content
|
||||
ConfigBase.logger.error('Invalid apprise yaml structure specified')
|
||||
return list()
|
||||
ConfigBase.logger.error(
|
||||
'Invalid Apprise YAML based configuration specified.')
|
||||
return (list(), list())
|
||||
|
||||
# YAML Version
|
||||
version = result.get('version', 1)
|
||||
if version != 1:
|
||||
# Invalid syntax
|
||||
ConfigBase.logger.error(
|
||||
'Invalid apprise yaml version specified {}.'.format(version))
|
||||
return list()
|
||||
'Invalid Apprise YAML version specified {}.'.format(version))
|
||||
return (list(), list())
|
||||
|
||||
#
|
||||
# global asset object
|
||||
|
@ -536,15 +700,38 @@ class ConfigBase(URLBase):
|
|||
# Store any preset tags
|
||||
global_tags = set(parse_list(tags))
|
||||
|
||||
#
|
||||
# include root directive
|
||||
#
|
||||
includes = result.get('include', None)
|
||||
if isinstance(includes, six.string_types):
|
||||
# Support a single inline string or multiple ones separated by a
|
||||
# comma and/or space
|
||||
includes = parse_urls(includes)
|
||||
|
||||
elif not isinstance(includes, (list, tuple)):
|
||||
# Not a problem; we simply have no includes
|
||||
includes = list()
|
||||
|
||||
# Iterate over each config URL
|
||||
for no, url in enumerate(includes):
|
||||
|
||||
if isinstance(url, six.string_types):
|
||||
# Support a single inline string or multiple ones separated by
|
||||
# a comma and/or space
|
||||
configs.extend(parse_urls(url))
|
||||
|
||||
elif isinstance(url, dict):
|
||||
# Store the url and ignore arguments associated
|
||||
configs.extend(u for u in url.keys())
|
||||
|
||||
#
|
||||
# urls root directive
|
||||
#
|
||||
urls = result.get('urls', None)
|
||||
if not isinstance(urls, (list, tuple)):
|
||||
# Unsupported
|
||||
ConfigBase.logger.error(
|
||||
'Missing "urls" directive in apprise yaml.')
|
||||
return list()
|
||||
# Not a problem; we simply have no urls
|
||||
urls = list()
|
||||
|
||||
# Iterate over each URL
|
||||
for no, url in enumerate(urls):
|
||||
|
@ -656,7 +843,7 @@ class ConfigBase(URLBase):
|
|||
else:
|
||||
# Unsupported
|
||||
ConfigBase.logger.warning(
|
||||
'Unsupported apprise yaml entry #{}'.format(no + 1))
|
||||
'Unsupported Apprise YAML entry #{}'.format(no + 1))
|
||||
continue
|
||||
|
||||
# Track our entries
|
||||
|
@ -669,7 +856,7 @@ class ConfigBase(URLBase):
|
|||
# Grab our first item
|
||||
_results = results.pop(0)
|
||||
|
||||
# tag is a special keyword that is managed by apprise object.
|
||||
# tag is a special keyword that is managed by Apprise object.
|
||||
# The below ensures our tags are set correctly
|
||||
if 'tag' in _results:
|
||||
# Tidy our list up
|
||||
|
@ -698,17 +885,19 @@ class ConfigBase(URLBase):
|
|||
ConfigBase.logger.debug(
|
||||
'Loaded URL: {}'.format(plugin.url()))
|
||||
|
||||
except Exception:
|
||||
except Exception as e:
|
||||
# the arguments are invalid or can not be used.
|
||||
ConfigBase.logger.warning(
|
||||
'Could not load apprise yaml entry #{}, item #{}'
|
||||
'Could not load Apprise YAML configuration '
|
||||
'entry #{}, item #{}'
|
||||
.format(no + 1, entry))
|
||||
ConfigBase.logger.debug('Loading Exception: %s' % str(e))
|
||||
continue
|
||||
|
||||
# if we reach here, we successfully loaded our data
|
||||
response.append(plugin)
|
||||
servers.append(plugin)
|
||||
|
||||
return response
|
||||
return (servers, configs)
|
||||
|
||||
def pop(self, index=-1):
|
||||
"""
|
||||
|
|
|
@ -28,6 +28,7 @@ import io
|
|||
import os
|
||||
from .ConfigBase import ConfigBase
|
||||
from ..common import ConfigFormat
|
||||
from ..common import ConfigIncludeMode
|
||||
from ..AppriseLocale import gettext_lazy as _
|
||||
|
||||
|
||||
|
@ -42,6 +43,9 @@ class ConfigFile(ConfigBase):
|
|||
# The default protocol
|
||||
protocol = 'file'
|
||||
|
||||
# Configuration file inclusion can only be of the same type
|
||||
allow_cross_includes = ConfigIncludeMode.STRICT
|
||||
|
||||
def __init__(self, path, **kwargs):
|
||||
"""
|
||||
Initialize File Object
|
||||
|
@ -53,7 +57,10 @@ class ConfigFile(ConfigBase):
|
|||
super(ConfigFile, self).__init__(**kwargs)
|
||||
|
||||
# Store our file path as it was set
|
||||
self.path = os.path.expanduser(path)
|
||||
self.path = os.path.abspath(os.path.expanduser(path))
|
||||
|
||||
# Update the config path to be relative to our file we just loaded
|
||||
self.config_path = os.path.dirname(self.path)
|
||||
|
||||
return
|
||||
|
||||
|
@ -69,19 +76,19 @@ class ConfigFile(ConfigBase):
|
|||
else:
|
||||
cache = int(self.cache)
|
||||
|
||||
# Define any arguments set
|
||||
args = {
|
||||
# Define any URL parameters
|
||||
params = {
|
||||
'encoding': self.encoding,
|
||||
'cache': cache,
|
||||
}
|
||||
|
||||
if self.config_format:
|
||||
# A format was enforced; make sure it's passed back with the url
|
||||
args['format'] = self.config_format
|
||||
params['format'] = self.config_format
|
||||
|
||||
return 'file://{path}{args}'.format(
|
||||
return 'file://{path}{params}'.format(
|
||||
path=self.quote(self.path),
|
||||
args='?{}'.format(self.urlencode(args)) if args else '',
|
||||
params='?{}'.format(self.urlencode(params)) if params else '',
|
||||
)
|
||||
|
||||
def read(self, **kwargs):
|
||||
|
@ -91,10 +98,9 @@ class ConfigFile(ConfigBase):
|
|||
|
||||
response = None
|
||||
|
||||
path = os.path.expanduser(self.path)
|
||||
try:
|
||||
if self.max_buffer_size > 0 and \
|
||||
os.path.getsize(path) > self.max_buffer_size:
|
||||
os.path.getsize(self.path) > self.max_buffer_size:
|
||||
|
||||
# Content exceeds maximum buffer size
|
||||
self.logger.error(
|
||||
|
@ -106,7 +112,7 @@ class ConfigFile(ConfigBase):
|
|||
# getsize() can throw this acception if the file is missing
|
||||
# and or simply isn't accessible
|
||||
self.logger.error(
|
||||
'File is not accessible: {}'.format(path))
|
||||
'File is not accessible: {}'.format(self.path))
|
||||
return None
|
||||
|
||||
# Always call throttle before any server i/o is made
|
||||
|
@ -115,7 +121,7 @@ class ConfigFile(ConfigBase):
|
|||
try:
|
||||
# Python 3 just supports open(), however to remain compatible with
|
||||
# Python 2, we use the io module
|
||||
with io.open(path, "rt", encoding=self.encoding) as f:
|
||||
with io.open(self.path, "rt", encoding=self.encoding) as f:
|
||||
# Store our content for parsing
|
||||
response = f.read()
|
||||
|
||||
|
@ -126,7 +132,7 @@ class ConfigFile(ConfigBase):
|
|||
|
||||
self.logger.error(
|
||||
'File not using expected encoding ({}) : {}'.format(
|
||||
self.encoding, path))
|
||||
self.encoding, self.path))
|
||||
return None
|
||||
|
||||
except (IOError, OSError):
|
||||
|
@ -136,13 +142,13 @@ class ConfigFile(ConfigBase):
|
|||
# Could not open and/or read the file; this is not a problem since
|
||||
# we scan a lot of default paths.
|
||||
self.logger.error(
|
||||
'File can not be opened for read: {}'.format(path))
|
||||
'File can not be opened for read: {}'.format(self.path))
|
||||
return None
|
||||
|
||||
# Detect config format based on file extension if it isn't already
|
||||
# enforced
|
||||
if self.config_format is None and \
|
||||
re.match(r'^.*\.ya?ml\s*$', path, re.I) is not None:
|
||||
re.match(r'^.*\.ya?ml\s*$', self.path, re.I) is not None:
|
||||
|
||||
# YAML Filename Detected
|
||||
self.default_config_format = ConfigFormat.YAML
|
||||
|
@ -163,7 +169,7 @@ class ConfigFile(ConfigBase):
|
|||
# We're done early; it's not a good URL
|
||||
return results
|
||||
|
||||
match = re.match(r'file://(?P<path>[^?]+)(\?.*)?', url, re.I)
|
||||
match = re.match(r'[a-z0-9]+://(?P<path>[^?]+)(\?.*)?', url, re.I)
|
||||
if not match:
|
||||
return None
|
||||
|
||||
|
|
|
@ -28,6 +28,7 @@ import six
|
|||
import requests
|
||||
from .ConfigBase import ConfigBase
|
||||
from ..common import ConfigFormat
|
||||
from ..common import ConfigIncludeMode
|
||||
from ..URLBase import PrivacyMode
|
||||
from ..AppriseLocale import gettext_lazy as _
|
||||
|
||||
|
@ -58,16 +59,15 @@ class ConfigHTTP(ConfigBase):
|
|||
# The default secure protocol
|
||||
secure_protocol = 'https'
|
||||
|
||||
# The maximum number of seconds to wait for a connection to be established
|
||||
# before out-right just giving up
|
||||
connection_timeout_sec = 5.0
|
||||
|
||||
# If an HTTP error occurs, define the number of characters you still want
|
||||
# to read back. This is useful for debugging purposes, but nothing else.
|
||||
# The idea behind enforcing this kind of restriction is to prevent abuse
|
||||
# from queries to services that may be untrusted.
|
||||
max_error_buffer_size = 2048
|
||||
|
||||
# Configuration file inclusion can always include this type
|
||||
allow_cross_includes = ConfigIncludeMode.ALWAYS
|
||||
|
||||
def __init__(self, headers=None, **kwargs):
|
||||
"""
|
||||
Initialize HTTP Object
|
||||
|
@ -104,18 +104,20 @@ class ConfigHTTP(ConfigBase):
|
|||
cache = int(self.cache)
|
||||
|
||||
# Define any arguments set
|
||||
args = {
|
||||
'verify': 'yes' if self.verify_certificate else 'no',
|
||||
params = {
|
||||
'encoding': self.encoding,
|
||||
'cache': cache,
|
||||
}
|
||||
|
||||
# Extend our parameters
|
||||
params.update(self.url_parameters(privacy=privacy, *args, **kwargs))
|
||||
|
||||
if self.config_format:
|
||||
# A format was enforced; make sure it's passed back with the url
|
||||
args['format'] = self.config_format
|
||||
params['format'] = self.config_format
|
||||
|
||||
# Append our headers into our args
|
||||
args.update({'+{}'.format(k): v for k, v in self.headers.items()})
|
||||
params.update({'+{}'.format(k): v for k, v in self.headers.items()})
|
||||
|
||||
# Determine Authentication
|
||||
auth = ''
|
||||
|
@ -132,14 +134,14 @@ class ConfigHTTP(ConfigBase):
|
|||
|
||||
default_port = 443 if self.secure else 80
|
||||
|
||||
return '{schema}://{auth}{hostname}{port}{fullpath}/?{args}'.format(
|
||||
return '{schema}://{auth}{hostname}{port}{fullpath}/?{params}'.format(
|
||||
schema=self.secure_protocol if self.secure else self.protocol,
|
||||
auth=auth,
|
||||
hostname=self.quote(self.host, safe=''),
|
||||
port='' if self.port is None or self.port == default_port
|
||||
else ':{}'.format(self.port),
|
||||
fullpath=self.quote(self.fullpath, safe='/'),
|
||||
args=self.urlencode(args),
|
||||
params=self.urlencode(params),
|
||||
)
|
||||
|
||||
def read(self, **kwargs):
|
||||
|
@ -185,7 +187,7 @@ class ConfigHTTP(ConfigBase):
|
|||
headers=headers,
|
||||
auth=auth,
|
||||
verify=self.verify_certificate,
|
||||
timeout=self.connection_timeout_sec,
|
||||
timeout=self.request_timeout,
|
||||
stream=True) as r:
|
||||
|
||||
# Handle Errors
|
||||
|
@ -211,7 +213,7 @@ class ConfigHTTP(ConfigBase):
|
|||
return None
|
||||
|
||||
# Store our result (but no more than our buffer length)
|
||||
response = r.content[:self.max_buffer_size + 1]
|
||||
response = r.text[:self.max_buffer_size + 1]
|
||||
|
||||
# Verify that our content did not exceed the buffer size:
|
||||
if len(response) > self.max_buffer_size:
|
||||
|
@ -240,7 +242,7 @@ class ConfigHTTP(ConfigBase):
|
|||
|
||||
except requests.RequestException as e:
|
||||
self.logger.error(
|
||||
'A Connection error occured retrieving HTTP '
|
||||
'A Connection error occurred retrieving HTTP '
|
||||
'configuration from %s.' % self.host)
|
||||
self.logger.debug('Socket Exception: %s' % str(e))
|
||||
|
||||
|
@ -254,7 +256,7 @@ class ConfigHTTP(ConfigBase):
|
|||
def parse_url(url):
|
||||
"""
|
||||
Parses the URL and returns enough arguments that can allow
|
||||
us to substantiate this object.
|
||||
us to re-instantiate this object.
|
||||
|
||||
"""
|
||||
results = ConfigBase.parse_url(url)
|
||||
|
|
|
@ -23,12 +23,12 @@
|
|||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
# THE SOFTWARE.
|
||||
|
||||
import six
|
||||
import re
|
||||
|
||||
import six
|
||||
from os import listdir
|
||||
from os.path import dirname
|
||||
from os.path import abspath
|
||||
from ..logger import logger
|
||||
|
||||
# Maintains a mapping of all of the configuration services
|
||||
SCHEMA_MAP = {}
|
||||
|
@ -88,29 +88,39 @@ def __load_matrix(path=abspath(dirname(__file__)), name='apprise.config'):
|
|||
# not the module:
|
||||
globals()[plugin_name] = plugin
|
||||
|
||||
# Load protocol(s) if defined
|
||||
proto = getattr(plugin, 'protocol', None)
|
||||
if isinstance(proto, six.string_types):
|
||||
if proto not in SCHEMA_MAP:
|
||||
SCHEMA_MAP[proto] = plugin
|
||||
fn = getattr(plugin, 'schemas', None)
|
||||
try:
|
||||
schemas = set([]) if not callable(fn) else fn(plugin)
|
||||
|
||||
elif isinstance(proto, (set, list, tuple)):
|
||||
# Support iterables list types
|
||||
for p in proto:
|
||||
if p not in SCHEMA_MAP:
|
||||
SCHEMA_MAP[p] = plugin
|
||||
except TypeError:
|
||||
# Python v2.x support where functions associated with classes
|
||||
# were considered bound to them and could not be called prior
|
||||
# to the classes initialization. This code can be dropped
|
||||
# once Python v2.x support is dropped. The below code introduces
|
||||
# replication as it already exists and is tested in
|
||||
# URLBase.schemas()
|
||||
schemas = set([])
|
||||
for key in ('protocol', 'secure_protocol'):
|
||||
schema = getattr(plugin, key, None)
|
||||
if isinstance(schema, six.string_types):
|
||||
schemas.add(schema)
|
||||
|
||||
# Load secure protocol(s) if defined
|
||||
protos = getattr(plugin, 'secure_protocol', None)
|
||||
if isinstance(protos, six.string_types):
|
||||
if protos not in SCHEMA_MAP:
|
||||
SCHEMA_MAP[protos] = plugin
|
||||
elif isinstance(schema, (set, list, tuple)):
|
||||
# Support iterables list types
|
||||
for s in schema:
|
||||
if isinstance(s, six.string_types):
|
||||
schemas.add(s)
|
||||
|
||||
if isinstance(protos, (set, list, tuple)):
|
||||
# Support iterables list types
|
||||
for p in protos:
|
||||
if p not in SCHEMA_MAP:
|
||||
SCHEMA_MAP[p] = plugin
|
||||
# map our schema to our plugin
|
||||
for schema in schemas:
|
||||
if schema in SCHEMA_MAP:
|
||||
logger.error(
|
||||
"Config schema ({}) mismatch detected - {} to {}"
|
||||
.format(schema, SCHEMA_MAP[schema], plugin))
|
||||
continue
|
||||
|
||||
# Assign plugin
|
||||
SCHEMA_MAP[schema] = plugin
|
||||
|
||||
return SCHEMA_MAP
|
||||
|
||||
|
|
|
@ -6,16 +6,16 @@
|
|||
#, fuzzy
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: apprise 0.8.5\n"
|
||||
"Project-Id-Version: apprise 0.8.8\n"
|
||||
"Report-Msgid-Bugs-To: lead2gold@gmail.com\n"
|
||||
"POT-Creation-Date: 2020-03-30 16:00-0400\n"
|
||||
"POT-Creation-Date: 2020-09-02 07:46-0400\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=utf-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Generated-By: Babel 2.8.0\n"
|
||||
"Generated-By: Babel 2.7.0\n"
|
||||
|
||||
msgid "API Key"
|
||||
msgstr ""
|
||||
|
@ -35,6 +35,9 @@ msgstr ""
|
|||
msgid "Access Token"
|
||||
msgstr ""
|
||||
|
||||
msgid "Account Email"
|
||||
msgstr ""
|
||||
|
||||
msgid "Account SID"
|
||||
msgstr ""
|
||||
|
||||
|
@ -59,6 +62,9 @@ msgstr ""
|
|||
msgid "Avatar Image"
|
||||
msgstr ""
|
||||
|
||||
msgid "Avatar URL"
|
||||
msgstr ""
|
||||
|
||||
msgid "Batch Mode"
|
||||
msgstr ""
|
||||
|
||||
|
@ -83,6 +89,12 @@ msgstr ""
|
|||
msgid "Channels"
|
||||
msgstr ""
|
||||
|
||||
msgid "Client ID"
|
||||
msgstr ""
|
||||
|
||||
msgid "Client Secret"
|
||||
msgstr ""
|
||||
|
||||
msgid "Consumer Key"
|
||||
msgstr ""
|
||||
|
||||
|
@ -92,9 +104,18 @@ msgstr ""
|
|||
msgid "Country"
|
||||
msgstr ""
|
||||
|
||||
msgid "Custom Icon"
|
||||
msgstr ""
|
||||
|
||||
msgid "Cycles"
|
||||
msgstr ""
|
||||
|
||||
msgid "Detect Bot Owner"
|
||||
msgstr ""
|
||||
|
||||
msgid "Device API Key"
|
||||
msgstr ""
|
||||
|
||||
msgid "Device ID"
|
||||
msgstr ""
|
||||
|
||||
|
@ -161,6 +182,9 @@ msgstr ""
|
|||
msgid "IRC Colors"
|
||||
msgstr ""
|
||||
|
||||
msgid "Icon Type"
|
||||
msgstr ""
|
||||
|
||||
msgid "Include Footer"
|
||||
msgstr ""
|
||||
|
||||
|
@ -188,6 +212,9 @@ msgstr ""
|
|||
msgid "Modal"
|
||||
msgstr ""
|
||||
|
||||
msgid "Mode"
|
||||
msgstr ""
|
||||
|
||||
msgid "Notify Format"
|
||||
msgstr ""
|
||||
|
||||
|
@ -269,6 +296,12 @@ msgstr ""
|
|||
msgid "Server Timeout"
|
||||
msgstr ""
|
||||
|
||||
msgid "Socket Connect Timeout"
|
||||
msgstr ""
|
||||
|
||||
msgid "Socket Read Timeout"
|
||||
msgstr ""
|
||||
|
||||
msgid "Sound"
|
||||
msgstr ""
|
||||
|
||||
|
@ -281,6 +314,12 @@ msgstr ""
|
|||
msgid "Source Phone No"
|
||||
msgstr ""
|
||||
|
||||
msgid "Sticky"
|
||||
msgstr ""
|
||||
|
||||
msgid "Subtitle"
|
||||
msgstr ""
|
||||
|
||||
msgid "Target Channel"
|
||||
msgstr ""
|
||||
|
||||
|
@ -338,6 +377,9 @@ msgstr ""
|
|||
msgid "Template Data"
|
||||
msgstr ""
|
||||
|
||||
msgid "Tenant Domain"
|
||||
msgstr ""
|
||||
|
||||
msgid "Text To Speech"
|
||||
msgstr ""
|
||||
|
||||
|
@ -368,6 +410,9 @@ msgstr ""
|
|||
msgid "Use Avatar"
|
||||
msgstr ""
|
||||
|
||||
msgid "User ID"
|
||||
msgstr ""
|
||||
|
||||
msgid "User Key"
|
||||
msgstr ""
|
||||
|
||||
|
|
|
@ -24,6 +24,7 @@
|
|||
# THE SOFTWARE.
|
||||
|
||||
import re
|
||||
import six
|
||||
|
||||
from ..URLBase import URLBase
|
||||
from ..common import NotifyType
|
||||
|
@ -36,7 +37,17 @@ from ..AppriseLocale import gettext_lazy as _
|
|||
from ..AppriseAttachment import AppriseAttachment
|
||||
|
||||
|
||||
class NotifyBase(URLBase):
|
||||
if six.PY3:
|
||||
# Wrap our base with the asyncio wrapper
|
||||
from ..py3compat.asyncio import AsyncNotifyBase
|
||||
BASE_OBJECT = AsyncNotifyBase
|
||||
|
||||
else:
|
||||
# Python v2.7 (backwards compatibility)
|
||||
BASE_OBJECT = URLBase
|
||||
|
||||
|
||||
class NotifyBase(BASE_OBJECT):
|
||||
"""
|
||||
This is the base class for all notification services
|
||||
"""
|
||||
|
@ -80,21 +91,11 @@ class NotifyBase(URLBase):
|
|||
# use a <b> tag. The below causes the <b>title</b> to get generated:
|
||||
default_html_tag_id = 'b'
|
||||
|
||||
# Define a default set of template arguments used for dynamically building
|
||||
# details about our individual plugins for developers.
|
||||
|
||||
# Define object templates
|
||||
templates = ()
|
||||
|
||||
# Provides a mapping of tokens, certain entries are fixed and automatically
|
||||
# configured if found (such as schema, host, user, pass, and port)
|
||||
template_tokens = {}
|
||||
|
||||
# Here is where we define all of the arguments we accept on the url
|
||||
# such as: schema://whatever/?overflow=upstream&format=text
|
||||
# These act the same way as tokens except they are optional and/or
|
||||
# have default values set if mandatory. This rule must be followed
|
||||
template_args = {
|
||||
template_args = dict(URLBase.template_args, **{
|
||||
'overflow': {
|
||||
'name': _('Overflow Mode'),
|
||||
'type': 'choice:string',
|
||||
|
@ -119,34 +120,7 @@ class NotifyBase(URLBase):
|
|||
# runtime.
|
||||
'_lookup_default': 'notify_format',
|
||||
},
|
||||
'verify': {
|
||||
'name': _('Verify SSL'),
|
||||
# SSL Certificate Authority Verification
|
||||
'type': 'bool',
|
||||
# Provide a default
|
||||
'default': URLBase.verify_certificate,
|
||||
# look up default using the following parent class value at
|
||||
# runtime.
|
||||
'_lookup_default': 'verify_certificate',
|
||||
},
|
||||
}
|
||||
|
||||
# kwargs are dynamically built because a prefix causes us to parse the
|
||||
# content slightly differently. The prefix is required and can be either
|
||||
# a (+ or -). Below would handle the +key=value:
|
||||
# {
|
||||
# 'headers': {
|
||||
# 'name': _('HTTP Header'),
|
||||
# 'prefix': '+',
|
||||
# 'type': 'string',
|
||||
# },
|
||||
# },
|
||||
#
|
||||
# In a kwarg situation, the 'key' is always presumed to be treated as
|
||||
# a string. When the 'type' is defined, it is being defined to respect
|
||||
# the 'value'.
|
||||
|
||||
template_kwargs = {}
|
||||
})
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
"""
|
||||
|
@ -161,7 +135,7 @@ class NotifyBase(URLBase):
|
|||
# Store the specified format if specified
|
||||
notify_format = kwargs.get('format', '')
|
||||
if notify_format.lower() not in NOTIFY_FORMATS:
|
||||
msg = 'Invalid notification format %s'.format(notify_format)
|
||||
msg = 'Invalid notification format {}'.format(notify_format)
|
||||
self.logger.error(msg)
|
||||
raise TypeError(msg)
|
||||
|
||||
|
@ -368,6 +342,23 @@ class NotifyBase(URLBase):
|
|||
raise NotImplementedError(
|
||||
"send() is not implimented by the child class.")
|
||||
|
||||
def url_parameters(self, *args, **kwargs):
|
||||
"""
|
||||
Provides a default set of parameters to work with. This can greatly
|
||||
simplify URL construction in the acommpanied url() function in all
|
||||
defined plugin services.
|
||||
"""
|
||||
|
||||
params = {
|
||||
'format': self.notify_format,
|
||||
'overflow': self.overflow_mode,
|
||||
}
|
||||
|
||||
params.update(super(NotifyBase, self).url_parameters(*args, **kwargs))
|
||||
|
||||
# return default parameters
|
||||
return params
|
||||
|
||||
@staticmethod
|
||||
def parse_url(url, verify_host=True):
|
||||
"""Parses the URL and returns it broken apart into a dictionary.
|
||||
|
|
|
@ -279,6 +279,7 @@ class NotifyBoxcar(NotifyBase):
|
|||
data=dumps(payload),
|
||||
headers=headers,
|
||||
verify=self.verify_certificate,
|
||||
timeout=self.request_timeout,
|
||||
)
|
||||
|
||||
# Boxcar returns 201 (Created) when successful
|
||||
|
@ -304,7 +305,7 @@ class NotifyBoxcar(NotifyBase):
|
|||
|
||||
except requests.RequestException as e:
|
||||
self.logger.warning(
|
||||
'A Connection error occured sending Boxcar '
|
||||
'A Connection error occurred sending Boxcar '
|
||||
'notification to %s.' % (host))
|
||||
|
||||
self.logger.debug('Socket Exception: %s' % str(e))
|
||||
|
@ -319,15 +320,15 @@ class NotifyBoxcar(NotifyBase):
|
|||
Returns the URL built dynamically based on specified arguments.
|
||||
"""
|
||||
|
||||
# Define any arguments set
|
||||
args = {
|
||||
'format': self.notify_format,
|
||||
'overflow': self.overflow_mode,
|
||||
# Define any URL parameters
|
||||
params = {
|
||||
'image': 'yes' if self.include_image else 'no',
|
||||
'verify': 'yes' if self.verify_certificate else 'no',
|
||||
}
|
||||
|
||||
return '{schema}://{access}/{secret}/{targets}?{args}'.format(
|
||||
# Extend our parameters
|
||||
params.update(self.url_parameters(privacy=privacy, *args, **kwargs))
|
||||
|
||||
return '{schema}://{access}/{secret}/{targets}?{params}'.format(
|
||||
schema=self.secure_protocol,
|
||||
access=self.pprint(self.access, privacy, safe=''),
|
||||
secret=self.pprint(
|
||||
|
@ -335,7 +336,7 @@ class NotifyBoxcar(NotifyBase):
|
|||
targets='/'.join([
|
||||
NotifyBoxcar.quote(x, safe='') for x in chain(
|
||||
self.tags, self.device_tokens) if x != DEFAULT_TAG]),
|
||||
args=NotifyBoxcar.urlencode(args),
|
||||
params=NotifyBoxcar.urlencode(params),
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
|
@ -345,7 +346,6 @@ class NotifyBoxcar(NotifyBase):
|
|||
|
||||
"""
|
||||
results = NotifyBase.parse_url(url, verify_host=False)
|
||||
|
||||
if not results:
|
||||
# We're done early
|
||||
return None
|
||||
|
|
|
@ -221,6 +221,7 @@ class NotifyClickSend(NotifyBase):
|
|||
data=dumps(payload),
|
||||
headers=headers,
|
||||
verify=self.verify_certificate,
|
||||
timeout=self.request_timeout,
|
||||
)
|
||||
if r.status_code != requests.codes.ok:
|
||||
# We had a problem
|
||||
|
@ -256,7 +257,7 @@ class NotifyClickSend(NotifyBase):
|
|||
|
||||
except requests.RequestException as e:
|
||||
self.logger.warning(
|
||||
'A Connection error occured sending {} ClickSend '
|
||||
'A Connection error occurred sending {} ClickSend '
|
||||
'notification(s).'.format(len(payload['messages'])))
|
||||
self.logger.debug('Socket Exception: %s' % str(e))
|
||||
|
||||
|
@ -271,14 +272,14 @@ class NotifyClickSend(NotifyBase):
|
|||
Returns the URL built dynamically based on specified arguments.
|
||||
"""
|
||||
|
||||
# Define any arguments set
|
||||
args = {
|
||||
'format': self.notify_format,
|
||||
'overflow': self.overflow_mode,
|
||||
'verify': 'yes' if self.verify_certificate else 'no',
|
||||
# Define any URL parameters
|
||||
params = {
|
||||
'batch': 'yes' if self.batch else 'no',
|
||||
}
|
||||
|
||||
# Extend our parameters
|
||||
params.update(self.url_parameters(privacy=privacy, *args, **kwargs))
|
||||
|
||||
# Setup Authentication
|
||||
auth = '{user}:{password}@'.format(
|
||||
user=NotifyClickSend.quote(self.user, safe=''),
|
||||
|
@ -286,19 +287,19 @@ class NotifyClickSend(NotifyBase):
|
|||
self.password, privacy, mode=PrivacyMode.Secret, safe=''),
|
||||
)
|
||||
|
||||
return '{schema}://{auth}{targets}?{args}'.format(
|
||||
return '{schema}://{auth}{targets}?{params}'.format(
|
||||
schema=self.secure_protocol,
|
||||
auth=auth,
|
||||
targets='/'.join(
|
||||
[NotifyClickSend.quote(x, safe='') for x in self.targets]),
|
||||
args=NotifyClickSend.urlencode(args),
|
||||
params=NotifyClickSend.urlencode(params),
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def parse_url(url):
|
||||
"""
|
||||
Parses the URL and returns enough arguments that can allow
|
||||
us to substantiate this object.
|
||||
us to re-instantiate this object.
|
||||
|
||||
"""
|
||||
results = NotifyBase.parse_url(url, verify_host=False)
|
||||
|
|
|
@ -304,6 +304,7 @@ class NotifyD7Networks(NotifyBase):
|
|||
data=dumps(payload),
|
||||
headers=headers,
|
||||
verify=self.verify_certificate,
|
||||
timeout=self.request_timeout,
|
||||
)
|
||||
|
||||
if r.status_code not in (
|
||||
|
@ -379,7 +380,7 @@ class NotifyD7Networks(NotifyBase):
|
|||
|
||||
except requests.RequestException as e:
|
||||
self.logger.warning(
|
||||
'A Connection error occured sending D7 Networks:%s ' % (
|
||||
'A Connection error occurred sending D7 Networks:%s ' % (
|
||||
', '.join(self.targets)) + 'notification.'
|
||||
)
|
||||
self.logger.debug('Socket Exception: %s' % str(e))
|
||||
|
@ -394,38 +395,37 @@ class NotifyD7Networks(NotifyBase):
|
|||
Returns the URL built dynamically based on specified arguments.
|
||||
"""
|
||||
|
||||
# Define any arguments set
|
||||
args = {
|
||||
'format': self.notify_format,
|
||||
'overflow': self.overflow_mode,
|
||||
'verify': 'yes' if self.verify_certificate else 'no',
|
||||
# Define any URL parameters
|
||||
params = {
|
||||
'batch': 'yes' if self.batch else 'no',
|
||||
}
|
||||
|
||||
# Extend our parameters
|
||||
params.update(self.url_parameters(privacy=privacy, *args, **kwargs))
|
||||
|
||||
if self.priority != self.template_args['priority']['default']:
|
||||
args['priority'] = str(self.priority)
|
||||
params['priority'] = str(self.priority)
|
||||
|
||||
if self.source:
|
||||
args['from'] = self.source
|
||||
params['from'] = self.source
|
||||
|
||||
return '{schema}://{user}:{password}@{targets}/?{args}'.format(
|
||||
return '{schema}://{user}:{password}@{targets}/?{params}'.format(
|
||||
schema=self.secure_protocol,
|
||||
user=NotifyD7Networks.quote(self.user, safe=''),
|
||||
password=self.pprint(
|
||||
self.password, privacy, mode=PrivacyMode.Secret, safe=''),
|
||||
targets='/'.join(
|
||||
[NotifyD7Networks.quote(x, safe='') for x in self.targets]),
|
||||
args=NotifyD7Networks.urlencode(args))
|
||||
params=NotifyD7Networks.urlencode(params))
|
||||
|
||||
@staticmethod
|
||||
def parse_url(url):
|
||||
"""
|
||||
Parses the URL and returns enough arguments that can allow
|
||||
us to substantiate this object.
|
||||
us to re-instantiate this object.
|
||||
|
||||
"""
|
||||
results = NotifyBase.parse_url(url, verify_host=False)
|
||||
|
||||
if not results:
|
||||
# We're done early as we couldn't load the results
|
||||
return results
|
||||
|
|
|
@ -29,7 +29,6 @@ from __future__ import print_function
|
|||
from .NotifyBase import NotifyBase
|
||||
from ..common import NotifyImageSize
|
||||
from ..common import NotifyType
|
||||
from ..utils import GET_SCHEMA_RE
|
||||
from ..utils import parse_bool
|
||||
from ..AppriseLocale import gettext_lazy as _
|
||||
|
||||
|
@ -141,7 +140,6 @@ class NotifyDBus(NotifyBase):
|
|||
# object if we were to reference, we wouldn't be backwards compatible with
|
||||
# Python v2. So converting the result set back into a list makes us
|
||||
# compatible
|
||||
|
||||
protocol = list(MAINLOOP_MAP.keys())
|
||||
|
||||
# A URL that takes you to the setup/help of the specific protocol
|
||||
|
@ -153,7 +151,7 @@ class NotifyDBus(NotifyBase):
|
|||
# Allows the user to specify the NotifyImageSize object
|
||||
image_size = NotifyImageSize.XY_128
|
||||
|
||||
# The number of seconds to keep the message present for
|
||||
# The number of milliseconds to keep the message present for
|
||||
message_timeout_ms = 13000
|
||||
|
||||
# Limit results to just the first 10 line otherwise there is just to much
|
||||
|
@ -171,7 +169,7 @@ class NotifyDBus(NotifyBase):
|
|||
|
||||
# Define object templates
|
||||
templates = (
|
||||
'{schema}://_/',
|
||||
'{schema}://',
|
||||
)
|
||||
|
||||
# Define our template arguments
|
||||
|
@ -355,27 +353,27 @@ class NotifyDBus(NotifyBase):
|
|||
DBusUrgency.HIGH: 'high',
|
||||
}
|
||||
|
||||
# Define any arguments set
|
||||
args = {
|
||||
'format': self.notify_format,
|
||||
'overflow': self.overflow_mode,
|
||||
# Define any URL parameters
|
||||
params = {
|
||||
'image': 'yes' if self.include_image else 'no',
|
||||
'urgency': 'normal' if self.urgency not in _map
|
||||
else _map[self.urgency],
|
||||
'verify': 'yes' if self.verify_certificate else 'no',
|
||||
}
|
||||
|
||||
# Extend our parameters
|
||||
params.update(self.url_parameters(privacy=privacy, *args, **kwargs))
|
||||
|
||||
# x in (x,y) screen coordinates
|
||||
if self.x_axis:
|
||||
args['x'] = str(self.x_axis)
|
||||
params['x'] = str(self.x_axis)
|
||||
|
||||
# y in (x,y) screen coordinates
|
||||
if self.y_axis:
|
||||
args['y'] = str(self.y_axis)
|
||||
params['y'] = str(self.y_axis)
|
||||
|
||||
return '{schema}://_/?{args}'.format(
|
||||
return '{schema}://_/?{params}'.format(
|
||||
schema=self.schema,
|
||||
args=NotifyDBus.urlencode(args),
|
||||
params=NotifyDBus.urlencode(params),
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
|
@ -386,24 +384,8 @@ class NotifyDBus(NotifyBase):
|
|||
is in place.
|
||||
|
||||
"""
|
||||
schema = GET_SCHEMA_RE.match(url)
|
||||
if schema is None:
|
||||
# Content is simply not parseable
|
||||
return None
|
||||
|
||||
results = NotifyBase.parse_url(url)
|
||||
if not results:
|
||||
results = {
|
||||
'schema': schema.group('schema').lower(),
|
||||
'user': None,
|
||||
'password': None,
|
||||
'port': None,
|
||||
'host': '_',
|
||||
'fullpath': None,
|
||||
'path': None,
|
||||
'url': url,
|
||||
'qsd': {},
|
||||
}
|
||||
results = NotifyBase.parse_url(url, verify_host=False)
|
||||
|
||||
# Include images with our message
|
||||
results['include_image'] = \
|
||||
|
|
|
@ -28,17 +28,17 @@
|
|||
# here you'll be able to access the Webhooks menu and create a new one.
|
||||
#
|
||||
# When you've completed, you'll get a URL that looks a little like this:
|
||||
# https://discordapp.com/api/webhooks/417429632418316298/\
|
||||
# https://discord.com/api/webhooks/417429632418316298/\
|
||||
# JHZ7lQml277CDHmQKMHI8qBe7bk2ZwO5UKjCiOAF7711o33MyqU344Qpgv7YTpadV_js
|
||||
#
|
||||
# Simplified, it looks like this:
|
||||
# https://discordapp.com/api/webhooks/WEBHOOK_ID/WEBHOOK_TOKEN
|
||||
# https://discord.com/api/webhooks/WEBHOOK_ID/WEBHOOK_TOKEN
|
||||
#
|
||||
# This plugin will simply work using the url of:
|
||||
# discord://WEBHOOK_ID/WEBHOOK_TOKEN
|
||||
#
|
||||
# API Documentation on Webhooks:
|
||||
# - https://discordapp.com/developers/docs/resources/webhook
|
||||
# - https://discord.com/developers/docs/resources/webhook
|
||||
#
|
||||
import re
|
||||
import requests
|
||||
|
@ -63,7 +63,7 @@ class NotifyDiscord(NotifyBase):
|
|||
service_name = 'Discord'
|
||||
|
||||
# The services URL
|
||||
service_url = 'https://discordapp.com/'
|
||||
service_url = 'https://discord.com/'
|
||||
|
||||
# The default secure protocol
|
||||
secure_protocol = 'discord'
|
||||
|
@ -72,7 +72,7 @@ class NotifyDiscord(NotifyBase):
|
|||
setup_url = 'https://github.com/caronc/apprise/wiki/Notify_discord'
|
||||
|
||||
# Discord Webhook
|
||||
notify_url = 'https://discordapp.com/api/webhooks'
|
||||
notify_url = 'https://discord.com/api/webhooks'
|
||||
|
||||
# Allows the user to specify the NotifyImageSize object
|
||||
image_size = NotifyImageSize.XY_256
|
||||
|
@ -119,6 +119,10 @@ class NotifyDiscord(NotifyBase):
|
|||
'type': 'bool',
|
||||
'default': True,
|
||||
},
|
||||
'avatar_url': {
|
||||
'name': _('Avatar URL'),
|
||||
'type': 'string',
|
||||
},
|
||||
'footer': {
|
||||
'name': _('Display Footer'),
|
||||
'type': 'bool',
|
||||
|
@ -139,7 +143,7 @@ class NotifyDiscord(NotifyBase):
|
|||
|
||||
def __init__(self, webhook_id, webhook_token, tts=False, avatar=True,
|
||||
footer=False, footer_logo=True, include_image=False,
|
||||
**kwargs):
|
||||
avatar_url=None, **kwargs):
|
||||
"""
|
||||
Initialize Discord Object
|
||||
|
||||
|
@ -177,6 +181,11 @@ class NotifyDiscord(NotifyBase):
|
|||
# Place a thumbnail image inline with the message body
|
||||
self.include_image = include_image
|
||||
|
||||
# Avatar URL
|
||||
# This allows a user to provide an over-ride to the otherwise
|
||||
# dynamically generated avatar url images
|
||||
self.avatar_url = avatar_url
|
||||
|
||||
return
|
||||
|
||||
def send(self, body, title='', notify_type=NotifyType.INFO, attach=None,
|
||||
|
@ -247,8 +256,9 @@ class NotifyDiscord(NotifyBase):
|
|||
payload['content'] = \
|
||||
body if not title else "{}\r\n{}".format(title, body)
|
||||
|
||||
if self.avatar and image_url:
|
||||
payload['avatar_url'] = image_url
|
||||
if self.avatar and (image_url or self.avatar_url):
|
||||
payload['avatar_url'] = \
|
||||
self.avatar_url if self.avatar_url else image_url
|
||||
|
||||
if self.user:
|
||||
# Optionally override the default username of the webhook
|
||||
|
@ -343,6 +353,7 @@ class NotifyDiscord(NotifyBase):
|
|||
headers=headers,
|
||||
files=files,
|
||||
verify=self.verify_certificate,
|
||||
timeout=self.request_timeout,
|
||||
)
|
||||
if r.status_code not in (
|
||||
requests.codes.ok, requests.codes.no_content):
|
||||
|
@ -370,14 +381,14 @@ class NotifyDiscord(NotifyBase):
|
|||
|
||||
except requests.RequestException as e:
|
||||
self.logger.warning(
|
||||
'A Connection error occured posting {}to Discord.'.format(
|
||||
'A Connection error occurred posting {}to Discord.'.format(
|
||||
attach.name if attach else ''))
|
||||
self.logger.debug('Socket Exception: %s' % str(e))
|
||||
return False
|
||||
|
||||
except (OSError, IOError) as e:
|
||||
self.logger.warning(
|
||||
'An I/O error occured while reading {}.'.format(
|
||||
'An I/O error occurred while reading {}.'.format(
|
||||
attach.name if attach else 'attachment'))
|
||||
self.logger.debug('I/O Exception: %s' % str(e))
|
||||
return False
|
||||
|
@ -395,37 +406,36 @@ class NotifyDiscord(NotifyBase):
|
|||
Returns the URL built dynamically based on specified arguments.
|
||||
"""
|
||||
|
||||
# Define any arguments set
|
||||
args = {
|
||||
'format': self.notify_format,
|
||||
'overflow': self.overflow_mode,
|
||||
# Define any URL parameters
|
||||
params = {
|
||||
'tts': 'yes' if self.tts else 'no',
|
||||
'avatar': 'yes' if self.avatar else 'no',
|
||||
'footer': 'yes' if self.footer else 'no',
|
||||
'footer_logo': 'yes' if self.footer_logo else 'no',
|
||||
'image': 'yes' if self.include_image else 'no',
|
||||
'verify': 'yes' if self.verify_certificate else 'no',
|
||||
}
|
||||
|
||||
return '{schema}://{webhook_id}/{webhook_token}/?{args}'.format(
|
||||
# Extend our parameters
|
||||
params.update(self.url_parameters(privacy=privacy, *args, **kwargs))
|
||||
|
||||
return '{schema}://{webhook_id}/{webhook_token}/?{params}'.format(
|
||||
schema=self.secure_protocol,
|
||||
webhook_id=self.pprint(self.webhook_id, privacy, safe=''),
|
||||
webhook_token=self.pprint(self.webhook_token, privacy, safe=''),
|
||||
args=NotifyDiscord.urlencode(args),
|
||||
params=NotifyDiscord.urlencode(params),
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def parse_url(url):
|
||||
"""
|
||||
Parses the URL and returns enough arguments that can allow
|
||||
us to substantiate this object.
|
||||
us to re-instantiate this object.
|
||||
|
||||
Syntax:
|
||||
discord://webhook_id/webhook_token
|
||||
|
||||
"""
|
||||
results = NotifyBase.parse_url(url)
|
||||
|
||||
results = NotifyBase.parse_url(url, verify_host=False)
|
||||
if not results:
|
||||
# We're done early as we couldn't load the results
|
||||
return results
|
||||
|
@ -459,43 +469,39 @@ class NotifyDiscord(NotifyBase):
|
|||
# Update Avatar Icon
|
||||
results['avatar'] = parse_bool(results['qsd'].get('avatar', True))
|
||||
|
||||
# Use Thumbnail
|
||||
if 'thumbnail' in results['qsd']:
|
||||
# Deprication Notice issued for v0.7.5
|
||||
NotifyDiscord.logger.deprecate(
|
||||
'The Discord URL contains the parameter '
|
||||
'"thumbnail=" which will be deprecated in an upcoming '
|
||||
'release. Please use "image=" instead.'
|
||||
)
|
||||
# Boolean to include an image or not
|
||||
results['include_image'] = parse_bool(results['qsd'].get(
|
||||
'image', NotifyDiscord.template_args['image']['default']))
|
||||
|
||||
# use image= for consistency with the other plugins but we also
|
||||
# support thumbnail= for backwards compatibility.
|
||||
results['include_image'] = \
|
||||
parse_bool(results['qsd'].get(
|
||||
'image', results['qsd'].get('thumbnail', False)))
|
||||
# Extract avatar url if it was specified
|
||||
if 'avatar_url' in results['qsd']:
|
||||
results['avatar_url'] = \
|
||||
NotifyDiscord.unquote(results['qsd']['avatar_url'])
|
||||
|
||||
return results
|
||||
|
||||
@staticmethod
|
||||
def parse_native_url(url):
|
||||
"""
|
||||
Support https://discordapp.com/api/webhooks/WEBHOOK_ID/WEBHOOK_TOKEN
|
||||
Support https://discord.com/api/webhooks/WEBHOOK_ID/WEBHOOK_TOKEN
|
||||
Support Legacy URL as well:
|
||||
https://discordapp.com/api/webhooks/WEBHOOK_ID/WEBHOOK_TOKEN
|
||||
"""
|
||||
|
||||
result = re.match(
|
||||
r'^https?://discordapp\.com/api/webhooks/'
|
||||
r'^https?://discord(app)?\.com/api/webhooks/'
|
||||
r'(?P<webhook_id>[0-9]+)/'
|
||||
r'(?P<webhook_token>[A-Z0-9_-]+)/?'
|
||||
r'(?P<args>\?.+)?$', url, re.I)
|
||||
r'(?P<params>\?.+)?$', url, re.I)
|
||||
|
||||
if result:
|
||||
return NotifyDiscord.parse_url(
|
||||
'{schema}://{webhook_id}/{webhook_token}/{args}'.format(
|
||||
'{schema}://{webhook_id}/{webhook_token}/{params}'.format(
|
||||
schema=NotifyDiscord.secure_protocol,
|
||||
webhook_id=result.group('webhook_id'),
|
||||
webhook_token=result.group('webhook_token'),
|
||||
args='' if not result.group('args')
|
||||
else result.group('args')))
|
||||
params='' if not result.group('params')
|
||||
else result.group('params')))
|
||||
|
||||
return None
|
||||
|
||||
|
|
|
@ -29,6 +29,9 @@ import smtplib
|
|||
from email.mime.text import MIMEText
|
||||
from email.mime.application import MIMEApplication
|
||||
from email.mime.multipart import MIMEMultipart
|
||||
from email.utils import formataddr
|
||||
from email.header import Header
|
||||
from email import charset
|
||||
|
||||
from socket import error as SocketError
|
||||
from datetime import datetime
|
||||
|
@ -38,10 +41,12 @@ from ..URLBase import PrivacyMode
|
|||
from ..common import NotifyFormat
|
||||
from ..common import NotifyType
|
||||
from ..utils import is_email
|
||||
from ..utils import parse_list
|
||||
from ..utils import GET_EMAIL_RE
|
||||
from ..utils import parse_emails
|
||||
from ..AppriseLocale import gettext_lazy as _
|
||||
|
||||
# Globally Default encoding mode set to Quoted Printable.
|
||||
charset.add_charset('utf-8', charset.QP, charset.QP, 'utf-8')
|
||||
|
||||
|
||||
class WebBaseLogin(object):
|
||||
"""
|
||||
|
@ -116,6 +121,21 @@ EMAIL_TEMPLATES = (
|
|||
},
|
||||
),
|
||||
|
||||
# Microsoft Office 365 (Email Server)
|
||||
# You must specify an authenticated sender address in the from= settings
|
||||
# and a valid email in the to= to deliver your emails to
|
||||
(
|
||||
'Microsoft Office 365',
|
||||
re.compile(
|
||||
r'^[^@]+@(?P<domain>(smtp\.)?office365\.com)$', re.I),
|
||||
{
|
||||
'port': 587,
|
||||
'smtp_host': 'smtp.office365.com',
|
||||
'secure': True,
|
||||
'secure_mode': SecureMailMode.STARTTLS,
|
||||
},
|
||||
),
|
||||
|
||||
# Yahoo Mail
|
||||
(
|
||||
'Yahoo Mail',
|
||||
|
@ -380,8 +400,8 @@ class NotifyEmail(NotifyBase):
|
|||
except (ValueError, TypeError):
|
||||
self.timeout = self.connect_timeout
|
||||
|
||||
# Acquire targets
|
||||
self.targets = parse_list(targets)
|
||||
# Acquire Email 'To'
|
||||
self.targets = list()
|
||||
|
||||
# Acquire Carbon Copies
|
||||
self.cc = set()
|
||||
|
@ -389,9 +409,11 @@ class NotifyEmail(NotifyBase):
|
|||
# Acquire Blind Carbon Copies
|
||||
self.bcc = set()
|
||||
|
||||
# For tracking our email -> name lookups
|
||||
self.names = {}
|
||||
|
||||
# Now we want to construct the To and From email
|
||||
# addresses from the URL provided
|
||||
self.from_name = from_name
|
||||
self.from_addr = from_addr
|
||||
|
||||
if self.user and not self.from_addr:
|
||||
|
@ -401,15 +423,18 @@ class NotifyEmail(NotifyBase):
|
|||
self.host,
|
||||
)
|
||||
|
||||
if not is_email(self.from_addr):
|
||||
result = is_email(self.from_addr)
|
||||
if not result:
|
||||
# Parse Source domain based on from_addr
|
||||
msg = 'Invalid ~From~ email specified: {}'.format(self.from_addr)
|
||||
self.logger.warning(msg)
|
||||
raise TypeError(msg)
|
||||
|
||||
# If our target email list is empty we want to add ourselves to it
|
||||
if len(self.targets) == 0:
|
||||
self.targets.append(self.from_addr)
|
||||
# Store our email address
|
||||
self.from_addr = result['full_email']
|
||||
|
||||
# Set our from name
|
||||
self.from_name = from_name if from_name else result['name']
|
||||
|
||||
# Now detect the SMTP Server
|
||||
self.smtp_host = \
|
||||
|
@ -425,11 +450,35 @@ class NotifyEmail(NotifyBase):
|
|||
self.logger.warning(msg)
|
||||
raise TypeError(msg)
|
||||
|
||||
# Validate recipients (cc:) and drop bad ones:
|
||||
for recipient in parse_list(cc):
|
||||
if targets:
|
||||
# Validate recipients (to:) and drop bad ones:
|
||||
for recipient in parse_emails(targets):
|
||||
result = is_email(recipient)
|
||||
if result:
|
||||
self.targets.append(
|
||||
(result['name'] if result['name'] else False,
|
||||
result['full_email']))
|
||||
continue
|
||||
|
||||
if GET_EMAIL_RE.match(recipient):
|
||||
self.cc.add(recipient)
|
||||
self.logger.warning(
|
||||
'Dropped invalid To email '
|
||||
'({}) specified.'.format(recipient),
|
||||
)
|
||||
|
||||
else:
|
||||
# If our target email list is empty we want to add ourselves to it
|
||||
self.targets.append(
|
||||
(self.from_name if self.from_name else False, self.from_addr))
|
||||
|
||||
# Validate recipients (cc:) and drop bad ones:
|
||||
for recipient in parse_emails(cc):
|
||||
email = is_email(recipient)
|
||||
if email:
|
||||
self.cc.add(email['full_email'])
|
||||
|
||||
# Index our name (if one exists)
|
||||
self.names[email['full_email']] = \
|
||||
email['name'] if email['name'] else False
|
||||
continue
|
||||
|
||||
self.logger.warning(
|
||||
|
@ -438,10 +487,14 @@ class NotifyEmail(NotifyBase):
|
|||
)
|
||||
|
||||
# Validate recipients (bcc:) and drop bad ones:
|
||||
for recipient in parse_list(bcc):
|
||||
for recipient in parse_emails(bcc):
|
||||
email = is_email(recipient)
|
||||
if email:
|
||||
self.bcc.add(email['full_email'])
|
||||
|
||||
if GET_EMAIL_RE.match(recipient):
|
||||
self.bcc.add(recipient)
|
||||
# Index our name (if one exists)
|
||||
self.names[email['full_email']] = \
|
||||
email['name'] if email['name'] else False
|
||||
continue
|
||||
|
||||
self.logger.warning(
|
||||
|
@ -529,36 +582,57 @@ class NotifyEmail(NotifyBase):
|
|||
Perform Email Notification
|
||||
"""
|
||||
|
||||
from_name = self.from_name
|
||||
if not from_name:
|
||||
from_name = self.app_desc
|
||||
# Initialize our default from name
|
||||
from_name = self.from_name if self.from_name else self.app_desc
|
||||
|
||||
# error tracking (used for function return)
|
||||
has_error = False
|
||||
|
||||
if not self.targets:
|
||||
# There is no one to email; we're done
|
||||
self.logger.warning(
|
||||
'There are no Email recipients to notify')
|
||||
return False
|
||||
|
||||
# Create a copy of the targets list
|
||||
emails = list(self.targets)
|
||||
while len(emails):
|
||||
# Get our email to notify
|
||||
to_addr = emails.pop(0)
|
||||
|
||||
if not is_email(to_addr):
|
||||
self.logger.warning(
|
||||
'Invalid ~To~ email specified: {}'.format(to_addr))
|
||||
has_error = True
|
||||
continue
|
||||
to_name, to_addr = emails.pop(0)
|
||||
|
||||
# Strip target out of cc list if in To or Bcc
|
||||
cc = (self.cc - self.bcc - set([to_addr]))
|
||||
|
||||
# Strip target out of bcc list if in To
|
||||
bcc = (self.bcc - set([to_addr]))
|
||||
|
||||
try:
|
||||
# Format our cc addresses to support the Name field
|
||||
cc = [formataddr(
|
||||
(self.names.get(addr, False), addr), charset='utf-8')
|
||||
for addr in cc]
|
||||
|
||||
# Format our bcc addresses to support the Name field
|
||||
bcc = [formataddr(
|
||||
(self.names.get(addr, False), addr), charset='utf-8')
|
||||
for addr in bcc]
|
||||
|
||||
except TypeError:
|
||||
# Python v2.x Support (no charset keyword)
|
||||
# Format our cc addresses to support the Name field
|
||||
cc = [formataddr(
|
||||
(self.names.get(addr, False), addr)) for addr in cc]
|
||||
|
||||
# Format our bcc addresses to support the Name field
|
||||
bcc = [formataddr(
|
||||
(self.names.get(addr, False), addr)) for addr in bcc]
|
||||
|
||||
self.logger.debug(
|
||||
'Email From: {} <{}>'.format(from_name, self.from_addr))
|
||||
self.logger.debug('Email To: {}'.format(to_addr))
|
||||
if len(cc):
|
||||
if cc:
|
||||
self.logger.debug('Email Cc: {}'.format(', '.join(cc)))
|
||||
if len(bcc):
|
||||
if bcc:
|
||||
self.logger.debug('Email Bcc: {}'.format(', '.join(bcc)))
|
||||
self.logger.debug('Login ID: {}'.format(self.user))
|
||||
self.logger.debug(
|
||||
|
@ -566,15 +640,25 @@ class NotifyEmail(NotifyBase):
|
|||
|
||||
# Prepare Email Message
|
||||
if self.notify_format == NotifyFormat.HTML:
|
||||
content = MIMEText(body, 'html')
|
||||
content = MIMEText(body, 'html', 'utf-8')
|
||||
|
||||
else:
|
||||
content = MIMEText(body, 'plain')
|
||||
content = MIMEText(body, 'plain', 'utf-8')
|
||||
|
||||
base = MIMEMultipart() if attach else content
|
||||
base['Subject'] = title
|
||||
base['From'] = '{} <{}>'.format(from_name, self.from_addr)
|
||||
base['To'] = to_addr
|
||||
base['Subject'] = Header(title, 'utf-8')
|
||||
try:
|
||||
base['From'] = formataddr(
|
||||
(from_name if from_name else False, self.from_addr),
|
||||
charset='utf-8')
|
||||
base['To'] = formataddr((to_name, to_addr), charset='utf-8')
|
||||
|
||||
except TypeError:
|
||||
# Python v2.x Support (no charset keyword)
|
||||
base['From'] = formataddr(
|
||||
(from_name if from_name else False, self.from_addr))
|
||||
base['To'] = formataddr((to_name, to_addr))
|
||||
|
||||
base['Cc'] = ','.join(cc)
|
||||
base['Date'] = \
|
||||
datetime.utcnow().strftime("%a, %d %b %Y %H:%M:%S +0000")
|
||||
|
@ -608,7 +692,8 @@ class NotifyEmail(NotifyBase):
|
|||
app.add_header(
|
||||
'Content-Disposition',
|
||||
'attachment; filename="{}"'.format(
|
||||
attachment.name))
|
||||
Header(attachment.name, 'utf-8')),
|
||||
)
|
||||
|
||||
base.attach(app)
|
||||
|
||||
|
@ -653,7 +738,7 @@ class NotifyEmail(NotifyBase):
|
|||
|
||||
except (SocketError, smtplib.SMTPException, RuntimeError) as e:
|
||||
self.logger.warning(
|
||||
'A Connection error occured sending Email '
|
||||
'A Connection error occurred sending Email '
|
||||
'notification to {}.'.format(self.smtp_host))
|
||||
self.logger.debug('Socket Exception: %s' % str(e))
|
||||
|
||||
|
@ -672,26 +757,34 @@ class NotifyEmail(NotifyBase):
|
|||
Returns the URL built dynamically based on specified arguments.
|
||||
"""
|
||||
|
||||
# Define any arguments set
|
||||
args = {
|
||||
'format': self.notify_format,
|
||||
'overflow': self.overflow_mode,
|
||||
# Define an URL parameters
|
||||
params = {
|
||||
'from': self.from_addr,
|
||||
'name': self.from_name,
|
||||
'mode': self.secure_mode,
|
||||
'smtp': self.smtp_host,
|
||||
'timeout': self.timeout,
|
||||
'user': self.user,
|
||||
'verify': 'yes' if self.verify_certificate else 'no',
|
||||
}
|
||||
|
||||
# Extend our parameters
|
||||
params.update(self.url_parameters(privacy=privacy, *args, **kwargs))
|
||||
|
||||
if self.from_name:
|
||||
params['name'] = self.from_name
|
||||
|
||||
if len(self.cc) > 0:
|
||||
# Handle our Carbon Copy Addresses
|
||||
args['cc'] = ','.join(self.cc)
|
||||
params['cc'] = ','.join(
|
||||
['{}{}'.format(
|
||||
'' if not e not in self.names
|
||||
else '{}:'.format(self.names[e]), e) for e in self.cc])
|
||||
|
||||
if len(self.bcc) > 0:
|
||||
# Handle our Blind Carbon Copy Addresses
|
||||
args['bcc'] = ','.join(self.bcc)
|
||||
params['bcc'] = ','.join(
|
||||
['{}{}'.format(
|
||||
'' if not e not in self.names
|
||||
else '{}:'.format(self.names[e]), e) for e in self.bcc])
|
||||
|
||||
# pull email suffix from username (if present)
|
||||
user = None if not self.user else self.user.split('@')[0]
|
||||
|
@ -717,28 +810,31 @@ class NotifyEmail(NotifyBase):
|
|||
# a simple boolean check as to whether we display our target emails
|
||||
# or not
|
||||
has_targets = \
|
||||
not (len(self.targets) == 1 and self.targets[0] == self.from_addr)
|
||||
not (len(self.targets) == 1
|
||||
and self.targets[0][1] == self.from_addr)
|
||||
|
||||
return '{schema}://{auth}{hostname}{port}/{targets}?{args}'.format(
|
||||
return '{schema}://{auth}{hostname}{port}/{targets}?{params}'.format(
|
||||
schema=self.secure_protocol if self.secure else self.protocol,
|
||||
auth=auth,
|
||||
hostname=NotifyEmail.quote(self.host, safe=''),
|
||||
# never encode hostname since we're expecting it to be a valid one
|
||||
hostname=self.host,
|
||||
port='' if self.port is None or self.port == default_port
|
||||
else ':{}'.format(self.port),
|
||||
targets='' if not has_targets else '/'.join(
|
||||
[NotifyEmail.quote(x, safe='') for x in self.targets]),
|
||||
args=NotifyEmail.urlencode(args),
|
||||
[NotifyEmail.quote('{}{}'.format(
|
||||
'' if not e[0] else '{}:'.format(e[0]), e[1]),
|
||||
safe='') for e in self.targets]),
|
||||
params=NotifyEmail.urlencode(params),
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def parse_url(url):
|
||||
"""
|
||||
Parses the URL and returns enough arguments that can allow
|
||||
us to substantiate this object.
|
||||
us to re-instantiate this object.
|
||||
|
||||
"""
|
||||
results = NotifyBase.parse_url(url)
|
||||
|
||||
if not results:
|
||||
# We're done early as we couldn't load the results
|
||||
return results
|
||||
|
@ -761,8 +857,7 @@ class NotifyEmail(NotifyBase):
|
|||
|
||||
# Attempt to detect 'to' email address
|
||||
if 'to' in results['qsd'] and len(results['qsd']['to']):
|
||||
results['targets'] += \
|
||||
NotifyEmail.parse_list(results['qsd']['to'])
|
||||
results['targets'].append(results['qsd']['to'])
|
||||
|
||||
if 'name' in results['qsd'] and len(results['qsd']['name']):
|
||||
# Extract from name to associate with from address
|
||||
|
@ -783,13 +878,11 @@ class NotifyEmail(NotifyBase):
|
|||
|
||||
# Handle Carbon Copy Addresses
|
||||
if 'cc' in results['qsd'] and len(results['qsd']['cc']):
|
||||
results['cc'] = \
|
||||
NotifyEmail.parse_list(results['qsd']['cc'])
|
||||
results['cc'] = results['qsd']['cc']
|
||||
|
||||
# Handle Blind Carbon Copy Addresses
|
||||
if 'bcc' in results['qsd'] and len(results['qsd']['bcc']):
|
||||
results['bcc'] = \
|
||||
NotifyEmail.parse_list(results['qsd']['bcc'])
|
||||
results['bcc'] = results['qsd']['bcc']
|
||||
|
||||
results['from_addr'] = from_addr
|
||||
results['smtp_host'] = smtp_host
|
||||
|
|
|
@ -61,9 +61,6 @@ class NotifyEmby(NotifyBase):
|
|||
# A URL that takes you to the setup/help of the specific protocol
|
||||
setup_url = 'https://github.com/caronc/apprise/wiki/Notify_emby'
|
||||
|
||||
# Emby uses the http protocol with JSON requests
|
||||
emby_default_port = 8096
|
||||
|
||||
# By default Emby requires you to provide it a device id
|
||||
# The following was just a random uuid4 generated one. There
|
||||
# is no real reason to change this, but hey; that's what open
|
||||
|
@ -94,6 +91,7 @@ class NotifyEmby(NotifyBase):
|
|||
'type': 'int',
|
||||
'min': 1,
|
||||
'max': 65535,
|
||||
'default': 8096
|
||||
},
|
||||
'user': {
|
||||
'name': _('Username'),
|
||||
|
@ -137,6 +135,10 @@ class NotifyEmby(NotifyBase):
|
|||
# or a modal type box (requires an Okay acknowledgement)
|
||||
self.modal = modal
|
||||
|
||||
if not self.port:
|
||||
# Assign default port if one isn't otherwise specified:
|
||||
self.port = self.template_tokens['port']['default']
|
||||
|
||||
if not self.user:
|
||||
# User was not specified
|
||||
msg = 'No Emby username was specified.'
|
||||
|
@ -207,6 +209,7 @@ class NotifyEmby(NotifyBase):
|
|||
headers=headers,
|
||||
data=dumps(payload),
|
||||
verify=self.verify_certificate,
|
||||
timeout=self.request_timeout,
|
||||
)
|
||||
|
||||
if r.status_code != requests.codes.ok:
|
||||
|
@ -229,7 +232,7 @@ class NotifyEmby(NotifyBase):
|
|||
|
||||
except requests.RequestException as e:
|
||||
self.logger.warning(
|
||||
'A Connection error occured authenticating a user with Emby '
|
||||
'A Connection error occurred authenticating a user with Emby '
|
||||
'at %s.' % self.host)
|
||||
self.logger.debug('Socket Exception: %s' % str(e))
|
||||
|
||||
|
@ -370,6 +373,7 @@ class NotifyEmby(NotifyBase):
|
|||
url,
|
||||
headers=headers,
|
||||
verify=self.verify_certificate,
|
||||
timeout=self.request_timeout,
|
||||
)
|
||||
|
||||
if r.status_code != requests.codes.ok:
|
||||
|
@ -392,7 +396,7 @@ class NotifyEmby(NotifyBase):
|
|||
|
||||
except requests.RequestException as e:
|
||||
self.logger.warning(
|
||||
'A Connection error occured querying Emby '
|
||||
'A Connection error occurred querying Emby '
|
||||
'for session information at %s.' % self.host)
|
||||
self.logger.debug('Socket Exception: %s' % str(e))
|
||||
|
||||
|
@ -449,6 +453,7 @@ class NotifyEmby(NotifyBase):
|
|||
url,
|
||||
headers=headers,
|
||||
verify=self.verify_certificate,
|
||||
timeout=self.request_timeout,
|
||||
)
|
||||
|
||||
if r.status_code not in (
|
||||
|
@ -477,7 +482,7 @@ class NotifyEmby(NotifyBase):
|
|||
|
||||
except requests.RequestException as e:
|
||||
self.logger.warning(
|
||||
'A Connection error occured querying Emby '
|
||||
'A Connection error occurred querying Emby '
|
||||
'to logoff user %s at %s.' % (self.user, self.host))
|
||||
self.logger.debug('Socket Exception: %s' % str(e))
|
||||
|
||||
|
@ -550,6 +555,7 @@ class NotifyEmby(NotifyBase):
|
|||
data=dumps(payload),
|
||||
headers=headers,
|
||||
verify=self.verify_certificate,
|
||||
timeout=self.request_timeout,
|
||||
)
|
||||
if r.status_code not in (
|
||||
requests.codes.ok,
|
||||
|
@ -577,7 +583,7 @@ class NotifyEmby(NotifyBase):
|
|||
|
||||
except requests.RequestException as e:
|
||||
self.logger.warning(
|
||||
'A Connection error occured sending Emby '
|
||||
'A Connection error occurred sending Emby '
|
||||
'notification to %s.' % self.host)
|
||||
self.logger.debug('Socket Exception: %s' % str(e))
|
||||
|
||||
|
@ -592,14 +598,14 @@ class NotifyEmby(NotifyBase):
|
|||
Returns the URL built dynamically based on specified arguments.
|
||||
"""
|
||||
|
||||
# Define any arguments set
|
||||
args = {
|
||||
'format': self.notify_format,
|
||||
'overflow': self.overflow_mode,
|
||||
# Define any URL parameters
|
||||
params = {
|
||||
'modal': 'yes' if self.modal else 'no',
|
||||
'verify': 'yes' if self.verify_certificate else 'no',
|
||||
}
|
||||
|
||||
# Extend our parameters
|
||||
params.update(self.url_parameters(privacy=privacy, *args, **kwargs))
|
||||
|
||||
# Determine Authentication
|
||||
auth = ''
|
||||
if self.user and self.password:
|
||||
|
@ -613,13 +619,14 @@ class NotifyEmby(NotifyBase):
|
|||
user=NotifyEmby.quote(self.user, safe=''),
|
||||
)
|
||||
|
||||
return '{schema}://{auth}{hostname}{port}/?{args}'.format(
|
||||
return '{schema}://{auth}{hostname}{port}/?{params}'.format(
|
||||
schema=self.secure_protocol if self.secure else self.protocol,
|
||||
auth=auth,
|
||||
hostname=NotifyEmby.quote(self.host, safe=''),
|
||||
port='' if self.port is None or self.port == self.emby_default_port
|
||||
hostname=self.host,
|
||||
port='' if self.port is None
|
||||
or self.port == self.template_tokens['port']['default']
|
||||
else ':{}'.format(self.port),
|
||||
args=NotifyEmby.urlencode(args),
|
||||
params=NotifyEmby.urlencode(params),
|
||||
)
|
||||
|
||||
@property
|
||||
|
@ -655,7 +662,7 @@ class NotifyEmby(NotifyBase):
|
|||
def parse_url(url):
|
||||
"""
|
||||
Parses the URL and returns enough arguments that can allow
|
||||
us to substantiate this object.
|
||||
us to re-instantiate this object.
|
||||
|
||||
"""
|
||||
results = NotifyBase.parse_url(url)
|
||||
|
@ -663,10 +670,6 @@ class NotifyEmby(NotifyBase):
|
|||
# We're done early
|
||||
return results
|
||||
|
||||
# Assign Default Emby Port
|
||||
if not results['port']:
|
||||
results['port'] = NotifyEmby.emby_default_port
|
||||
|
||||
# Modal type popup (default False)
|
||||
results['modal'] = parse_bool(results['qsd'].get('modal', False))
|
||||
|
||||
|
@ -679,7 +682,7 @@ class NotifyEmby(NotifyBase):
|
|||
try:
|
||||
self.logout()
|
||||
|
||||
except LookupError:
|
||||
except LookupError: # pragma: no cover
|
||||
# Python v3.5 call to requests can sometimes throw the exception
|
||||
# "/usr/lib64/python3.7/socket.py", line 748, in getaddrinfo
|
||||
# LookupError: unknown encoding: idna
|
||||
|
|
|
@ -184,16 +184,16 @@ class NotifyEnigma2(NotifyBase):
|
|||
Returns the URL built dynamically based on specified arguments.
|
||||
"""
|
||||
|
||||
# Define any arguments set
|
||||
args = {
|
||||
'format': self.notify_format,
|
||||
'overflow': self.overflow_mode,
|
||||
'verify': 'yes' if self.verify_certificate else 'no',
|
||||
# Define any URL parameters
|
||||
params = {
|
||||
'timeout': str(self.timeout),
|
||||
}
|
||||
|
||||
# Append our headers into our args
|
||||
args.update({'+{}'.format(k): v for k, v in self.headers.items()})
|
||||
# Append our headers into our parameters
|
||||
params.update({'+{}'.format(k): v for k, v in self.headers.items()})
|
||||
|
||||
# Extend our parameters
|
||||
params.update(self.url_parameters(privacy=privacy, *args, **kwargs))
|
||||
|
||||
# Determine Authentication
|
||||
auth = ''
|
||||
|
@ -210,14 +210,15 @@ class NotifyEnigma2(NotifyBase):
|
|||
|
||||
default_port = 443 if self.secure else 80
|
||||
|
||||
return '{schema}://{auth}{hostname}{port}{fullpath}?{args}'.format(
|
||||
return '{schema}://{auth}{hostname}{port}{fullpath}?{params}'.format(
|
||||
schema=self.secure_protocol if self.secure else self.protocol,
|
||||
auth=auth,
|
||||
hostname=NotifyEnigma2.quote(self.host, safe=''),
|
||||
# never encode hostname since we're expecting it to be a valid one
|
||||
hostname=self.host,
|
||||
port='' if self.port is None or self.port == default_port
|
||||
else ':{}'.format(self.port),
|
||||
fullpath=NotifyEnigma2.quote(self.fullpath, safe='/'),
|
||||
args=NotifyEnigma2.urlencode(args),
|
||||
params=NotifyEnigma2.urlencode(params),
|
||||
)
|
||||
|
||||
def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs):
|
||||
|
@ -269,6 +270,7 @@ class NotifyEnigma2(NotifyBase):
|
|||
headers=headers,
|
||||
auth=auth,
|
||||
verify=self.verify_certificate,
|
||||
timeout=self.request_timeout,
|
||||
)
|
||||
|
||||
if r.status_code != requests.codes.ok:
|
||||
|
@ -313,7 +315,7 @@ class NotifyEnigma2(NotifyBase):
|
|||
|
||||
except requests.RequestException as e:
|
||||
self.logger.warning(
|
||||
'A Connection error occured sending Enigma2 '
|
||||
'A Connection error occurred sending Enigma2 '
|
||||
'notification to %s.' % self.host)
|
||||
self.logger.debug('Socket Exception: %s' % str(e))
|
||||
|
||||
|
@ -326,11 +328,10 @@ class NotifyEnigma2(NotifyBase):
|
|||
def parse_url(url):
|
||||
"""
|
||||
Parses the URL and returns enough arguments that can allow
|
||||
us to substantiate this object.
|
||||
us to re-instantiate this object.
|
||||
|
||||
"""
|
||||
results = NotifyBase.parse_url(url)
|
||||
|
||||
if not results:
|
||||
# We're done early as we couldn't load the results
|
||||
return results
|
||||
|
|
|
@ -29,6 +29,7 @@ from ..common import NotifyImageSize
|
|||
from ..common import NotifyType
|
||||
from ..utils import parse_bool
|
||||
from ..AppriseLocale import gettext_lazy as _
|
||||
from ..utils import validate_regex
|
||||
|
||||
|
||||
class NotifyFaast(NotifyBase):
|
||||
|
@ -86,7 +87,12 @@ class NotifyFaast(NotifyBase):
|
|||
super(NotifyFaast, self).__init__(**kwargs)
|
||||
|
||||
# Store the Authentication Token
|
||||
self.authtoken = authtoken
|
||||
self.authtoken = validate_regex(authtoken)
|
||||
if not self.authtoken:
|
||||
msg = 'An invalid Faast Authentication Token ' \
|
||||
'({}) was specified.'.format(authtoken)
|
||||
self.logger.warning(msg)
|
||||
raise TypeError(msg)
|
||||
|
||||
# Associate an image with our post
|
||||
self.include_image = include_image
|
||||
|
@ -131,6 +137,7 @@ class NotifyFaast(NotifyBase):
|
|||
data=payload,
|
||||
headers=headers,
|
||||
verify=self.verify_certificate,
|
||||
timeout=self.request_timeout,
|
||||
)
|
||||
if r.status_code != requests.codes.ok:
|
||||
# We had a problem
|
||||
|
@ -154,7 +161,7 @@ class NotifyFaast(NotifyBase):
|
|||
|
||||
except requests.RequestException as e:
|
||||
self.logger.warning(
|
||||
'A Connection error occured sending Faast notification.',
|
||||
'A Connection error occurred sending Faast notification.',
|
||||
)
|
||||
self.logger.debug('Socket Exception: %s' % str(e))
|
||||
|
||||
|
@ -168,29 +175,28 @@ class NotifyFaast(NotifyBase):
|
|||
Returns the URL built dynamically based on specified arguments.
|
||||
"""
|
||||
|
||||
# Define any arguments set
|
||||
args = {
|
||||
'format': self.notify_format,
|
||||
'overflow': self.overflow_mode,
|
||||
# Define any URL parameters
|
||||
params = {
|
||||
'image': 'yes' if self.include_image else 'no',
|
||||
'verify': 'yes' if self.verify_certificate else 'no',
|
||||
}
|
||||
|
||||
return '{schema}://{authtoken}/?{args}'.format(
|
||||
# Extend our parameters
|
||||
params.update(self.url_parameters(privacy=privacy, *args, **kwargs))
|
||||
|
||||
return '{schema}://{authtoken}/?{params}'.format(
|
||||
schema=self.protocol,
|
||||
authtoken=self.pprint(self.authtoken, privacy, safe=''),
|
||||
args=NotifyFaast.urlencode(args),
|
||||
params=NotifyFaast.urlencode(params),
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def parse_url(url):
|
||||
"""
|
||||
Parses the URL and returns enough arguments that can allow
|
||||
us to substantiate this object.
|
||||
us to re-instantiate this object.
|
||||
|
||||
"""
|
||||
results = NotifyBase.parse_url(url)
|
||||
|
||||
results = NotifyBase.parse_url(url, verify_host=False)
|
||||
if not results:
|
||||
# We're done early as we couldn't load the results
|
||||
return results
|
||||
|
|
|
@ -100,7 +100,7 @@ class NotifyFlock(NotifyBase):
|
|||
'token': {
|
||||
'name': _('Access Key'),
|
||||
'type': 'string',
|
||||
'regex': (r'^[a-z0-9-]{24}$', 'i'),
|
||||
'regex': (r'^[a-z0-9-]+$', 'i'),
|
||||
'private': True,
|
||||
'required': True,
|
||||
},
|
||||
|
@ -112,14 +112,14 @@ class NotifyFlock(NotifyBase):
|
|||
'name': _('To User ID'),
|
||||
'type': 'string',
|
||||
'prefix': '@',
|
||||
'regex': (r'^[A-Z0-9_]{12}$', 'i'),
|
||||
'regex': (r'^[A-Z0-9_]+$', 'i'),
|
||||
'map_to': 'targets',
|
||||
},
|
||||
'to_channel': {
|
||||
'name': _('To Channel ID'),
|
||||
'type': 'string',
|
||||
'prefix': '#',
|
||||
'regex': (r'^[A-Z0-9_]{12}$', 'i'),
|
||||
'regex': (r'^[A-Z0-9_]+$', 'i'),
|
||||
'map_to': 'targets',
|
||||
},
|
||||
'targets': {
|
||||
|
@ -269,6 +269,7 @@ class NotifyFlock(NotifyBase):
|
|||
data=dumps(payload),
|
||||
headers=headers,
|
||||
verify=self.verify_certificate,
|
||||
timeout=self.request_timeout,
|
||||
)
|
||||
if r.status_code != requests.codes.ok:
|
||||
# We had a problem
|
||||
|
@ -294,7 +295,7 @@ class NotifyFlock(NotifyBase):
|
|||
|
||||
except requests.RequestException as e:
|
||||
self.logger.warning(
|
||||
'A Connection error occured sending Flock notification.'
|
||||
'A Connection error occurred sending Flock notification.'
|
||||
)
|
||||
self.logger.debug('Socket Exception: %s' % str(e))
|
||||
|
||||
|
@ -308,31 +309,31 @@ class NotifyFlock(NotifyBase):
|
|||
Returns the URL built dynamically based on specified arguments.
|
||||
"""
|
||||
|
||||
args = {
|
||||
'format': self.notify_format,
|
||||
'overflow': self.overflow_mode,
|
||||
# Define any URL parameters
|
||||
params = {
|
||||
'image': 'yes' if self.include_image else 'no',
|
||||
'verify': 'yes' if self.verify_certificate else 'no',
|
||||
}
|
||||
|
||||
return '{schema}://{token}/{targets}?{args}'\
|
||||
# Extend our parameters
|
||||
params.update(self.url_parameters(privacy=privacy, *args, **kwargs))
|
||||
|
||||
return '{schema}://{token}/{targets}?{params}'\
|
||||
.format(
|
||||
schema=self.secure_protocol,
|
||||
token=self.pprint(self.token, privacy, safe=''),
|
||||
targets='/'.join(
|
||||
[NotifyFlock.quote(target, safe='')
|
||||
for target in self.targets]),
|
||||
args=NotifyFlock.urlencode(args),
|
||||
params=NotifyFlock.urlencode(params),
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def parse_url(url):
|
||||
"""
|
||||
Parses the URL and returns enough arguments that can allow
|
||||
us to substantiate this object.
|
||||
us to re-instantiate this object.
|
||||
"""
|
||||
results = NotifyBase.parse_url(url)
|
||||
|
||||
results = NotifyBase.parse_url(url, verify_host=False)
|
||||
if not results:
|
||||
# We're done early as we couldn't load the results
|
||||
return results
|
||||
|
@ -363,14 +364,14 @@ class NotifyFlock(NotifyBase):
|
|||
result = re.match(
|
||||
r'^https?://api\.flock\.com/hooks/sendMessage/'
|
||||
r'(?P<token>[a-z0-9-]{24})/?'
|
||||
r'(?P<args>\?.+)?$', url, re.I)
|
||||
r'(?P<params>\?.+)?$', url, re.I)
|
||||
|
||||
if result:
|
||||
return NotifyFlock.parse_url(
|
||||
'{schema}://{token}/{args}'.format(
|
||||
'{schema}://{token}/{params}'.format(
|
||||
schema=NotifyFlock.secure_protocol,
|
||||
token=result.group('token'),
|
||||
args='' if not result.group('args')
|
||||
else result.group('args')))
|
||||
params='' if not result.group('params')
|
||||
else result.group('params')))
|
||||
|
||||
return None
|
||||
|
|
|
@ -309,6 +309,7 @@ class NotifyGitter(NotifyBase):
|
|||
data=payload,
|
||||
headers=headers,
|
||||
verify=self.verify_certificate,
|
||||
timeout=self.request_timeout,
|
||||
)
|
||||
|
||||
if r.status_code != requests.codes.ok:
|
||||
|
@ -366,30 +367,29 @@ class NotifyGitter(NotifyBase):
|
|||
Returns the URL built dynamically based on specified arguments.
|
||||
"""
|
||||
|
||||
# Define any arguments set
|
||||
args = {
|
||||
'format': self.notify_format,
|
||||
'overflow': self.overflow_mode,
|
||||
# Define any URL parameters
|
||||
params = {
|
||||
'image': 'yes' if self.include_image else 'no',
|
||||
'verify': 'yes' if self.verify_certificate else 'no',
|
||||
}
|
||||
|
||||
return '{schema}://{token}/{targets}/?{args}'.format(
|
||||
# Extend our parameters
|
||||
params.update(self.url_parameters(privacy=privacy, *args, **kwargs))
|
||||
|
||||
return '{schema}://{token}/{targets}/?{params}'.format(
|
||||
schema=self.secure_protocol,
|
||||
token=self.pprint(self.token, privacy, safe=''),
|
||||
targets='/'.join(
|
||||
[NotifyGitter.quote(x, safe='') for x in self.targets]),
|
||||
args=NotifyGitter.urlencode(args))
|
||||
params=NotifyGitter.urlencode(params))
|
||||
|
||||
@staticmethod
|
||||
def parse_url(url):
|
||||
"""
|
||||
Parses the URL and returns enough arguments that can allow
|
||||
us to substantiate this object.
|
||||
us to re-instantiate this object.
|
||||
|
||||
"""
|
||||
results = NotifyBase.parse_url(url)
|
||||
|
||||
results = NotifyBase.parse_url(url, verify_host=False)
|
||||
if not results:
|
||||
# We're done early as we couldn't load the results
|
||||
return results
|
||||
|
|
|
@ -113,7 +113,7 @@ class NotifyGnome(NotifyBase):
|
|||
|
||||
# Define object templates
|
||||
templates = (
|
||||
'{schema}://_/',
|
||||
'{schema}://',
|
||||
)
|
||||
|
||||
# Define our template arguments
|
||||
|
@ -141,7 +141,7 @@ class NotifyGnome(NotifyBase):
|
|||
|
||||
# The urgency of the message
|
||||
if urgency not in GNOME_URGENCIES:
|
||||
self.urgency = GnomeUrgency.NORMAL
|
||||
self.urgency = self.template_args['urgency']['default']
|
||||
|
||||
else:
|
||||
self.urgency = urgency
|
||||
|
@ -214,19 +214,19 @@ class NotifyGnome(NotifyBase):
|
|||
GnomeUrgency.HIGH: 'high',
|
||||
}
|
||||
|
||||
# Define any arguments set
|
||||
args = {
|
||||
'format': self.notify_format,
|
||||
'overflow': self.overflow_mode,
|
||||
# Define any URL parameters
|
||||
params = {
|
||||
'image': 'yes' if self.include_image else 'no',
|
||||
'urgency': 'normal' if self.urgency not in _map
|
||||
else _map[self.urgency],
|
||||
'verify': 'yes' if self.verify_certificate else 'no',
|
||||
}
|
||||
|
||||
return '{schema}://_/?{args}'.format(
|
||||
# Extend our parameters
|
||||
params.update(self.url_parameters(privacy=privacy, *args, **kwargs))
|
||||
|
||||
return '{schema}://?{params}'.format(
|
||||
schema=self.protocol,
|
||||
args=NotifyGnome.urlencode(args),
|
||||
params=NotifyGnome.urlencode(params),
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
|
@ -238,19 +238,7 @@ class NotifyGnome(NotifyBase):
|
|||
|
||||
"""
|
||||
|
||||
results = NotifyBase.parse_url(url)
|
||||
if not results:
|
||||
results = {
|
||||
'schema': NotifyGnome.protocol,
|
||||
'user': None,
|
||||
'password': None,
|
||||
'port': None,
|
||||
'host': '_',
|
||||
'fullpath': None,
|
||||
'path': None,
|
||||
'url': url,
|
||||
'qsd': {},
|
||||
}
|
||||
results = NotifyBase.parse_url(url, verify_host=False)
|
||||
|
||||
# Include images with our message
|
||||
results['include_image'] = \
|
||||
|
|
|
@ -77,10 +77,15 @@ class NotifyGotify(NotifyBase):
|
|||
# A URL that takes you to the setup/help of the specific protocol
|
||||
setup_url = 'https://github.com/caronc/apprise/wiki/Notify_gotify'
|
||||
|
||||
# Disable throttle rate
|
||||
request_rate_per_sec = 0
|
||||
|
||||
# Define object templates
|
||||
templates = (
|
||||
'{schema}://{host}/{token}',
|
||||
'{schema}://{host}:{port}/{token}',
|
||||
'{schema}://{host}{path}{token}',
|
||||
'{schema}://{host}:{port}{path}{token}',
|
||||
)
|
||||
|
||||
# Define our template tokens
|
||||
|
@ -96,6 +101,13 @@ class NotifyGotify(NotifyBase):
|
|||
'type': 'string',
|
||||
'required': True,
|
||||
},
|
||||
'path': {
|
||||
'name': _('Path'),
|
||||
'type': 'string',
|
||||
'map_to': 'fullpath',
|
||||
'default': '/',
|
||||
'required': True,
|
||||
},
|
||||
'port': {
|
||||
'name': _('Port'),
|
||||
'type': 'int',
|
||||
|
@ -129,6 +141,9 @@ class NotifyGotify(NotifyBase):
|
|||
self.logger.warning(msg)
|
||||
raise TypeError(msg)
|
||||
|
||||
# prepare our fullpath
|
||||
self.fullpath = kwargs.get('fullpath', '/')
|
||||
|
||||
if priority not in GOTIFY_PRIORITIES:
|
||||
self.priority = GotifyPriority.NORMAL
|
||||
|
||||
|
@ -153,7 +168,7 @@ class NotifyGotify(NotifyBase):
|
|||
url += ':%d' % self.port
|
||||
|
||||
# Append our remaining path
|
||||
url += '/message'
|
||||
url += '{fullpath}message'.format(fullpath=self.fullpath)
|
||||
|
||||
# Define our parameteers
|
||||
params = {
|
||||
|
@ -188,6 +203,7 @@ class NotifyGotify(NotifyBase):
|
|||
data=dumps(payload),
|
||||
headers=headers,
|
||||
verify=self.verify_certificate,
|
||||
timeout=self.request_timeout,
|
||||
)
|
||||
if r.status_code != requests.codes.ok:
|
||||
# We had a problem
|
||||
|
@ -212,7 +228,7 @@ class NotifyGotify(NotifyBase):
|
|||
|
||||
except requests.RequestException as e:
|
||||
self.logger.warning(
|
||||
'A Connection error occured sending Gotify '
|
||||
'A Connection error occurred sending Gotify '
|
||||
'notification to %s.' % self.host)
|
||||
self.logger.debug('Socket Exception: %s' % str(e))
|
||||
|
||||
|
@ -226,30 +242,33 @@ class NotifyGotify(NotifyBase):
|
|||
Returns the URL built dynamically based on specified arguments.
|
||||
"""
|
||||
|
||||
# Define any arguments set
|
||||
args = {
|
||||
'format': self.notify_format,
|
||||
'overflow': self.overflow_mode,
|
||||
# Define any URL parameters
|
||||
params = {
|
||||
'priority': self.priority,
|
||||
'verify': 'yes' if self.verify_certificate else 'no',
|
||||
}
|
||||
|
||||
# Extend our parameters
|
||||
params.update(self.url_parameters(privacy=privacy, *args, **kwargs))
|
||||
|
||||
# Our default port
|
||||
default_port = 443 if self.secure else 80
|
||||
|
||||
return '{schema}://{hostname}{port}/{token}/?{args}'.format(
|
||||
return '{schema}://{hostname}{port}{fullpath}{token}/?{params}'.format(
|
||||
schema=self.secure_protocol if self.secure else self.protocol,
|
||||
hostname=NotifyGotify.quote(self.host, safe=''),
|
||||
# never encode hostname since we're expecting it to be a valid one
|
||||
hostname=self.host,
|
||||
port='' if self.port is None or self.port == default_port
|
||||
else ':{}'.format(self.port),
|
||||
fullpath=NotifyGotify.quote(self.fullpath, safe='/'),
|
||||
token=self.pprint(self.token, privacy, safe=''),
|
||||
args=NotifyGotify.urlencode(args),
|
||||
params=NotifyGotify.urlencode(params),
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def parse_url(url):
|
||||
"""
|
||||
Parses the URL and returns enough arguments that can allow
|
||||
us to substantiate this object.
|
||||
us to re-instantiate this object.
|
||||
|
||||
"""
|
||||
results = NotifyBase.parse_url(url)
|
||||
|
@ -262,13 +281,17 @@ class NotifyGotify(NotifyBase):
|
|||
|
||||
# optionally find the provider key
|
||||
try:
|
||||
# The first entry is our token
|
||||
results['token'] = entries.pop(0)
|
||||
# The last entry is our token
|
||||
results['token'] = entries.pop()
|
||||
|
||||
except IndexError:
|
||||
# No token was set
|
||||
results['token'] = None
|
||||
|
||||
# Re-assemble our full path
|
||||
results['fullpath'] = \
|
||||
'/' if not entries else '/{}/'.format('/'.join(entries))
|
||||
|
||||
if 'priority' in results['qsd'] and len(results['qsd']['priority']):
|
||||
_map = {
|
||||
'l': GotifyPriority.LOW,
|
||||
|
|
|
@ -1,374 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2019 Chris Caron <lead2gold@gmail.com>
|
||||
# All rights reserved.
|
||||
#
|
||||
# This code is licensed under the MIT License.
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files(the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions :
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
# THE SOFTWARE.
|
||||
|
||||
from .gntp import notifier
|
||||
from .gntp import errors
|
||||
from ..NotifyBase import NotifyBase
|
||||
from ...URLBase import PrivacyMode
|
||||
from ...common import NotifyImageSize
|
||||
from ...common import NotifyType
|
||||
from ...utils import parse_bool
|
||||
from ...AppriseLocale import gettext_lazy as _
|
||||
|
||||
|
||||
# Priorities
|
||||
class GrowlPriority(object):
|
||||
LOW = -2
|
||||
MODERATE = -1
|
||||
NORMAL = 0
|
||||
HIGH = 1
|
||||
EMERGENCY = 2
|
||||
|
||||
|
||||
GROWL_PRIORITIES = (
|
||||
GrowlPriority.LOW,
|
||||
GrowlPriority.MODERATE,
|
||||
GrowlPriority.NORMAL,
|
||||
GrowlPriority.HIGH,
|
||||
GrowlPriority.EMERGENCY,
|
||||
)
|
||||
|
||||
GROWL_NOTIFICATION_TYPE = "New Messages"
|
||||
|
||||
|
||||
class NotifyGrowl(NotifyBase):
|
||||
"""
|
||||
A wrapper to Growl Notifications
|
||||
|
||||
"""
|
||||
|
||||
# The default descriptive name associated with the Notification
|
||||
service_name = 'Growl'
|
||||
|
||||
# The services URL
|
||||
service_url = 'http://growl.info/'
|
||||
|
||||
# The default protocol
|
||||
protocol = 'growl'
|
||||
|
||||
# A URL that takes you to the setup/help of the specific protocol
|
||||
setup_url = 'https://github.com/caronc/apprise/wiki/Notify_growl'
|
||||
|
||||
# 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
|
||||
|
||||
# Define object templates
|
||||
# Define object templates
|
||||
templates = (
|
||||
'{schema}://{host}',
|
||||
'{schema}://{host}:{port}',
|
||||
'{schema}://{password}@{host}',
|
||||
'{schema}://{password}@{host}:{port}',
|
||||
)
|
||||
|
||||
# Define our template tokens
|
||||
template_tokens = dict(NotifyBase.template_tokens, **{
|
||||
'host': {
|
||||
'name': _('Hostname'),
|
||||
'type': 'string',
|
||||
'required': True,
|
||||
},
|
||||
'port': {
|
||||
'name': _('Port'),
|
||||
'type': 'int',
|
||||
'min': 1,
|
||||
'max': 65535,
|
||||
},
|
||||
'password': {
|
||||
'name': _('Password'),
|
||||
'type': 'string',
|
||||
'private': True,
|
||||
},
|
||||
})
|
||||
|
||||
# Define our template arguments
|
||||
template_args = dict(NotifyBase.template_args, **{
|
||||
'priority': {
|
||||
'name': _('Priority'),
|
||||
'type': 'choice:int',
|
||||
'values': GROWL_PRIORITIES,
|
||||
'default': GrowlPriority.NORMAL,
|
||||
},
|
||||
'version': {
|
||||
'name': _('Version'),
|
||||
'type': 'choice:int',
|
||||
'values': (1, 2),
|
||||
'default': 2,
|
||||
},
|
||||
'image': {
|
||||
'name': _('Include Image'),
|
||||
'type': 'bool',
|
||||
'default': True,
|
||||
'map_to': 'include_image',
|
||||
},
|
||||
})
|
||||
|
||||
def __init__(self, priority=None, version=2, include_image=True, **kwargs):
|
||||
"""
|
||||
Initialize Growl Object
|
||||
"""
|
||||
super(NotifyGrowl, self).__init__(**kwargs)
|
||||
|
||||
if not self.port:
|
||||
self.port = self.default_port
|
||||
|
||||
# The Priority of the message
|
||||
if priority not in GROWL_PRIORITIES:
|
||||
self.priority = GrowlPriority.NORMAL
|
||||
|
||||
else:
|
||||
self.priority = priority
|
||||
|
||||
# Always default the sticky flag to False
|
||||
self.sticky = False
|
||||
|
||||
# Store Version
|
||||
self.version = version
|
||||
|
||||
payload = {
|
||||
'applicationName': self.app_id,
|
||||
'notifications': [GROWL_NOTIFICATION_TYPE, ],
|
||||
'defaultNotifications': [GROWL_NOTIFICATION_TYPE, ],
|
||||
'hostname': self.host,
|
||||
'port': self.port,
|
||||
}
|
||||
|
||||
if self.password is not None:
|
||||
payload['password'] = self.password
|
||||
|
||||
self.logger.debug('Growl Registration Payload: %s' % str(payload))
|
||||
self.growl = notifier.GrowlNotifier(**payload)
|
||||
|
||||
try:
|
||||
self.growl.register()
|
||||
self.logger.debug(
|
||||
'Growl server registration completed successfully.'
|
||||
)
|
||||
|
||||
except errors.NetworkError:
|
||||
msg = 'A network error occured sending Growl ' \
|
||||
'notification to {}.'.format(self.host)
|
||||
self.logger.warning(msg)
|
||||
raise TypeError(msg)
|
||||
|
||||
except errors.AuthError:
|
||||
msg = 'An authentication error occured sending Growl ' \
|
||||
'notification to {}.'.format(self.host)
|
||||
self.logger.warning(msg)
|
||||
raise TypeError(msg)
|
||||
|
||||
except errors.UnsupportedError:
|
||||
msg = 'An unsupported error occured sending Growl ' \
|
||||
'notification to {}.'.format(self.host)
|
||||
self.logger.warning(msg)
|
||||
raise TypeError(msg)
|
||||
|
||||
# Track whether or not we want to send an image with our notification
|
||||
# or not.
|
||||
self.include_image = include_image
|
||||
|
||||
return
|
||||
|
||||
def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs):
|
||||
"""
|
||||
Perform Growl Notification
|
||||
"""
|
||||
|
||||
icon = None
|
||||
if self.version >= 2:
|
||||
# URL Based
|
||||
icon = None if not self.include_image \
|
||||
else self.image_url(notify_type)
|
||||
|
||||
else:
|
||||
# Raw
|
||||
icon = None if not self.include_image \
|
||||
else self.image_raw(notify_type)
|
||||
|
||||
payload = {
|
||||
'noteType': GROWL_NOTIFICATION_TYPE,
|
||||
'title': title,
|
||||
'description': body,
|
||||
'icon': icon is not None,
|
||||
'sticky': False,
|
||||
'priority': self.priority,
|
||||
}
|
||||
self.logger.debug('Growl Payload: %s' % str(payload))
|
||||
|
||||
# Update icon of payload to be raw data; this is intentionally done
|
||||
# here after we spit the debug message above (so we don't try to
|
||||
# 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):
|
||||
self.logger.warning(
|
||||
'Growl notification failed to send with response: %s' %
|
||||
str(response),
|
||||
)
|
||||
|
||||
else:
|
||||
self.logger.info('Sent Growl notification.')
|
||||
|
||||
except errors.BaseError as e:
|
||||
# Since Growl servers listen for UDP broadcasts, it's possible
|
||||
# that you will never get to this part of the code since there is
|
||||
# no acknowledgement as to whether it accepted what was sent to it
|
||||
# or not.
|
||||
|
||||
# However, if the host/server is unavailable, you will get to this
|
||||
# point of the code.
|
||||
self.logger.warning(
|
||||
'A Connection error occured sending Growl '
|
||||
'notification to %s.' % self.host)
|
||||
self.logger.debug('Growl Exception: %s' % str(e))
|
||||
|
||||
# Return; we're done
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def url(self, privacy=False, *args, **kwargs):
|
||||
"""
|
||||
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,
|
||||
'image': 'yes' if self.include_image else 'no',
|
||||
'priority':
|
||||
_map[GrowlPriority.NORMAL] if self.priority not in _map
|
||||
else _map[self.priority],
|
||||
'version': self.version,
|
||||
'verify': 'yes' if self.verify_certificate else 'no',
|
||||
}
|
||||
|
||||
auth = ''
|
||||
if self.user:
|
||||
# The growl password is stored in the user field
|
||||
auth = '{password}@'.format(
|
||||
password=self.pprint(
|
||||
self.user, privacy, mode=PrivacyMode.Secret, safe=''),
|
||||
)
|
||||
|
||||
return '{schema}://{auth}{hostname}{port}/?{args}'.format(
|
||||
schema=self.secure_protocol if self.secure else self.protocol,
|
||||
auth=auth,
|
||||
hostname=NotifyGrowl.quote(self.host, safe=''),
|
||||
port='' if self.port is None or self.port == self.default_port
|
||||
else ':{}'.format(self.port),
|
||||
args=NotifyGrowl.urlencode(args),
|
||||
)
|
||||
|
||||
@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
|
||||
|
||||
version = None
|
||||
if 'version' in results['qsd'] and len(results['qsd']['version']):
|
||||
# Allow the user to specify the version of the protocol to use.
|
||||
try:
|
||||
version = int(
|
||||
NotifyGrowl.unquote(
|
||||
results['qsd']['version']).strip().split('.')[0])
|
||||
|
||||
except (AttributeError, IndexError, TypeError, ValueError):
|
||||
NotifyGrowl.logger.warning(
|
||||
'An invalid Growl version of "%s" was specified and will '
|
||||
'be ignored.' % results['qsd']['version']
|
||||
)
|
||||
pass
|
||||
|
||||
if 'priority' in results['qsd'] and len(results['qsd']['priority']):
|
||||
_map = {
|
||||
'l': GrowlPriority.LOW,
|
||||
'm': GrowlPriority.MODERATE,
|
||||
'n': GrowlPriority.NORMAL,
|
||||
'h': GrowlPriority.HIGH,
|
||||
'e': GrowlPriority.EMERGENCY,
|
||||
}
|
||||
try:
|
||||
results['priority'] = \
|
||||
_map[results['qsd']['priority'][0].lower()]
|
||||
|
||||
except KeyError:
|
||||
# No priority was set
|
||||
pass
|
||||
|
||||
# Because of the URL formatting, the password is actually where the
|
||||
# username field is. For this reason, we just preform this small hack
|
||||
# to make it (the URL) conform correctly. The following strips out the
|
||||
# existing password entry (if exists) so that it can be swapped with
|
||||
# the new one we specify.
|
||||
if results.get('password', None) is None:
|
||||
results['password'] = results.get('user', None)
|
||||
|
||||
# Include images with our message
|
||||
results['include_image'] = \
|
||||
parse_bool(results['qsd'].get('image', True))
|
||||
|
||||
# Set our version
|
||||
if version:
|
||||
results['version'] = version
|
||||
|
||||
return results
|
|
@ -1,141 +0,0 @@
|
|||
# Copyright: 2013 Paul Traylor
|
||||
# These sources are released under the terms of the MIT license: see LICENSE
|
||||
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
from optparse import OptionParser, OptionGroup
|
||||
|
||||
from .notifier import GrowlNotifier
|
||||
from .shim import RawConfigParser
|
||||
from .version import __version__
|
||||
|
||||
DEFAULT_CONFIG = os.path.expanduser('~/.gntp')
|
||||
|
||||
config = RawConfigParser({
|
||||
'hostname': 'localhost',
|
||||
'password': None,
|
||||
'port': 23053,
|
||||
})
|
||||
config.read([DEFAULT_CONFIG])
|
||||
if not config.has_section('gntp'):
|
||||
config.add_section('gntp')
|
||||
|
||||
|
||||
class ClientParser(OptionParser):
|
||||
def __init__(self):
|
||||
OptionParser.__init__(self, version="%%prog %s" % __version__)
|
||||
|
||||
group = OptionGroup(self, "Network Options")
|
||||
group.add_option("-H", "--host",
|
||||
dest="host", default=config.get('gntp', 'hostname'),
|
||||
help="Specify a hostname to which to send a remote notification. [%default]")
|
||||
group.add_option("--port",
|
||||
dest="port", default=config.getint('gntp', 'port'), type="int",
|
||||
help="port to listen on [%default]")
|
||||
group.add_option("-P", "--password",
|
||||
dest='password', default=config.get('gntp', 'password'),
|
||||
help="Network password")
|
||||
self.add_option_group(group)
|
||||
|
||||
group = OptionGroup(self, "Notification Options")
|
||||
group.add_option("-n", "--name",
|
||||
dest="app", default='Python GNTP Test Client',
|
||||
help="Set the name of the application [%default]")
|
||||
group.add_option("-s", "--sticky",
|
||||
dest='sticky', default=False, action="store_true",
|
||||
help="Make the notification sticky [%default]")
|
||||
group.add_option("--image",
|
||||
dest="icon", default=None,
|
||||
help="Icon for notification (URL or /path/to/file)")
|
||||
group.add_option("-m", "--message",
|
||||
dest="message", default=None,
|
||||
help="Sets the message instead of using stdin")
|
||||
group.add_option("-p", "--priority",
|
||||
dest="priority", default=0, type="int",
|
||||
help="-2 to 2 [%default]")
|
||||
group.add_option("-d", "--identifier",
|
||||
dest="identifier",
|
||||
help="Identifier for coalescing")
|
||||
group.add_option("-t", "--title",
|
||||
dest="title", default=None,
|
||||
help="Set the title of the notification [%default]")
|
||||
group.add_option("-N", "--notification",
|
||||
dest="name", default='Notification',
|
||||
help="Set the notification name [%default]")
|
||||
group.add_option("--callback",
|
||||
dest="callback",
|
||||
help="URL callback")
|
||||
self.add_option_group(group)
|
||||
|
||||
# Extra Options
|
||||
self.add_option('-v', '--verbose',
|
||||
dest='verbose', default=0, action='count',
|
||||
help="Verbosity levels")
|
||||
|
||||
def parse_args(self, args=None, values=None):
|
||||
values, args = OptionParser.parse_args(self, args, values)
|
||||
|
||||
if values.message is None:
|
||||
print('Enter a message followed by Ctrl-D')
|
||||
try:
|
||||
message = sys.stdin.read()
|
||||
except KeyboardInterrupt:
|
||||
exit()
|
||||
else:
|
||||
message = values.message
|
||||
|
||||
if values.title is None:
|
||||
values.title = ' '.join(args)
|
||||
|
||||
# If we still have an empty title, use the
|
||||
# first bit of the message as the title
|
||||
if values.title == '':
|
||||
values.title = message[:20]
|
||||
|
||||
values.verbose = logging.WARNING - values.verbose * 10
|
||||
|
||||
return values, message
|
||||
|
||||
|
||||
def main():
|
||||
(options, message) = ClientParser().parse_args()
|
||||
logging.basicConfig(level=options.verbose)
|
||||
if not os.path.exists(DEFAULT_CONFIG):
|
||||
logging.info('No config read found at %s', DEFAULT_CONFIG)
|
||||
|
||||
growl = GrowlNotifier(
|
||||
applicationName=options.app,
|
||||
notifications=[options.name],
|
||||
defaultNotifications=[options.name],
|
||||
hostname=options.host,
|
||||
password=options.password,
|
||||
port=options.port,
|
||||
)
|
||||
result = growl.register()
|
||||
if result is not True:
|
||||
exit(result)
|
||||
|
||||
# This would likely be better placed within the growl notifier
|
||||
# class but until I make _checkIcon smarter this is "easier"
|
||||
if options.icon is not None and not options.icon.startswith('http'):
|
||||
logging.info('Loading image %s', options.icon)
|
||||
f = open(options.icon)
|
||||
options.icon = f.read()
|
||||
f.close()
|
||||
|
||||
result = growl.notify(
|
||||
noteType=options.name,
|
||||
title=options.title,
|
||||
description=message,
|
||||
icon=options.icon,
|
||||
sticky=options.sticky,
|
||||
priority=options.priority,
|
||||
callback=options.callback,
|
||||
identifier=options.identifier,
|
||||
)
|
||||
if result is not True:
|
||||
exit(result)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
|
@ -1,77 +0,0 @@
|
|||
# Copyright: 2013 Paul Traylor
|
||||
# These sources are released under the terms of the MIT license: see LICENSE
|
||||
|
||||
"""
|
||||
The gntp.config module is provided as an extended GrowlNotifier object that takes
|
||||
advantage of the ConfigParser module to allow us to setup some default values
|
||||
(such as hostname, password, and port) in a more global way to be shared among
|
||||
programs using gntp
|
||||
"""
|
||||
import logging
|
||||
import os
|
||||
|
||||
from .gntp import notifier
|
||||
from .gntp import shim
|
||||
|
||||
__all__ = [
|
||||
'mini',
|
||||
'GrowlNotifier'
|
||||
]
|
||||
|
||||
logger = logging.getLogger('gntp')
|
||||
|
||||
|
||||
class GrowlNotifier(notifier.GrowlNotifier):
|
||||
"""
|
||||
ConfigParser enhanced GrowlNotifier object
|
||||
|
||||
For right now, we are only interested in letting users overide certain
|
||||
values from ~/.gntp
|
||||
|
||||
::
|
||||
|
||||
[gntp]
|
||||
hostname = ?
|
||||
password = ?
|
||||
port = ?
|
||||
"""
|
||||
def __init__(self, *args, **kwargs):
|
||||
config = shim.RawConfigParser({
|
||||
'hostname': kwargs.get('hostname', 'localhost'),
|
||||
'password': kwargs.get('password'),
|
||||
'port': kwargs.get('port', 23053),
|
||||
})
|
||||
|
||||
config.read([os.path.expanduser('~/.gntp')])
|
||||
|
||||
# If the file does not exist, then there will be no gntp section defined
|
||||
# and the config.get() lines below will get confused. Since we are not
|
||||
# saving the config, it should be safe to just add it here so the
|
||||
# code below doesn't complain
|
||||
if not config.has_section('gntp'):
|
||||
logger.info('Error reading ~/.gntp config file')
|
||||
config.add_section('gntp')
|
||||
|
||||
kwargs['password'] = config.get('gntp', 'password')
|
||||
kwargs['hostname'] = config.get('gntp', 'hostname')
|
||||
kwargs['port'] = config.getint('gntp', 'port')
|
||||
|
||||
super(GrowlNotifier, self).__init__(*args, **kwargs)
|
||||
|
||||
|
||||
def mini(description, **kwargs):
|
||||
"""Single notification function
|
||||
|
||||
Simple notification function in one line. Has only one required parameter
|
||||
and attempts to use reasonable defaults for everything else
|
||||
:param string description: Notification message
|
||||
"""
|
||||
kwargs['notifierFactory'] = GrowlNotifier
|
||||
notifier.mini(description, **kwargs)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
# If we're running this module directly we're likely running it as a test
|
||||
# so extra debugging is useful
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
mini('Testing mini notification')
|
|
@ -1,511 +0,0 @@
|
|||
# Copyright: 2013 Paul Traylor
|
||||
# These sources are released under the terms of the MIT license: see LICENSE
|
||||
|
||||
import hashlib
|
||||
import re
|
||||
import time
|
||||
|
||||
from . import shim
|
||||
from . import errors as errors
|
||||
|
||||
__all__ = [
|
||||
'GNTPRegister',
|
||||
'GNTPNotice',
|
||||
'GNTPSubscribe',
|
||||
'GNTPOK',
|
||||
'GNTPError',
|
||||
'parse_gntp',
|
||||
]
|
||||
|
||||
#GNTP/<version> <messagetype> <encryptionAlgorithmID>[:<ivValue>][ <keyHashAlgorithmID>:<keyHash>.<salt>]
|
||||
GNTP_INFO_LINE = re.compile(
|
||||
r'GNTP/(?P<version>\d+\.\d+) (?P<messagetype>REGISTER|NOTIFY|SUBSCRIBE|\-OK|\-ERROR)' +
|
||||
r' (?P<encryptionAlgorithmID>[A-Z0-9]+(:(?P<ivValue>[A-F0-9]+))?) ?' +
|
||||
r'((?P<keyHashAlgorithmID>[A-Z0-9]+):(?P<keyHash>[A-F0-9]+).(?P<salt>[A-F0-9]+))?\r\n',
|
||||
re.IGNORECASE
|
||||
)
|
||||
|
||||
GNTP_INFO_LINE_SHORT = re.compile(
|
||||
r'GNTP/(?P<version>\d+\.\d+) (?P<messagetype>REGISTER|NOTIFY|SUBSCRIBE|\-OK|\-ERROR)',
|
||||
re.IGNORECASE
|
||||
)
|
||||
|
||||
GNTP_HEADER = re.compile(r'([\w-]+):(.+)')
|
||||
|
||||
GNTP_EOL = shim.b('\r\n')
|
||||
GNTP_SEP = shim.b(': ')
|
||||
|
||||
|
||||
class _GNTPBuffer(shim.StringIO):
|
||||
"""GNTP Buffer class"""
|
||||
def writeln(self, value=None):
|
||||
if value:
|
||||
self.write(shim.b(value))
|
||||
self.write(GNTP_EOL)
|
||||
|
||||
def writeheader(self, key, value):
|
||||
if not isinstance(value, str):
|
||||
value = str(value)
|
||||
self.write(shim.b(key))
|
||||
self.write(GNTP_SEP)
|
||||
self.write(shim.b(value))
|
||||
self.write(GNTP_EOL)
|
||||
|
||||
|
||||
class _GNTPBase(object):
|
||||
"""Base initilization
|
||||
|
||||
:param string messagetype: GNTP Message type
|
||||
:param string version: GNTP Protocol version
|
||||
:param string encription: Encryption protocol
|
||||
"""
|
||||
def __init__(self, messagetype=None, version='1.0', encryption=None):
|
||||
self.info = {
|
||||
'version': version,
|
||||
'messagetype': messagetype,
|
||||
'encryptionAlgorithmID': encryption
|
||||
}
|
||||
self.hash_algo = {
|
||||
'MD5': hashlib.md5,
|
||||
'SHA1': hashlib.sha1,
|
||||
'SHA256': hashlib.sha256,
|
||||
'SHA512': hashlib.sha512,
|
||||
}
|
||||
self.headers = {}
|
||||
self.resources = {}
|
||||
|
||||
def __str__(self):
|
||||
return self.encode()
|
||||
|
||||
def _parse_info(self, data):
|
||||
"""Parse the first line of a GNTP message to get security and other info values
|
||||
|
||||
:param string data: GNTP Message
|
||||
:return dict: Parsed GNTP Info line
|
||||
"""
|
||||
|
||||
match = GNTP_INFO_LINE.match(data)
|
||||
|
||||
if not match:
|
||||
raise errors.ParseError('ERROR_PARSING_INFO_LINE')
|
||||
|
||||
info = match.groupdict()
|
||||
if info['encryptionAlgorithmID'] == 'NONE':
|
||||
info['encryptionAlgorithmID'] = None
|
||||
|
||||
return info
|
||||
|
||||
def set_password(self, password, encryptAlgo='MD5'):
|
||||
"""Set a password for a GNTP Message
|
||||
|
||||
:param string password: Null to clear password
|
||||
:param string encryptAlgo: Supports MD5, SHA1, SHA256, SHA512
|
||||
"""
|
||||
if not password:
|
||||
self.info['encryptionAlgorithmID'] = None
|
||||
self.info['keyHashAlgorithm'] = None
|
||||
return
|
||||
|
||||
self.password = shim.b(password)
|
||||
self.encryptAlgo = encryptAlgo.upper()
|
||||
|
||||
if not self.encryptAlgo in self.hash_algo:
|
||||
raise errors.UnsupportedError('INVALID HASH "%s"' % self.encryptAlgo)
|
||||
|
||||
hashfunction = self.hash_algo.get(self.encryptAlgo)
|
||||
|
||||
password = password.encode('utf8')
|
||||
seed = time.ctime().encode('utf8')
|
||||
salt = hashfunction(seed).hexdigest()
|
||||
saltHash = hashfunction(seed).digest()
|
||||
keyBasis = password + saltHash
|
||||
key = hashfunction(keyBasis).digest()
|
||||
keyHash = hashfunction(key).hexdigest()
|
||||
|
||||
self.info['keyHashAlgorithmID'] = self.encryptAlgo
|
||||
self.info['keyHash'] = keyHash.upper()
|
||||
self.info['salt'] = salt.upper()
|
||||
|
||||
def _decode_hex(self, value):
|
||||
"""Helper function to decode hex string to `proper` hex string
|
||||
|
||||
:param string value: Human readable hex string
|
||||
:return string: Hex string
|
||||
"""
|
||||
result = ''
|
||||
for i in range(0, len(value), 2):
|
||||
tmp = int(value[i:i + 2], 16)
|
||||
result += chr(tmp)
|
||||
return result
|
||||
|
||||
def _decode_binary(self, rawIdentifier, identifier):
|
||||
rawIdentifier += '\r\n\r\n'
|
||||
dataLength = int(identifier['Length'])
|
||||
pointerStart = self.raw.find(rawIdentifier) + len(rawIdentifier)
|
||||
pointerEnd = pointerStart + dataLength
|
||||
data = self.raw[pointerStart:pointerEnd]
|
||||
if not len(data) == dataLength:
|
||||
raise errors.ParseError('INVALID_DATA_LENGTH Expected: %s Recieved %s' % (dataLength, len(data)))
|
||||
return data
|
||||
|
||||
def _validate_password(self, password):
|
||||
"""Validate GNTP Message against stored password"""
|
||||
self.password = password
|
||||
if password is None:
|
||||
raise errors.AuthError('Missing password')
|
||||
keyHash = self.info.get('keyHash', None)
|
||||
if keyHash is None and self.password is None:
|
||||
return True
|
||||
if keyHash is None:
|
||||
raise errors.AuthError('Invalid keyHash')
|
||||
if self.password is None:
|
||||
raise errors.AuthError('Missing password')
|
||||
|
||||
keyHashAlgorithmID = self.info.get('keyHashAlgorithmID','MD5')
|
||||
|
||||
password = self.password.encode('utf8')
|
||||
saltHash = self._decode_hex(self.info['salt'])
|
||||
|
||||
keyBasis = password + saltHash
|
||||
self.key = self.hash_algo[keyHashAlgorithmID](keyBasis).digest()
|
||||
keyHash = self.hash_algo[keyHashAlgorithmID](self.key).hexdigest()
|
||||
|
||||
if not keyHash.upper() == self.info['keyHash'].upper():
|
||||
raise errors.AuthError('Invalid Hash')
|
||||
return True
|
||||
|
||||
def validate(self):
|
||||
"""Verify required headers"""
|
||||
for header in self._requiredHeaders:
|
||||
if not self.headers.get(header, False):
|
||||
raise errors.ParseError('Missing Notification Header: ' + header)
|
||||
|
||||
def _format_info(self):
|
||||
"""Generate info line for GNTP Message
|
||||
|
||||
:return string:
|
||||
"""
|
||||
info = 'GNTP/%s %s' % (
|
||||
self.info.get('version'),
|
||||
self.info.get('messagetype'),
|
||||
)
|
||||
if self.info.get('encryptionAlgorithmID', None):
|
||||
info += ' %s:%s' % (
|
||||
self.info.get('encryptionAlgorithmID'),
|
||||
self.info.get('ivValue'),
|
||||
)
|
||||
else:
|
||||
info += ' NONE'
|
||||
|
||||
if self.info.get('keyHashAlgorithmID', None):
|
||||
info += ' %s:%s.%s' % (
|
||||
self.info.get('keyHashAlgorithmID'),
|
||||
self.info.get('keyHash'),
|
||||
self.info.get('salt')
|
||||
)
|
||||
|
||||
return info
|
||||
|
||||
def _parse_dict(self, data):
|
||||
"""Helper function to parse blocks of GNTP headers into a dictionary
|
||||
|
||||
:param string data:
|
||||
:return dict: Dictionary of parsed GNTP Headers
|
||||
"""
|
||||
d = {}
|
||||
for line in data.split('\r\n'):
|
||||
match = GNTP_HEADER.match(line)
|
||||
if not match:
|
||||
continue
|
||||
|
||||
key = match.group(1).strip()
|
||||
val = match.group(2).strip()
|
||||
d[key] = val
|
||||
return d
|
||||
|
||||
def add_header(self, key, value):
|
||||
self.headers[key] = value
|
||||
|
||||
def add_resource(self, data):
|
||||
"""Add binary resource
|
||||
|
||||
:param string data: Binary Data
|
||||
"""
|
||||
data = shim.b(data)
|
||||
identifier = hashlib.md5(data).hexdigest()
|
||||
self.resources[identifier] = data
|
||||
return 'x-growl-resource://%s' % identifier
|
||||
|
||||
def decode(self, data, password=None):
|
||||
"""Decode GNTP Message
|
||||
|
||||
:param string data:
|
||||
"""
|
||||
self.password = password
|
||||
self.raw = shim.u(data)
|
||||
parts = self.raw.split('\r\n\r\n')
|
||||
self.info = self._parse_info(self.raw)
|
||||
self.headers = self._parse_dict(parts[0])
|
||||
|
||||
def encode(self):
|
||||
"""Encode a generic GNTP Message
|
||||
|
||||
:return string: GNTP Message ready to be sent. Returned as a byte string
|
||||
"""
|
||||
|
||||
buff = _GNTPBuffer()
|
||||
|
||||
buff.writeln(self._format_info())
|
||||
|
||||
#Headers
|
||||
for k, v in self.headers.items():
|
||||
buff.writeheader(k, v)
|
||||
buff.writeln()
|
||||
|
||||
#Resources
|
||||
for resource, data in self.resources.items():
|
||||
buff.writeheader('Identifier', resource)
|
||||
buff.writeheader('Length', len(data))
|
||||
buff.writeln()
|
||||
buff.write(data)
|
||||
buff.writeln()
|
||||
buff.writeln()
|
||||
|
||||
return buff.getvalue()
|
||||
|
||||
|
||||
class GNTPRegister(_GNTPBase):
|
||||
"""Represents a GNTP Registration Command
|
||||
|
||||
:param string data: (Optional) See decode()
|
||||
:param string password: (Optional) Password to use while encoding/decoding messages
|
||||
"""
|
||||
_requiredHeaders = [
|
||||
'Application-Name',
|
||||
'Notifications-Count'
|
||||
]
|
||||
_requiredNotificationHeaders = ['Notification-Name']
|
||||
|
||||
def __init__(self, data=None, password=None):
|
||||
_GNTPBase.__init__(self, 'REGISTER')
|
||||
self.notifications = []
|
||||
|
||||
if data:
|
||||
self.decode(data, password)
|
||||
else:
|
||||
self.set_password(password)
|
||||
self.add_header('Application-Name', 'pygntp')
|
||||
self.add_header('Notifications-Count', 0)
|
||||
|
||||
def validate(self):
|
||||
'''Validate required headers and validate notification headers'''
|
||||
for header in self._requiredHeaders:
|
||||
if not self.headers.get(header, False):
|
||||
raise errors.ParseError('Missing Registration Header: ' + header)
|
||||
for notice in self.notifications:
|
||||
for header in self._requiredNotificationHeaders:
|
||||
if not notice.get(header, False):
|
||||
raise errors.ParseError('Missing Notification Header: ' + header)
|
||||
|
||||
def decode(self, data, password):
|
||||
"""Decode existing GNTP Registration message
|
||||
|
||||
:param string data: Message to decode
|
||||
"""
|
||||
self.raw = shim.u(data)
|
||||
parts = self.raw.split('\r\n\r\n')
|
||||
self.info = self._parse_info(self.raw)
|
||||
self._validate_password(password)
|
||||
self.headers = self._parse_dict(parts[0])
|
||||
|
||||
for i, part in enumerate(parts):
|
||||
if i == 0:
|
||||
continue # Skip Header
|
||||
if part.strip() == '':
|
||||
continue
|
||||
notice = self._parse_dict(part)
|
||||
if notice.get('Notification-Name', False):
|
||||
self.notifications.append(notice)
|
||||
elif notice.get('Identifier', False):
|
||||
notice['Data'] = self._decode_binary(part, notice)
|
||||
#open('register.png','wblol').write(notice['Data'])
|
||||
self.resources[notice.get('Identifier')] = notice
|
||||
|
||||
def add_notification(self, name, enabled=True):
|
||||
"""Add new Notification to Registration message
|
||||
|
||||
:param string name: Notification Name
|
||||
:param boolean enabled: Enable this notification by default
|
||||
"""
|
||||
notice = {}
|
||||
notice['Notification-Name'] = name
|
||||
notice['Notification-Enabled'] = enabled
|
||||
|
||||
self.notifications.append(notice)
|
||||
self.add_header('Notifications-Count', len(self.notifications))
|
||||
|
||||
def encode(self):
|
||||
"""Encode a GNTP Registration Message
|
||||
|
||||
:return string: Encoded GNTP Registration message. Returned as a byte string
|
||||
"""
|
||||
|
||||
buff = _GNTPBuffer()
|
||||
|
||||
buff.writeln(self._format_info())
|
||||
|
||||
#Headers
|
||||
for k, v in self.headers.items():
|
||||
buff.writeheader(k, v)
|
||||
buff.writeln()
|
||||
|
||||
#Notifications
|
||||
if len(self.notifications) > 0:
|
||||
for notice in self.notifications:
|
||||
for k, v in notice.items():
|
||||
buff.writeheader(k, v)
|
||||
buff.writeln()
|
||||
|
||||
#Resources
|
||||
for resource, data in self.resources.items():
|
||||
buff.writeheader('Identifier', resource)
|
||||
buff.writeheader('Length', len(data))
|
||||
buff.writeln()
|
||||
buff.write(data)
|
||||
buff.writeln()
|
||||
buff.writeln()
|
||||
|
||||
return buff.getvalue()
|
||||
|
||||
|
||||
class GNTPNotice(_GNTPBase):
|
||||
"""Represents a GNTP Notification Command
|
||||
|
||||
:param string data: (Optional) See decode()
|
||||
:param string app: (Optional) Set Application-Name
|
||||
:param string name: (Optional) Set Notification-Name
|
||||
:param string title: (Optional) Set Notification Title
|
||||
:param string password: (Optional) Password to use while encoding/decoding messages
|
||||
"""
|
||||
_requiredHeaders = [
|
||||
'Application-Name',
|
||||
'Notification-Name',
|
||||
'Notification-Title'
|
||||
]
|
||||
|
||||
def __init__(self, data=None, app=None, name=None, title=None, password=None):
|
||||
_GNTPBase.__init__(self, 'NOTIFY')
|
||||
|
||||
if data:
|
||||
self.decode(data, password)
|
||||
else:
|
||||
self.set_password(password)
|
||||
if app:
|
||||
self.add_header('Application-Name', app)
|
||||
if name:
|
||||
self.add_header('Notification-Name', name)
|
||||
if title:
|
||||
self.add_header('Notification-Title', title)
|
||||
|
||||
def decode(self, data, password):
|
||||
"""Decode existing GNTP Notification message
|
||||
|
||||
:param string data: Message to decode.
|
||||
"""
|
||||
self.raw = shim.u(data)
|
||||
parts = self.raw.split('\r\n\r\n')
|
||||
self.info = self._parse_info(self.raw)
|
||||
self._validate_password(password)
|
||||
self.headers = self._parse_dict(parts[0])
|
||||
|
||||
for i, part in enumerate(parts):
|
||||
if i == 0:
|
||||
continue # Skip Header
|
||||
if part.strip() == '':
|
||||
continue
|
||||
notice = self._parse_dict(part)
|
||||
if notice.get('Identifier', False):
|
||||
notice['Data'] = self._decode_binary(part, notice)
|
||||
#open('notice.png','wblol').write(notice['Data'])
|
||||
self.resources[notice.get('Identifier')] = notice
|
||||
|
||||
|
||||
class GNTPSubscribe(_GNTPBase):
|
||||
"""Represents a GNTP Subscribe Command
|
||||
|
||||
:param string data: (Optional) See decode()
|
||||
:param string password: (Optional) Password to use while encoding/decoding messages
|
||||
"""
|
||||
_requiredHeaders = [
|
||||
'Subscriber-ID',
|
||||
'Subscriber-Name',
|
||||
]
|
||||
|
||||
def __init__(self, data=None, password=None):
|
||||
_GNTPBase.__init__(self, 'SUBSCRIBE')
|
||||
if data:
|
||||
self.decode(data, password)
|
||||
else:
|
||||
self.set_password(password)
|
||||
|
||||
|
||||
class GNTPOK(_GNTPBase):
|
||||
"""Represents a GNTP OK Response
|
||||
|
||||
:param string data: (Optional) See _GNTPResponse.decode()
|
||||
:param string action: (Optional) Set type of action the OK Response is for
|
||||
"""
|
||||
_requiredHeaders = ['Response-Action']
|
||||
|
||||
def __init__(self, data=None, action=None):
|
||||
_GNTPBase.__init__(self, '-OK')
|
||||
if data:
|
||||
self.decode(data)
|
||||
if action:
|
||||
self.add_header('Response-Action', action)
|
||||
|
||||
|
||||
class GNTPError(_GNTPBase):
|
||||
"""Represents a GNTP Error response
|
||||
|
||||
:param string data: (Optional) See _GNTPResponse.decode()
|
||||
:param string errorcode: (Optional) Error code
|
||||
:param string errordesc: (Optional) Error Description
|
||||
"""
|
||||
_requiredHeaders = ['Error-Code', 'Error-Description']
|
||||
|
||||
def __init__(self, data=None, errorcode=None, errordesc=None):
|
||||
_GNTPBase.__init__(self, '-ERROR')
|
||||
if data:
|
||||
self.decode(data)
|
||||
if errorcode:
|
||||
self.add_header('Error-Code', errorcode)
|
||||
self.add_header('Error-Description', errordesc)
|
||||
|
||||
def error(self):
|
||||
return (self.headers.get('Error-Code', None),
|
||||
self.headers.get('Error-Description', None))
|
||||
|
||||
|
||||
def parse_gntp(data, password=None):
|
||||
"""Attempt to parse a message as a GNTP message
|
||||
|
||||
:param string data: Message to be parsed
|
||||
:param string password: Optional password to be used to verify the message
|
||||
"""
|
||||
data = shim.u(data)
|
||||
match = GNTP_INFO_LINE_SHORT.match(data)
|
||||
if not match:
|
||||
raise errors.ParseError('INVALID_GNTP_INFO')
|
||||
info = match.groupdict()
|
||||
if info['messagetype'] == 'REGISTER':
|
||||
return GNTPRegister(data, password=password)
|
||||
elif info['messagetype'] == 'NOTIFY':
|
||||
return GNTPNotice(data, password=password)
|
||||
elif info['messagetype'] == 'SUBSCRIBE':
|
||||
return GNTPSubscribe(data, password=password)
|
||||
elif info['messagetype'] == '-OK':
|
||||
return GNTPOK(data)
|
||||
elif info['messagetype'] == '-ERROR':
|
||||
return GNTPError(data)
|
||||
raise errors.ParseError('INVALID_GNTP_MESSAGE')
|
|
@ -1,25 +0,0 @@
|
|||
# Copyright: 2013 Paul Traylor
|
||||
# These sources are released under the terms of the MIT license: see LICENSE
|
||||
|
||||
class BaseError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class ParseError(BaseError):
|
||||
errorcode = 500
|
||||
errordesc = 'Error parsing the message'
|
||||
|
||||
|
||||
class AuthError(BaseError):
|
||||
errorcode = 400
|
||||
errordesc = 'Error with authorization'
|
||||
|
||||
|
||||
class UnsupportedError(BaseError):
|
||||
errorcode = 500
|
||||
errordesc = 'Currently unsupported by gntp.py'
|
||||
|
||||
|
||||
class NetworkError(BaseError):
|
||||
errorcode = 500
|
||||
errordesc = "Error connecting to growl server"
|
|
@ -1,265 +0,0 @@
|
|||
# Copyright: 2013 Paul Traylor
|
||||
# These sources are released under the terms of the MIT license: see LICENSE
|
||||
|
||||
"""
|
||||
The gntp.notifier module is provided as a simple way to send notifications
|
||||
using GNTP
|
||||
|
||||
.. note::
|
||||
This class is intended to mostly mirror the older Python bindings such
|
||||
that you should be able to replace instances of the old bindings with
|
||||
this class.
|
||||
`Original Python bindings <http://code.google.com/p/growl/source/browse/Bindings/python/Growl.py>`_
|
||||
|
||||
"""
|
||||
import logging
|
||||
import platform
|
||||
import socket
|
||||
import sys
|
||||
|
||||
from .version import __version__
|
||||
from . import core
|
||||
from . import errors as errors
|
||||
from . import shim
|
||||
|
||||
__all__ = [
|
||||
'mini',
|
||||
'GrowlNotifier',
|
||||
]
|
||||
|
||||
logger = logging.getLogger('gntp')
|
||||
|
||||
|
||||
class GrowlNotifier(object):
|
||||
"""Helper class to simplfy sending Growl messages
|
||||
|
||||
:param string applicationName: Sending application name
|
||||
:param list notification: List of valid notifications
|
||||
:param list defaultNotifications: List of notifications that should be enabled
|
||||
by default
|
||||
:param string applicationIcon: Icon URL
|
||||
:param string hostname: Remote host
|
||||
:param integer port: Remote port
|
||||
"""
|
||||
|
||||
passwordHash = 'MD5'
|
||||
socketTimeout = 3
|
||||
|
||||
def __init__(self, applicationName='Python GNTP', notifications=[],
|
||||
defaultNotifications=None, applicationIcon=None, hostname='localhost',
|
||||
password=None, port=23053):
|
||||
|
||||
self.applicationName = applicationName
|
||||
self.notifications = list(notifications)
|
||||
if defaultNotifications:
|
||||
self.defaultNotifications = list(defaultNotifications)
|
||||
else:
|
||||
self.defaultNotifications = self.notifications
|
||||
self.applicationIcon = applicationIcon
|
||||
|
||||
self.password = password
|
||||
self.hostname = hostname
|
||||
self.port = int(port)
|
||||
|
||||
def _checkIcon(self, data):
|
||||
'''
|
||||
Check the icon to see if it's valid
|
||||
|
||||
If it's a simple URL icon, then we return True. If it's a data icon
|
||||
then we return False
|
||||
'''
|
||||
logger.info('Checking icon')
|
||||
return shim.u(data).startswith('http')
|
||||
|
||||
def register(self):
|
||||
"""Send GNTP Registration
|
||||
|
||||
.. warning::
|
||||
Before sending notifications to Growl, you need to have
|
||||
sent a registration message at least once
|
||||
"""
|
||||
logger.info('Sending registration to %s:%s', self.hostname, self.port)
|
||||
register = core.GNTPRegister()
|
||||
register.add_header('Application-Name', self.applicationName)
|
||||
for notification in self.notifications:
|
||||
enabled = notification in self.defaultNotifications
|
||||
register.add_notification(notification, enabled)
|
||||
if self.applicationIcon:
|
||||
if self._checkIcon(self.applicationIcon):
|
||||
register.add_header('Application-Icon', self.applicationIcon)
|
||||
else:
|
||||
resource = register.add_resource(self.applicationIcon)
|
||||
register.add_header('Application-Icon', resource)
|
||||
if self.password:
|
||||
register.set_password(self.password, self.passwordHash)
|
||||
self.add_origin_info(register)
|
||||
self.register_hook(register)
|
||||
return self._send('register', register)
|
||||
|
||||
def notify(self, noteType, title, description, icon=None, sticky=False,
|
||||
priority=None, callback=None, identifier=None, custom={}):
|
||||
"""Send a GNTP notifications
|
||||
|
||||
.. warning::
|
||||
Must have registered with growl beforehand or messages will be ignored
|
||||
|
||||
:param string noteType: One of the notification names registered earlier
|
||||
:param string title: Notification title (usually displayed on the notification)
|
||||
:param string description: The main content of the notification
|
||||
:param string icon: Icon URL path
|
||||
:param boolean sticky: Sticky notification
|
||||
:param integer priority: Message priority level from -2 to 2
|
||||
:param string callback: URL callback
|
||||
:param dict custom: Custom attributes. Key names should be prefixed with X-
|
||||
according to the spec but this is not enforced by this class
|
||||
|
||||
.. warning::
|
||||
For now, only URL callbacks are supported. In the future, the
|
||||
callback argument will also support a function
|
||||
"""
|
||||
logger.info('Sending notification [%s] to %s:%s', noteType, self.hostname, self.port)
|
||||
assert noteType in self.notifications
|
||||
notice = core.GNTPNotice()
|
||||
notice.add_header('Application-Name', self.applicationName)
|
||||
notice.add_header('Notification-Name', noteType)
|
||||
notice.add_header('Notification-Title', title)
|
||||
if self.password:
|
||||
notice.set_password(self.password, self.passwordHash)
|
||||
if sticky:
|
||||
notice.add_header('Notification-Sticky', sticky)
|
||||
if priority:
|
||||
notice.add_header('Notification-Priority', priority)
|
||||
if icon:
|
||||
if self._checkIcon(icon):
|
||||
notice.add_header('Notification-Icon', icon)
|
||||
else:
|
||||
resource = notice.add_resource(icon)
|
||||
notice.add_header('Notification-Icon', resource)
|
||||
|
||||
if description:
|
||||
notice.add_header('Notification-Text', description)
|
||||
if callback:
|
||||
notice.add_header('Notification-Callback-Target', callback)
|
||||
if identifier:
|
||||
notice.add_header('Notification-Coalescing-ID', identifier)
|
||||
|
||||
for key in custom:
|
||||
notice.add_header(key, custom[key])
|
||||
|
||||
self.add_origin_info(notice)
|
||||
self.notify_hook(notice)
|
||||
|
||||
return self._send('notify', notice)
|
||||
|
||||
def subscribe(self, id, name, port):
|
||||
"""Send a Subscribe request to a remote machine"""
|
||||
sub = core.GNTPSubscribe()
|
||||
sub.add_header('Subscriber-ID', id)
|
||||
sub.add_header('Subscriber-Name', name)
|
||||
sub.add_header('Subscriber-Port', port)
|
||||
if self.password:
|
||||
sub.set_password(self.password, self.passwordHash)
|
||||
|
||||
self.add_origin_info(sub)
|
||||
self.subscribe_hook(sub)
|
||||
|
||||
return self._send('subscribe', sub)
|
||||
|
||||
def add_origin_info(self, packet):
|
||||
"""Add optional Origin headers to message"""
|
||||
packet.add_header('Origin-Machine-Name', platform.node())
|
||||
packet.add_header('Origin-Software-Name', 'gntp.py')
|
||||
packet.add_header('Origin-Software-Version', __version__)
|
||||
packet.add_header('Origin-Platform-Name', platform.system())
|
||||
packet.add_header('Origin-Platform-Version', platform.platform())
|
||||
|
||||
def register_hook(self, packet):
|
||||
pass
|
||||
|
||||
def notify_hook(self, packet):
|
||||
pass
|
||||
|
||||
def subscribe_hook(self, packet):
|
||||
pass
|
||||
|
||||
def _send(self, messagetype, packet):
|
||||
"""Send the GNTP Packet"""
|
||||
|
||||
packet.validate()
|
||||
data = packet.encode()
|
||||
|
||||
logger.debug('To : %s:%s <%s>\n%s', self.hostname, self.port, packet.__class__, data)
|
||||
|
||||
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
s.settimeout(self.socketTimeout)
|
||||
try:
|
||||
s.connect((self.hostname, self.port))
|
||||
s.send(data)
|
||||
recv_data = s.recv(1024)
|
||||
while not recv_data.endswith(shim.b("\r\n\r\n")):
|
||||
recv_data += s.recv(1024)
|
||||
except socket.error:
|
||||
# Python2.5 and Python3 compatibile exception
|
||||
exc = sys.exc_info()[1]
|
||||
raise errors.NetworkError(exc)
|
||||
|
||||
response = core.parse_gntp(recv_data)
|
||||
s.close()
|
||||
|
||||
logger.debug('From : %s:%s <%s>\n%s', self.hostname, self.port, response.__class__, response)
|
||||
|
||||
if type(response) == core.GNTPOK:
|
||||
return True
|
||||
logger.error('Invalid response: %s', response.error())
|
||||
return response.error()
|
||||
|
||||
|
||||
def mini(description, applicationName='PythonMini', noteType="Message",
|
||||
title="Mini Message", applicationIcon=None, hostname='localhost',
|
||||
password=None, port=23053, sticky=False, priority=None,
|
||||
callback=None, notificationIcon=None, identifier=None,
|
||||
notifierFactory=GrowlNotifier):
|
||||
"""Single notification function
|
||||
|
||||
Simple notification function in one line. Has only one required parameter
|
||||
and attempts to use reasonable defaults for everything else
|
||||
:param string description: Notification message
|
||||
|
||||
.. warning::
|
||||
For now, only URL callbacks are supported. In the future, the
|
||||
callback argument will also support a function
|
||||
"""
|
||||
try:
|
||||
growl = notifierFactory(
|
||||
applicationName=applicationName,
|
||||
notifications=[noteType],
|
||||
defaultNotifications=[noteType],
|
||||
applicationIcon=applicationIcon,
|
||||
hostname=hostname,
|
||||
password=password,
|
||||
port=port,
|
||||
)
|
||||
result = growl.register()
|
||||
if result is not True:
|
||||
return result
|
||||
|
||||
return growl.notify(
|
||||
noteType=noteType,
|
||||
title=title,
|
||||
description=description,
|
||||
icon=notificationIcon,
|
||||
sticky=sticky,
|
||||
priority=priority,
|
||||
callback=callback,
|
||||
identifier=identifier,
|
||||
)
|
||||
except Exception:
|
||||
# We want the "mini" function to be simple and swallow Exceptions
|
||||
# in order to be less invasive
|
||||
logger.exception("Growl error")
|
||||
|
||||
if __name__ == '__main__':
|
||||
# If we're running this module directly we're likely running it as a test
|
||||
# so extra debugging is useful
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
mini('Testing mini notification')
|
|
@ -1,45 +0,0 @@
|
|||
# Copyright: 2013 Paul Traylor
|
||||
# These sources are released under the terms of the MIT license: see LICENSE
|
||||
|
||||
"""
|
||||
Python2.5 and Python3.3 compatibility shim
|
||||
|
||||
Heavily inspirted by the "six" library.
|
||||
https://pypi.python.org/pypi/six
|
||||
"""
|
||||
|
||||
import sys
|
||||
|
||||
PY3 = sys.version_info[0] == 3
|
||||
|
||||
if PY3:
|
||||
def b(s):
|
||||
if isinstance(s, bytes):
|
||||
return s
|
||||
return s.encode('utf8', 'replace')
|
||||
|
||||
def u(s):
|
||||
if isinstance(s, bytes):
|
||||
return s.decode('utf8', 'replace')
|
||||
return s
|
||||
|
||||
from io import BytesIO as StringIO
|
||||
from configparser import RawConfigParser
|
||||
else:
|
||||
def b(s):
|
||||
if isinstance(s, unicode): # noqa
|
||||
return s.encode('utf8', 'replace')
|
||||
return s
|
||||
|
||||
def u(s):
|
||||
if isinstance(s, unicode): # noqa
|
||||
return s
|
||||
if isinstance(s, int):
|
||||
s = str(s)
|
||||
return unicode(s, "utf8", "replace") # noqa
|
||||
|
||||
from StringIO import StringIO
|
||||
from ConfigParser import RawConfigParser
|
||||
|
||||
b.__doc__ = "Ensure we have a byte string"
|
||||
u.__doc__ = "Ensure we have a unicode string"
|
|
@ -1,4 +0,0 @@
|
|||
# Copyright: 2013 Paul Traylor
|
||||
# These sources are released under the terms of the MIT license: see LICENSE
|
||||
|
||||
__version__ = '1.0.2'
|
|
@ -242,6 +242,7 @@ class NotifyIFTTT(NotifyBase):
|
|||
data=dumps(payload),
|
||||
headers=headers,
|
||||
verify=self.verify_certificate,
|
||||
timeout=self.request_timeout,
|
||||
)
|
||||
self.logger.debug(
|
||||
u"IFTTT HTTP response headers: %r" % r.headers)
|
||||
|
@ -274,7 +275,7 @@ class NotifyIFTTT(NotifyBase):
|
|||
|
||||
except requests.RequestException as e:
|
||||
self.logger.warning(
|
||||
'A Connection error occured sending IFTTT:%s ' % (
|
||||
'A Connection error occurred sending IFTTT:%s ' % (
|
||||
event) + 'notification.'
|
||||
)
|
||||
self.logger.debug('Socket Exception: %s' % str(e))
|
||||
|
@ -290,34 +291,29 @@ class NotifyIFTTT(NotifyBase):
|
|||
Returns the URL built dynamically based on specified arguments.
|
||||
"""
|
||||
|
||||
# Define any arguments set
|
||||
args = {
|
||||
'format': self.notify_format,
|
||||
'overflow': self.overflow_mode,
|
||||
'verify': 'yes' if self.verify_certificate else 'no',
|
||||
}
|
||||
# Our URL parameters
|
||||
params = self.url_parameters(privacy=privacy, *args, **kwargs)
|
||||
|
||||
# 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})
|
||||
params.update({'+{}'.format(k): v for k, v in self.add_tokens})
|
||||
params.update({'-{}'.format(k): '' for k in self.del_tokens})
|
||||
|
||||
return '{schema}://{webhook_id}@{events}/?{args}'.format(
|
||||
return '{schema}://{webhook_id}@{events}/?{params}'.format(
|
||||
schema=self.secure_protocol,
|
||||
webhook_id=self.pprint(self.webhook_id, privacy, safe=''),
|
||||
events='/'.join([NotifyIFTTT.quote(x, safe='')
|
||||
for x in self.events]),
|
||||
args=NotifyIFTTT.urlencode(args),
|
||||
params=NotifyIFTTT.urlencode(params),
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def parse_url(url):
|
||||
"""
|
||||
Parses the URL and returns enough arguments that can allow
|
||||
us to substantiate this object.
|
||||
us to re-instantiate this object.
|
||||
|
||||
"""
|
||||
results = NotifyBase.parse_url(url)
|
||||
|
||||
results = NotifyBase.parse_url(url, verify_host=False)
|
||||
if not results:
|
||||
# We're done early as we couldn't load the results
|
||||
return results
|
||||
|
@ -356,16 +352,16 @@ class NotifyIFTTT(NotifyBase):
|
|||
r'^https?://maker\.ifttt\.com/use/'
|
||||
r'(?P<webhook_id>[A-Z0-9_-]+)'
|
||||
r'/?(?P<events>([A-Z0-9_-]+/?)+)?'
|
||||
r'/?(?P<args>\?.+)?$', url, re.I)
|
||||
r'/?(?P<params>\?.+)?$', url, re.I)
|
||||
|
||||
if result:
|
||||
return NotifyIFTTT.parse_url(
|
||||
'{schema}://{webhook_id}{events}{args}'.format(
|
||||
'{schema}://{webhook_id}{events}{params}'.format(
|
||||
schema=NotifyIFTTT.secure_protocol,
|
||||
webhook_id=result.group('webhook_id'),
|
||||
events='' if not result.group('events')
|
||||
else '@{}'.format(result.group('events')),
|
||||
args='' if not result.group('args')
|
||||
else result.group('args')))
|
||||
params='' if not result.group('params')
|
||||
else result.group('params')))
|
||||
|
||||
return None
|
||||
|
|
|
@ -128,15 +128,11 @@ class NotifyJSON(NotifyBase):
|
|||
Returns the URL built dynamically based on specified arguments.
|
||||
"""
|
||||
|
||||
# Define any arguments set
|
||||
args = {
|
||||
'format': self.notify_format,
|
||||
'overflow': self.overflow_mode,
|
||||
'verify': 'yes' if self.verify_certificate else 'no',
|
||||
}
|
||||
# Our URL parameters
|
||||
params = self.url_parameters(privacy=privacy, *args, **kwargs)
|
||||
|
||||
# Append our headers into our args
|
||||
args.update({'+{}'.format(k): v for k, v in self.headers.items()})
|
||||
# Append our headers into our parameters
|
||||
params.update({'+{}'.format(k): v for k, v in self.headers.items()})
|
||||
|
||||
# Determine Authentication
|
||||
auth = ''
|
||||
|
@ -153,14 +149,15 @@ class NotifyJSON(NotifyBase):
|
|||
|
||||
default_port = 443 if self.secure else 80
|
||||
|
||||
return '{schema}://{auth}{hostname}{port}{fullpath}/?{args}'.format(
|
||||
return '{schema}://{auth}{hostname}{port}{fullpath}/?{params}'.format(
|
||||
schema=self.secure_protocol if self.secure else self.protocol,
|
||||
auth=auth,
|
||||
hostname=NotifyJSON.quote(self.host, safe=''),
|
||||
# never encode hostname since we're expecting it to be a valid one
|
||||
hostname=self.host,
|
||||
port='' if self.port is None or self.port == default_port
|
||||
else ':{}'.format(self.port),
|
||||
fullpath=NotifyJSON.quote(self.fullpath, safe='/'),
|
||||
args=NotifyJSON.urlencode(args),
|
||||
params=NotifyJSON.urlencode(params),
|
||||
)
|
||||
|
||||
def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs):
|
||||
|
@ -215,6 +212,7 @@ class NotifyJSON(NotifyBase):
|
|||
headers=headers,
|
||||
auth=auth,
|
||||
verify=self.verify_certificate,
|
||||
timeout=self.request_timeout,
|
||||
)
|
||||
if r.status_code != requests.codes.ok:
|
||||
# We had a problem
|
||||
|
@ -238,7 +236,7 @@ class NotifyJSON(NotifyBase):
|
|||
|
||||
except requests.RequestException as e:
|
||||
self.logger.warning(
|
||||
'A Connection error occured sending JSON '
|
||||
'A Connection error occurred sending JSON '
|
||||
'notification to %s.' % self.host)
|
||||
self.logger.debug('Socket Exception: %s' % str(e))
|
||||
|
||||
|
@ -251,11 +249,10 @@ class NotifyJSON(NotifyBase):
|
|||
def parse_url(url):
|
||||
"""
|
||||
Parses the URL and returns enough arguments that can allow
|
||||
us to substantiate this object.
|
||||
us to re-instantiate this object.
|
||||
|
||||
"""
|
||||
results = NotifyBase.parse_url(url)
|
||||
|
||||
if not results:
|
||||
# We're done early as we couldn't load the results
|
||||
return results
|
||||
|
|
|
@ -280,6 +280,7 @@ class NotifyJoin(NotifyBase):
|
|||
data=payload,
|
||||
headers=headers,
|
||||
verify=self.verify_certificate,
|
||||
timeout=self.request_timeout,
|
||||
)
|
||||
|
||||
if r.status_code != requests.codes.ok:
|
||||
|
@ -308,7 +309,7 @@ class NotifyJoin(NotifyBase):
|
|||
|
||||
except requests.RequestException as e:
|
||||
self.logger.warning(
|
||||
'A Connection error occured sending Join:%s '
|
||||
'A Connection error occurred sending Join:%s '
|
||||
'notification.' % target
|
||||
)
|
||||
self.logger.debug('Socket Exception: %s' % str(e))
|
||||
|
@ -331,33 +332,32 @@ class NotifyJoin(NotifyBase):
|
|||
JoinPriority.EMERGENCY: 'emergency',
|
||||
}
|
||||
|
||||
# Define any arguments set
|
||||
args = {
|
||||
'format': self.notify_format,
|
||||
'overflow': self.overflow_mode,
|
||||
# Define any URL parameters
|
||||
params = {
|
||||
'priority':
|
||||
_map[self.template_args['priority']['default']]
|
||||
if self.priority not in _map else _map[self.priority],
|
||||
'image': 'yes' if self.include_image else 'no',
|
||||
'verify': 'yes' if self.verify_certificate else 'no',
|
||||
}
|
||||
|
||||
return '{schema}://{apikey}/{targets}/?{args}'.format(
|
||||
# Extend our parameters
|
||||
params.update(self.url_parameters(privacy=privacy, *args, **kwargs))
|
||||
|
||||
return '{schema}://{apikey}/{targets}/?{params}'.format(
|
||||
schema=self.secure_protocol,
|
||||
apikey=self.pprint(self.apikey, privacy, safe=''),
|
||||
targets='/'.join([NotifyJoin.quote(x, safe='')
|
||||
for x in self.targets]),
|
||||
args=NotifyJoin.urlencode(args))
|
||||
params=NotifyJoin.urlencode(params))
|
||||
|
||||
@staticmethod
|
||||
def parse_url(url):
|
||||
"""
|
||||
Parses the URL and returns enough arguments that can allow
|
||||
us to substantiate this object.
|
||||
us to re-instantiate this object.
|
||||
|
||||
"""
|
||||
results = NotifyBase.parse_url(url)
|
||||
|
||||
results = NotifyBase.parse_url(url, verify_host=False)
|
||||
if not results:
|
||||
# We're done early as we couldn't load the results
|
||||
return results
|
||||
|
|
|
@ -263,6 +263,7 @@ class NotifyKavenegar(NotifyBase):
|
|||
params=payload,
|
||||
headers=headers,
|
||||
verify=self.verify_certificate,
|
||||
timeout=self.request_timeout,
|
||||
)
|
||||
|
||||
if r.status_code not in (
|
||||
|
@ -310,7 +311,7 @@ class NotifyKavenegar(NotifyBase):
|
|||
|
||||
except requests.RequestException as e:
|
||||
self.logger.warning(
|
||||
'A Connection error occured sending Kavenegar:%s ' % (
|
||||
'A Connection error occurred sending Kavenegar:%s ' % (
|
||||
', '.join(self.targets)) + 'notification.'
|
||||
)
|
||||
self.logger.debug('Socket Exception: %s' % str(e))
|
||||
|
@ -325,30 +326,25 @@ class NotifyKavenegar(NotifyBase):
|
|||
Returns the URL built dynamically based on specified arguments.
|
||||
"""
|
||||
|
||||
# Define any arguments set
|
||||
args = {
|
||||
'format': self.notify_format,
|
||||
'overflow': self.overflow_mode,
|
||||
'verify': 'yes' if self.verify_certificate else 'no',
|
||||
}
|
||||
# Our URL parameters
|
||||
params = self.url_parameters(privacy=privacy, *args, **kwargs)
|
||||
|
||||
return '{schema}://{source}{apikey}/{targets}?{args}'.format(
|
||||
return '{schema}://{source}{apikey}/{targets}?{params}'.format(
|
||||
schema=self.secure_protocol,
|
||||
source='' if not self.source else '{}@'.format(self.source),
|
||||
apikey=self.pprint(self.apikey, privacy, safe=''),
|
||||
targets='/'.join(
|
||||
[NotifyKavenegar.quote(x, safe='') for x in self.targets]),
|
||||
args=NotifyKavenegar.urlencode(args))
|
||||
params=NotifyKavenegar.urlencode(params))
|
||||
|
||||
@staticmethod
|
||||
def parse_url(url):
|
||||
"""
|
||||
Parses the URL and returns enough arguments that can allow
|
||||
us to substantiate this object.
|
||||
us to re-instantiate this object.
|
||||
|
||||
"""
|
||||
results = NotifyBase.parse_url(url, verify_host=False)
|
||||
|
||||
if not results:
|
||||
# We're done early as we couldn't load the results
|
||||
return results
|
||||
|
|
|
@ -163,6 +163,7 @@ class NotifyKumulos(NotifyBase):
|
|||
headers=headers,
|
||||
auth=auth,
|
||||
verify=self.verify_certificate,
|
||||
timeout=self.request_timeout,
|
||||
)
|
||||
if r.status_code != requests.codes.ok:
|
||||
# We had a problem
|
||||
|
@ -187,7 +188,7 @@ class NotifyKumulos(NotifyBase):
|
|||
|
||||
except requests.RequestException as e:
|
||||
self.logger.warning(
|
||||
'A Connection error occured sending Kumulos '
|
||||
'A Connection error occurred sending Kumulos '
|
||||
'notification.')
|
||||
self.logger.debug('Socket Exception: %s' % str(e))
|
||||
|
||||
|
@ -199,29 +200,24 @@ class NotifyKumulos(NotifyBase):
|
|||
Returns the URL built dynamically based on specified arguments.
|
||||
"""
|
||||
|
||||
# Define any arguments set
|
||||
args = {
|
||||
'format': self.notify_format,
|
||||
'overflow': self.overflow_mode,
|
||||
'verify': 'yes' if self.verify_certificate else 'no',
|
||||
}
|
||||
# Our URL parameters
|
||||
params = self.url_parameters(privacy=privacy, *args, **kwargs)
|
||||
|
||||
return '{schema}://{apikey}/{serverkey}/?{args}'.format(
|
||||
return '{schema}://{apikey}/{serverkey}/?{params}'.format(
|
||||
schema=self.secure_protocol,
|
||||
apikey=self.pprint(self.apikey, privacy, safe=''),
|
||||
serverkey=self.pprint(self.serverkey, privacy, safe=''),
|
||||
args=NotifyKumulos.urlencode(args),
|
||||
params=NotifyKumulos.urlencode(params),
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def parse_url(url):
|
||||
"""
|
||||
Parses the URL and returns enough arguments that can allow
|
||||
us to substantiate this object.
|
||||
us to re-instantiate this object.
|
||||
|
||||
"""
|
||||
results = NotifyBase.parse_url(url)
|
||||
|
||||
results = NotifyBase.parse_url(url, verify_host=False)
|
||||
if not results:
|
||||
# We're done early as we couldn't load the results
|
||||
return results
|
||||
|
|
|
@ -276,6 +276,7 @@ class NotifyMSG91(NotifyBase):
|
|||
data=payload,
|
||||
headers=headers,
|
||||
verify=self.verify_certificate,
|
||||
timeout=self.request_timeout,
|
||||
)
|
||||
|
||||
if r.status_code != requests.codes.ok:
|
||||
|
@ -302,7 +303,7 @@ class NotifyMSG91(NotifyBase):
|
|||
|
||||
except requests.RequestException as e:
|
||||
self.logger.warning(
|
||||
'A Connection error occured sending MSG91:%s '
|
||||
'A Connection error occurred sending MSG91:%s '
|
||||
'notification.' % ','.join(self.targets)
|
||||
)
|
||||
self.logger.debug('Socket Exception: %s' % str(e))
|
||||
|
@ -316,34 +317,33 @@ class NotifyMSG91(NotifyBase):
|
|||
Returns the URL built dynamically based on specified arguments.
|
||||
"""
|
||||
|
||||
# Define any arguments set
|
||||
args = {
|
||||
'format': self.notify_format,
|
||||
'overflow': self.overflow_mode,
|
||||
'verify': 'yes' if self.verify_certificate else 'no',
|
||||
# Define any URL parameters
|
||||
params = {
|
||||
'route': str(self.route),
|
||||
}
|
||||
|
||||
if self.country:
|
||||
args['country'] = str(self.country)
|
||||
# Extend our parameters
|
||||
params.update(self.url_parameters(privacy=privacy, *args, **kwargs))
|
||||
|
||||
return '{schema}://{authkey}/{targets}/?{args}'.format(
|
||||
if self.country:
|
||||
params['country'] = str(self.country)
|
||||
|
||||
return '{schema}://{authkey}/{targets}/?{params}'.format(
|
||||
schema=self.secure_protocol,
|
||||
authkey=self.pprint(self.authkey, privacy, safe=''),
|
||||
targets='/'.join(
|
||||
[NotifyMSG91.quote(x, safe='') for x in self.targets]),
|
||||
args=NotifyMSG91.urlencode(args))
|
||||
params=NotifyMSG91.urlencode(params))
|
||||
|
||||
@staticmethod
|
||||
def parse_url(url):
|
||||
"""
|
||||
Parses the URL and returns enough arguments that can allow
|
||||
us to substantiate this object.
|
||||
us to re-instantiate this object.
|
||||
|
||||
"""
|
||||
|
||||
results = NotifyBase.parse_url(url)
|
||||
|
||||
results = NotifyBase.parse_url(url, verify_host=False)
|
||||
if not results:
|
||||
# We're done early as we couldn't load the results
|
||||
return results
|
||||
|
|
|
@ -240,6 +240,7 @@ class NotifyMSTeams(NotifyBase):
|
|||
data=dumps(payload),
|
||||
headers=headers,
|
||||
verify=self.verify_certificate,
|
||||
timeout=self.request_timeout,
|
||||
)
|
||||
if r.status_code != requests.codes.ok:
|
||||
# We had a problem
|
||||
|
@ -264,7 +265,7 @@ class NotifyMSTeams(NotifyBase):
|
|||
|
||||
except requests.RequestException as e:
|
||||
self.logger.warning(
|
||||
'A Connection error occured sending MSTeams notification.')
|
||||
'A Connection error occurred sending MSTeams notification.')
|
||||
self.logger.debug('Socket Exception: %s' % str(e))
|
||||
|
||||
# We failed
|
||||
|
@ -277,32 +278,31 @@ class NotifyMSTeams(NotifyBase):
|
|||
Returns the URL built dynamically based on specified arguments.
|
||||
"""
|
||||
|
||||
# Define any arguments set
|
||||
args = {
|
||||
'format': self.notify_format,
|
||||
'overflow': self.overflow_mode,
|
||||
# Define any URL parameters
|
||||
params = {
|
||||
'image': 'yes' if self.include_image else 'no',
|
||||
'verify': 'yes' if self.verify_certificate else 'no',
|
||||
}
|
||||
|
||||
# Extend our parameters
|
||||
params.update(self.url_parameters(privacy=privacy, *args, **kwargs))
|
||||
|
||||
return '{schema}://{token_a}/{token_b}/{token_c}/'\
|
||||
'?{args}'.format(
|
||||
'?{params}'.format(
|
||||
schema=self.secure_protocol,
|
||||
token_a=self.pprint(self.token_a, privacy, safe=''),
|
||||
token_b=self.pprint(self.token_b, privacy, safe=''),
|
||||
token_c=self.pprint(self.token_c, privacy, safe=''),
|
||||
args=NotifyMSTeams.urlencode(args),
|
||||
params=NotifyMSTeams.urlencode(params),
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def parse_url(url):
|
||||
"""
|
||||
Parses the URL and returns enough arguments that can allow
|
||||
us to substantiate this object.
|
||||
us to re-instantiate this object.
|
||||
|
||||
"""
|
||||
results = NotifyBase.parse_url(url, verify_host=False)
|
||||
|
||||
if not results:
|
||||
# We're done early as we couldn't load the results
|
||||
return results
|
||||
|
@ -359,16 +359,16 @@ class NotifyMSTeams(NotifyBase):
|
|||
r'IncomingWebhook/'
|
||||
r'(?P<token_b>[A-Z0-9]+)/'
|
||||
r'(?P<token_c>[A-Z0-9-]+)/?'
|
||||
r'(?P<args>\?.+)?$', url, re.I)
|
||||
r'(?P<params>\?.+)?$', url, re.I)
|
||||
|
||||
if result:
|
||||
return NotifyMSTeams.parse_url(
|
||||
'{schema}://{token_a}/{token_b}/{token_c}/{args}'.format(
|
||||
'{schema}://{token_a}/{token_b}/{token_c}/{params}'.format(
|
||||
schema=NotifyMSTeams.secure_protocol,
|
||||
token_a=result.group('token_a'),
|
||||
token_b=result.group('token_b'),
|
||||
token_c=result.group('token_c'),
|
||||
args='' if not result.group('args')
|
||||
else result.group('args')))
|
||||
params='' if not result.group('params')
|
||||
else result.group('params')))
|
||||
|
||||
return None
|
||||
|
|
|
@ -269,6 +269,7 @@ class NotifyMailgun(NotifyBase):
|
|||
data=payload,
|
||||
headers=headers,
|
||||
verify=self.verify_certificate,
|
||||
timeout=self.request_timeout,
|
||||
)
|
||||
|
||||
if r.status_code != requests.codes.ok:
|
||||
|
@ -298,7 +299,7 @@ class NotifyMailgun(NotifyBase):
|
|||
|
||||
except requests.RequestException as e:
|
||||
self.logger.warning(
|
||||
'A Connection error occured sending Mailgun:%s ' % (
|
||||
'A Connection error occurred sending Mailgun:%s ' % (
|
||||
email) + 'notification.'
|
||||
)
|
||||
self.logger.debug('Socket Exception: %s' % str(e))
|
||||
|
@ -314,36 +315,35 @@ class NotifyMailgun(NotifyBase):
|
|||
Returns the URL built dynamically based on specified arguments.
|
||||
"""
|
||||
|
||||
# Define any arguments set
|
||||
args = {
|
||||
'format': self.notify_format,
|
||||
'overflow': self.overflow_mode,
|
||||
'verify': 'yes' if self.verify_certificate else 'no',
|
||||
# Define any URL parameters
|
||||
params = {
|
||||
'region': self.region_name,
|
||||
}
|
||||
|
||||
# Extend our parameters
|
||||
params.update(self.url_parameters(privacy=privacy, *args, **kwargs))
|
||||
|
||||
if self.from_name is not None:
|
||||
# from_name specified; pass it back on the url
|
||||
args['name'] = self.from_name
|
||||
params['name'] = self.from_name
|
||||
|
||||
return '{schema}://{user}@{host}/{apikey}/{targets}/?{args}'.format(
|
||||
return '{schema}://{user}@{host}/{apikey}/{targets}/?{params}'.format(
|
||||
schema=self.secure_protocol,
|
||||
host=self.host,
|
||||
user=NotifyMailgun.quote(self.user, safe=''),
|
||||
apikey=self.pprint(self.apikey, privacy, safe=''),
|
||||
targets='/'.join(
|
||||
[NotifyMailgun.quote(x, safe='') for x in self.targets]),
|
||||
args=NotifyMailgun.urlencode(args))
|
||||
params=NotifyMailgun.urlencode(params))
|
||||
|
||||
@staticmethod
|
||||
def parse_url(url):
|
||||
"""
|
||||
Parses the URL and returns enough arguments that can allow
|
||||
us to substantiate this object.
|
||||
us to re-instantiate this object.
|
||||
|
||||
"""
|
||||
results = NotifyBase.parse_url(url)
|
||||
|
||||
results = NotifyBase.parse_url(url, verify_host=False)
|
||||
if not results:
|
||||
# We're done early as we couldn't load the results
|
||||
return results
|
||||
|
|
|
@ -319,6 +319,7 @@ class NotifyMatrix(NotifyBase):
|
|||
data=dumps(payload),
|
||||
headers=headers,
|
||||
verify=self.verify_certificate,
|
||||
timeout=self.request_timeout,
|
||||
)
|
||||
if r.status_code != requests.codes.ok:
|
||||
# We had a problem
|
||||
|
@ -343,7 +344,7 @@ class NotifyMatrix(NotifyBase):
|
|||
|
||||
except requests.RequestException as e:
|
||||
self.logger.warning(
|
||||
'A Connection error occured sending Matrix notification.'
|
||||
'A Connection error occurred sending Matrix notification.'
|
||||
)
|
||||
self.logger.debug('Socket Exception: %s' % str(e))
|
||||
# Return; we're done
|
||||
|
@ -927,6 +928,7 @@ class NotifyMatrix(NotifyBase):
|
|||
params=params,
|
||||
headers=headers,
|
||||
verify=self.verify_certificate,
|
||||
timeout=self.request_timeout,
|
||||
)
|
||||
|
||||
response = loads(r.content)
|
||||
|
@ -986,7 +988,7 @@ class NotifyMatrix(NotifyBase):
|
|||
|
||||
except requests.RequestException as e:
|
||||
self.logger.warning(
|
||||
'A Connection error occured while registering with Matrix'
|
||||
'A Connection error occurred while registering with Matrix'
|
||||
' server.')
|
||||
self.logger.debug('Socket Exception: %s' % str(e))
|
||||
# Return; we're done
|
||||
|
@ -1009,15 +1011,15 @@ class NotifyMatrix(NotifyBase):
|
|||
Returns the URL built dynamically based on specified arguments.
|
||||
"""
|
||||
|
||||
# Define any arguments set
|
||||
args = {
|
||||
'format': self.notify_format,
|
||||
'overflow': self.overflow_mode,
|
||||
# Define any URL parameters
|
||||
params = {
|
||||
'image': 'yes' if self.include_image else 'no',
|
||||
'verify': 'yes' if self.verify_certificate else 'no',
|
||||
'mode': self.mode,
|
||||
}
|
||||
|
||||
# Extend our parameters
|
||||
params.update(self.url_parameters(privacy=privacy, *args, **kwargs))
|
||||
|
||||
# Determine Authentication
|
||||
auth = ''
|
||||
if self.user and self.password:
|
||||
|
@ -1034,21 +1036,21 @@ class NotifyMatrix(NotifyBase):
|
|||
|
||||
default_port = 443 if self.secure else 80
|
||||
|
||||
return '{schema}://{auth}{hostname}{port}/{rooms}?{args}'.format(
|
||||
return '{schema}://{auth}{hostname}{port}/{rooms}?{params}'.format(
|
||||
schema=self.secure_protocol if self.secure else self.protocol,
|
||||
auth=auth,
|
||||
hostname=NotifyMatrix.quote(self.host, safe=''),
|
||||
port='' if self.port is None
|
||||
or self.port == default_port else ':{}'.format(self.port),
|
||||
rooms=NotifyMatrix.quote('/'.join(self.rooms)),
|
||||
args=NotifyMatrix.urlencode(args),
|
||||
params=NotifyMatrix.urlencode(params),
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def parse_url(url):
|
||||
"""
|
||||
Parses the URL and returns enough arguments that can allow
|
||||
us to substantiate this object.
|
||||
us to re-instantiate this object.
|
||||
|
||||
"""
|
||||
results = NotifyBase.parse_url(url, verify_host=False)
|
||||
|
@ -1067,34 +1069,12 @@ class NotifyMatrix(NotifyBase):
|
|||
if 'to' in results['qsd'] and len(results['qsd']['to']):
|
||||
results['targets'] += NotifyMatrix.parse_list(results['qsd']['to'])
|
||||
|
||||
# Thumbnail (old way)
|
||||
if 'thumbnail' in results['qsd']:
|
||||
# Deprication Notice issued for v0.7.5
|
||||
NotifyMatrix.logger.deprecate(
|
||||
'The Matrix URL contains the parameter '
|
||||
'"thumbnail=" which will be deprecated in an upcoming '
|
||||
'release. Please use "image=" instead.'
|
||||
)
|
||||
# Boolean to include an image or not
|
||||
results['include_image'] = parse_bool(results['qsd'].get(
|
||||
'image', NotifyMatrix.template_args['image']['default']))
|
||||
|
||||
# use image= for consistency with the other plugins but we also
|
||||
# support thumbnail= for backwards compatibility.
|
||||
results['include_image'] = \
|
||||
parse_bool(results['qsd'].get(
|
||||
'image', results['qsd'].get('thumbnail', False)))
|
||||
|
||||
# Webhook (old way)
|
||||
if 'webhook' in results['qsd']:
|
||||
# Deprication Notice issued for v0.7.5
|
||||
NotifyMatrix.logger.deprecate(
|
||||
'The Matrix URL contains the parameter '
|
||||
'"webhook=" which will be deprecated in an upcoming '
|
||||
'release. Please use "mode=" instead.'
|
||||
)
|
||||
|
||||
# use mode= for consistency with the other plugins but we also
|
||||
# support webhook= for backwards compatibility.
|
||||
results['mode'] = results['qsd'].get(
|
||||
'mode', results['qsd'].get('webhook'))
|
||||
# Get our mode
|
||||
results['mode'] = results['qsd'].get('mode')
|
||||
|
||||
# t2bot detection... look for just a hostname, and/or just a user/host
|
||||
# if we match this; we can go ahead and set the mode (but only if
|
||||
|
@ -1117,16 +1097,16 @@ class NotifyMatrix(NotifyBase):
|
|||
result = re.match(
|
||||
r'^https?://webhooks\.t2bot\.io/api/v1/matrix/hook/'
|
||||
r'(?P<webhook_token>[A-Z0-9_-]+)/?'
|
||||
r'(?P<args>\?.+)?$', url, re.I)
|
||||
r'(?P<params>\?.+)?$', url, re.I)
|
||||
|
||||
if result:
|
||||
mode = 'mode={}'.format(MatrixWebhookMode.T2BOT)
|
||||
|
||||
return NotifyMatrix.parse_url(
|
||||
'{schema}://{webhook_token}/{args}'.format(
|
||||
'{schema}://{webhook_token}/{params}'.format(
|
||||
schema=NotifyMatrix.secure_protocol,
|
||||
webhook_token=result.group('webhook_token'),
|
||||
args='?{}'.format(mode) if not result.group('args')
|
||||
else '{}&{}'.format(result.group('args'), mode)))
|
||||
params='?{}'.format(mode) if not result.group('params')
|
||||
else '{}&{}'.format(result.group('params'), mode)))
|
||||
|
||||
return None
|
||||
|
|
|
@ -227,6 +227,7 @@ class NotifyMatterMost(NotifyBase):
|
|||
data=dumps(payload),
|
||||
headers=headers,
|
||||
verify=self.verify_certificate,
|
||||
timeout=self.request_timeout,
|
||||
)
|
||||
|
||||
if r.status_code != requests.codes.ok:
|
||||
|
@ -259,7 +260,7 @@ class NotifyMatterMost(NotifyBase):
|
|||
|
||||
except requests.RequestException as e:
|
||||
self.logger.warning(
|
||||
'A Connection error occured sending MatterMost '
|
||||
'A Connection error occurred sending MatterMost '
|
||||
'notification{}.'.format(
|
||||
'' if not channel
|
||||
else ' to channel {}'.format(channel)))
|
||||
|
@ -277,19 +278,19 @@ class NotifyMatterMost(NotifyBase):
|
|||
Returns the URL built dynamically based on specified arguments.
|
||||
"""
|
||||
|
||||
# Define any arguments set
|
||||
args = {
|
||||
'format': self.notify_format,
|
||||
'overflow': self.overflow_mode,
|
||||
# Define any URL parameters
|
||||
params = {
|
||||
'image': 'yes' if self.include_image else 'no',
|
||||
'verify': 'yes' if self.verify_certificate else 'no',
|
||||
}
|
||||
|
||||
# Extend our parameters
|
||||
params.update(self.url_parameters(privacy=privacy, *args, **kwargs))
|
||||
|
||||
if self.channels:
|
||||
# historically the value only accepted one channel and is
|
||||
# therefore identified as 'channel'. Channels have always been
|
||||
# optional, so that is why this setting is nested in an if block
|
||||
args['channel'] = ','.join(self.channels)
|
||||
params['channel'] = ','.join(self.channels)
|
||||
|
||||
default_port = 443 if self.secure else self.default_port
|
||||
default_schema = self.secure_protocol if self.secure else self.protocol
|
||||
|
@ -303,27 +304,28 @@ class NotifyMatterMost(NotifyBase):
|
|||
|
||||
return \
|
||||
'{schema}://{botname}{hostname}{port}{fullpath}{authtoken}' \
|
||||
'/?{args}'.format(
|
||||
'/?{params}'.format(
|
||||
schema=default_schema,
|
||||
botname=botname,
|
||||
hostname=NotifyMatterMost.quote(self.host, safe=''),
|
||||
# never encode hostname since we're expecting it to be a valid
|
||||
# one
|
||||
hostname=self.host,
|
||||
port='' if not self.port or self.port == default_port
|
||||
else ':{}'.format(self.port),
|
||||
fullpath='/' if not self.fullpath else '{}/'.format(
|
||||
NotifyMatterMost.quote(self.fullpath, safe='/')),
|
||||
authtoken=self.pprint(self.authtoken, privacy, safe=''),
|
||||
args=NotifyMatterMost.urlencode(args),
|
||||
params=NotifyMatterMost.urlencode(params),
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def parse_url(url):
|
||||
"""
|
||||
Parses the URL and returns enough arguments that can allow
|
||||
us to substantiate this object.
|
||||
us to re-instantiate this object.
|
||||
|
||||
"""
|
||||
results = NotifyBase.parse_url(url)
|
||||
|
||||
if not results:
|
||||
# We're done early as we couldn't load the results
|
||||
return results
|
||||
|
|
|
@ -234,6 +234,7 @@ class NotifyMessageBird(NotifyBase):
|
|||
data=payload,
|
||||
headers=headers,
|
||||
verify=self.verify_certificate,
|
||||
timeout=self.request_timeout,
|
||||
)
|
||||
|
||||
# Sample output of a successful transmission
|
||||
|
@ -297,7 +298,7 @@ class NotifyMessageBird(NotifyBase):
|
|||
|
||||
except requests.RequestException as e:
|
||||
self.logger.warning(
|
||||
'A Connection error occured sending MessageBird:%s ' % (
|
||||
'A Connection error occurred sending MessageBird:%s ' % (
|
||||
target) + 'notification.'
|
||||
)
|
||||
self.logger.debug('Socket Exception: %s' % str(e))
|
||||
|
@ -313,31 +314,26 @@ class NotifyMessageBird(NotifyBase):
|
|||
Returns the URL built dynamically based on specified arguments.
|
||||
"""
|
||||
|
||||
# Define any arguments set
|
||||
args = {
|
||||
'format': self.notify_format,
|
||||
'overflow': self.overflow_mode,
|
||||
'verify': 'yes' if self.verify_certificate else 'no',
|
||||
}
|
||||
# Our URL parameters
|
||||
params = self.url_parameters(privacy=privacy, *args, **kwargs)
|
||||
|
||||
return '{schema}://{apikey}/{source}/{targets}/?{args}'.format(
|
||||
return '{schema}://{apikey}/{source}/{targets}/?{params}'.format(
|
||||
schema=self.secure_protocol,
|
||||
apikey=self.pprint(self.apikey, privacy, safe=''),
|
||||
source=self.source,
|
||||
targets='/'.join(
|
||||
[NotifyMessageBird.quote(x, safe='') for x in self.targets]),
|
||||
args=NotifyMessageBird.urlencode(args))
|
||||
params=NotifyMessageBird.urlencode(params))
|
||||
|
||||
@staticmethod
|
||||
def parse_url(url):
|
||||
"""
|
||||
Parses the URL and returns enough arguments that can allow
|
||||
us to substantiate this object.
|
||||
us to re-instantiate this object.
|
||||
|
||||
"""
|
||||
|
||||
results = NotifyBase.parse_url(url)
|
||||
|
||||
results = NotifyBase.parse_url(url, verify_host=False)
|
||||
if not results:
|
||||
# We're done early as we couldn't load the results
|
||||
return results
|
||||
|
@ -352,7 +348,7 @@ class NotifyMessageBird(NotifyBase):
|
|||
except IndexError:
|
||||
# No path specified... this URL is potentially un-parseable; we can
|
||||
# hope for a from= entry
|
||||
pass
|
||||
results['source'] = None
|
||||
|
||||
# The hostname is our authentication key
|
||||
results['apikey'] = NotifyMessageBird.unquote(results['host'])
|
||||
|
|
|
@ -82,7 +82,7 @@ class NotifyNexmo(NotifyBase):
|
|||
'name': _('API Key'),
|
||||
'type': 'string',
|
||||
'required': True,
|
||||
'regex': (r'^AC[a-z0-9]{8}$', 'i'),
|
||||
'regex': (r'^[a-z0-9]+$', 'i'),
|
||||
'private': True,
|
||||
},
|
||||
'secret': {
|
||||
|
@ -90,7 +90,7 @@ class NotifyNexmo(NotifyBase):
|
|||
'type': 'string',
|
||||
'private': True,
|
||||
'required': True,
|
||||
'regex': (r'^[a-z0-9]{16}$', 'i'),
|
||||
'regex': (r'^[a-z0-9]+$', 'i'),
|
||||
},
|
||||
'from_phone': {
|
||||
'name': _('From Phone No'),
|
||||
|
@ -280,6 +280,7 @@ class NotifyNexmo(NotifyBase):
|
|||
data=payload,
|
||||
headers=headers,
|
||||
verify=self.verify_certificate,
|
||||
timeout=self.request_timeout,
|
||||
)
|
||||
|
||||
if r.status_code != requests.codes.ok:
|
||||
|
@ -308,7 +309,7 @@ class NotifyNexmo(NotifyBase):
|
|||
|
||||
except requests.RequestException as e:
|
||||
self.logger.warning(
|
||||
'A Connection error occured sending Nexmo:%s '
|
||||
'A Connection error occurred sending Nexmo:%s '
|
||||
'notification.' % target
|
||||
)
|
||||
self.logger.debug('Socket Exception: %s' % str(e))
|
||||
|
@ -324,15 +325,15 @@ class NotifyNexmo(NotifyBase):
|
|||
Returns the URL built dynamically based on specified arguments.
|
||||
"""
|
||||
|
||||
# Define any arguments set
|
||||
args = {
|
||||
'format': self.notify_format,
|
||||
'overflow': self.overflow_mode,
|
||||
'verify': 'yes' if self.verify_certificate else 'no',
|
||||
# Define any URL parameters
|
||||
params = {
|
||||
'ttl': str(self.ttl),
|
||||
}
|
||||
|
||||
return '{schema}://{key}:{secret}@{source}/{targets}/?{args}'.format(
|
||||
# Extend our parameters
|
||||
params.update(self.url_parameters(privacy=privacy, *args, **kwargs))
|
||||
|
||||
return '{schema}://{key}:{secret}@{source}/{targets}/?{params}'.format(
|
||||
schema=self.secure_protocol,
|
||||
key=self.pprint(self.apikey, privacy, safe=''),
|
||||
secret=self.pprint(
|
||||
|
@ -340,17 +341,16 @@ class NotifyNexmo(NotifyBase):
|
|||
source=NotifyNexmo.quote(self.source, safe=''),
|
||||
targets='/'.join(
|
||||
[NotifyNexmo.quote(x, safe='') for x in self.targets]),
|
||||
args=NotifyNexmo.urlencode(args))
|
||||
params=NotifyNexmo.urlencode(params))
|
||||
|
||||
@staticmethod
|
||||
def parse_url(url):
|
||||
"""
|
||||
Parses the URL and returns enough arguments that can allow
|
||||
us to substantiate this object.
|
||||
us to re-instantiate this object.
|
||||
|
||||
"""
|
||||
results = NotifyBase.parse_url(url, verify_host=False)
|
||||
|
||||
if not results:
|
||||
# We're done early as we couldn't load the results
|
||||
return results
|
||||
|
|
|
@ -185,6 +185,7 @@ class NotifyNextcloud(NotifyBase):
|
|||
headers=headers,
|
||||
auth=auth,
|
||||
verify=self.verify_certificate,
|
||||
timeout=self.request_timeout,
|
||||
)
|
||||
if r.status_code != requests.codes.ok:
|
||||
# We had a problem
|
||||
|
@ -210,7 +211,7 @@ class NotifyNextcloud(NotifyBase):
|
|||
|
||||
except requests.RequestException as e:
|
||||
self.logger.warning(
|
||||
'A Connection error occured sending Nextcloud '
|
||||
'A Connection error occurred sending Nextcloud '
|
||||
'notification.',
|
||||
)
|
||||
self.logger.debug('Socket Exception: %s' % str(e))
|
||||
|
@ -226,15 +227,11 @@ class NotifyNextcloud(NotifyBase):
|
|||
Returns the URL built dynamically based on specified arguments.
|
||||
"""
|
||||
|
||||
# Define any arguments set
|
||||
args = {
|
||||
'format': self.notify_format,
|
||||
'overflow': self.overflow_mode,
|
||||
'verify': 'yes' if self.verify_certificate else 'no',
|
||||
}
|
||||
# Create URL parameters from our headers
|
||||
params = {'+{}'.format(k): v for k, v in self.headers.items()}
|
||||
|
||||
# Append our headers into our args
|
||||
args.update({'+{}'.format(k): v for k, v in self.headers.items()})
|
||||
# Our URL parameters
|
||||
params = self.url_parameters(privacy=privacy, *args, **kwargs)
|
||||
|
||||
# Determine Authentication
|
||||
auth = ''
|
||||
|
@ -251,24 +248,26 @@ class NotifyNextcloud(NotifyBase):
|
|||
|
||||
default_port = 443 if self.secure else 80
|
||||
|
||||
return '{schema}://{auth}{hostname}{port}/{targets}?{args}' \
|
||||
return '{schema}://{auth}{hostname}{port}/{targets}?{params}' \
|
||||
.format(
|
||||
schema=self.secure_protocol
|
||||
if self.secure else self.protocol,
|
||||
auth=auth,
|
||||
hostname=NotifyNextcloud.quote(self.host, safe=''),
|
||||
# never encode hostname since we're expecting it to be a
|
||||
# valid one
|
||||
hostname=self.host,
|
||||
port='' if self.port is None or self.port == default_port
|
||||
else ':{}'.format(self.port),
|
||||
targets='/'.join([NotifyNextcloud.quote(x)
|
||||
for x in self.targets]),
|
||||
args=NotifyNextcloud.urlencode(args),
|
||||
params=NotifyNextcloud.urlencode(params),
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def parse_url(url):
|
||||
"""
|
||||
Parses the URL and returns enough arguments that can allow
|
||||
us to substantiate this object.
|
||||
us to re-instantiate this object.
|
||||
|
||||
"""
|
||||
|
||||
|
|
|
@ -103,6 +103,14 @@ class NotifyNotica(NotifyBase):
|
|||
'{schema}://{user}@{host}:{port}/{token}',
|
||||
'{schema}://{user}:{password}@{host}/{token}',
|
||||
'{schema}://{user}:{password}@{host}:{port}/{token}',
|
||||
|
||||
# Self-hosted notica servers (with custom path)
|
||||
'{schema}://{host}{path}{token}',
|
||||
'{schema}://{host}:{port}{path}{token}',
|
||||
'{schema}://{user}@{host}{path}{token}',
|
||||
'{schema}://{user}@{host}:{port}{path}{token}',
|
||||
'{schema}://{user}:{password}@{host}{path}{token}',
|
||||
'{schema}://{user}:{password}@{host}:{port}{path}{token}',
|
||||
)
|
||||
|
||||
# Define our template tokens
|
||||
|
@ -133,6 +141,12 @@ class NotifyNotica(NotifyBase):
|
|||
'type': 'string',
|
||||
'private': True,
|
||||
},
|
||||
'path': {
|
||||
'name': _('Path'),
|
||||
'type': 'string',
|
||||
'map_to': 'fullpath',
|
||||
'default': '/',
|
||||
},
|
||||
})
|
||||
|
||||
# Define any kwargs we're using
|
||||
|
@ -228,6 +242,7 @@ class NotifyNotica(NotifyBase):
|
|||
headers=headers,
|
||||
auth=auth,
|
||||
verify=self.verify_certificate,
|
||||
timeout=self.request_timeout,
|
||||
)
|
||||
if r.status_code != requests.codes.ok:
|
||||
# We had a problem
|
||||
|
@ -251,7 +266,7 @@ class NotifyNotica(NotifyBase):
|
|||
|
||||
except requests.RequestException as e:
|
||||
self.logger.warning(
|
||||
'A Connection error occured sending Notica notification.',
|
||||
'A Connection error occurred sending Notica notification.',
|
||||
)
|
||||
self.logger.debug('Socket Exception: %s' % str(e))
|
||||
|
||||
|
@ -265,25 +280,21 @@ class NotifyNotica(NotifyBase):
|
|||
Returns the URL built dynamically based on specified arguments.
|
||||
"""
|
||||
|
||||
# Define any arguments set
|
||||
args = {
|
||||
'format': self.notify_format,
|
||||
'overflow': self.overflow_mode,
|
||||
'verify': 'yes' if self.verify_certificate else 'no',
|
||||
}
|
||||
# Our URL parameters
|
||||
params = self.url_parameters(privacy=privacy, *args, **kwargs)
|
||||
|
||||
if self.mode == NoticaMode.OFFICIAL:
|
||||
# Official URLs are easy to assemble
|
||||
return '{schema}://{token}/?{args}'.format(
|
||||
return '{schema}://{token}/?{params}'.format(
|
||||
schema=self.protocol,
|
||||
token=self.pprint(self.token, privacy, safe=''),
|
||||
args=NotifyNotica.urlencode(args),
|
||||
params=NotifyNotica.urlencode(params),
|
||||
)
|
||||
|
||||
# If we reach here then we are assembling a self hosted URL
|
||||
|
||||
# Append our headers into our args
|
||||
args.update({'+{}'.format(k): v for k, v in self.headers.items()})
|
||||
# Append URL parameters from our headers
|
||||
params.update({'+{}'.format(k): v for k, v in self.headers.items()})
|
||||
|
||||
# Authorization can be used for self-hosted sollutions
|
||||
auth = ''
|
||||
|
@ -302,7 +313,7 @@ class NotifyNotica(NotifyBase):
|
|||
|
||||
default_port = 443 if self.secure else 80
|
||||
|
||||
return '{schema}://{auth}{hostname}{port}{fullpath}{token}/?{args}' \
|
||||
return '{schema}://{auth}{hostname}{port}{fullpath}{token}/?{params}' \
|
||||
.format(
|
||||
schema=self.secure_protocol
|
||||
if self.secure else self.protocol,
|
||||
|
@ -313,14 +324,14 @@ class NotifyNotica(NotifyBase):
|
|||
fullpath=NotifyNotica.quote(
|
||||
self.fullpath, safe='/'),
|
||||
token=self.pprint(self.token, privacy, safe=''),
|
||||
args=NotifyNotica.urlencode(args),
|
||||
params=NotifyNotica.urlencode(params),
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def parse_url(url):
|
||||
"""
|
||||
Parses the URL and returns enough arguments that can allow
|
||||
us to substantiate this object.
|
||||
us to re-instantiate this object.
|
||||
|
||||
"""
|
||||
results = NotifyBase.parse_url(url, verify_host=False)
|
||||
|
@ -367,14 +378,14 @@ class NotifyNotica(NotifyBase):
|
|||
|
||||
result = re.match(
|
||||
r'^https?://notica\.us/?'
|
||||
r'\??(?P<token>[^&]+)([&\s]*(?P<args>.+))?$', url, re.I)
|
||||
r'\??(?P<token>[^&]+)([&\s]*(?P<params>.+))?$', url, re.I)
|
||||
|
||||
if result:
|
||||
return NotifyNotica.parse_url(
|
||||
'{schema}://{token}/{args}'.format(
|
||||
'{schema}://{token}/{params}'.format(
|
||||
schema=NotifyNotica.protocol,
|
||||
token=result.group('token'),
|
||||
args='' if not result.group('args')
|
||||
else '?{}'.format(result.group('args'))))
|
||||
params='' if not result.group('params')
|
||||
else '?{}'.format(result.group('params'))))
|
||||
|
||||
return None
|
||||
|
|
|
@ -199,20 +199,20 @@ class NotifyNotifico(NotifyBase):
|
|||
Returns the URL built dynamically based on specified arguments.
|
||||
"""
|
||||
|
||||
# Define any arguments set
|
||||
args = {
|
||||
'format': self.notify_format,
|
||||
'overflow': self.overflow_mode,
|
||||
'verify': 'yes' if self.verify_certificate else 'no',
|
||||
# Define any URL parameters
|
||||
params = {
|
||||
'color': 'yes' if self.color else 'no',
|
||||
'prefix': 'yes' if self.prefix else 'no',
|
||||
}
|
||||
|
||||
return '{schema}://{proj}/{hook}/?{args}'.format(
|
||||
# Extend our parameters
|
||||
params.update(self.url_parameters(privacy=privacy, *args, **kwargs))
|
||||
|
||||
return '{schema}://{proj}/{hook}/?{params}'.format(
|
||||
schema=self.secure_protocol,
|
||||
proj=self.pprint(self.project_id, privacy, safe=''),
|
||||
hook=self.pprint(self.msghook, privacy, safe=''),
|
||||
args=NotifyNotifico.urlencode(args),
|
||||
params=NotifyNotifico.urlencode(params),
|
||||
)
|
||||
|
||||
def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs):
|
||||
|
@ -288,6 +288,7 @@ class NotifyNotifico(NotifyBase):
|
|||
params=payload,
|
||||
headers=headers,
|
||||
verify=self.verify_certificate,
|
||||
timeout=self.request_timeout,
|
||||
)
|
||||
if r.status_code != requests.codes.ok:
|
||||
# We had a problem
|
||||
|
@ -311,7 +312,7 @@ class NotifyNotifico(NotifyBase):
|
|||
|
||||
except requests.RequestException as e:
|
||||
self.logger.warning(
|
||||
'A Connection error occured sending Notifico '
|
||||
'A Connection error occurred sending Notifico '
|
||||
'notification.')
|
||||
self.logger.debug('Socket Exception: %s' % str(e))
|
||||
|
||||
|
@ -324,11 +325,11 @@ class NotifyNotifico(NotifyBase):
|
|||
def parse_url(url):
|
||||
"""
|
||||
Parses the URL and returns enough arguments that can allow
|
||||
us to substantiate this object.
|
||||
us to re-instantiate this object.
|
||||
|
||||
"""
|
||||
|
||||
results = NotifyBase.parse_url(url)
|
||||
results = NotifyBase.parse_url(url, verify_host=False)
|
||||
if not results:
|
||||
# We're done early as we couldn't load the results
|
||||
return results
|
||||
|
@ -364,15 +365,15 @@ class NotifyNotifico(NotifyBase):
|
|||
r'^https?://n\.tkte\.ch/h/'
|
||||
r'(?P<proj>[0-9]+)/'
|
||||
r'(?P<hook>[A-Z0-9]+)/?'
|
||||
r'(?P<args>\?.+)?$', url, re.I)
|
||||
r'(?P<params>\?.+)?$', url, re.I)
|
||||
|
||||
if result:
|
||||
return NotifyNotifico.parse_url(
|
||||
'{schema}://{proj}/{hook}/{args}'.format(
|
||||
'{schema}://{proj}/{hook}/{params}'.format(
|
||||
schema=NotifyNotifico.secure_protocol,
|
||||
proj=result.group('proj'),
|
||||
hook=result.group('hook'),
|
||||
args='' if not result.group('args')
|
||||
else result.group('args')))
|
||||
params='' if not result.group('params')
|
||||
else result.group('params')))
|
||||
|
||||
return None
|
||||
|
|
|
@ -191,6 +191,7 @@ class NotifyProwl(NotifyBase):
|
|||
data=payload,
|
||||
headers=headers,
|
||||
verify=self.verify_certificate,
|
||||
timeout=self.request_timeout,
|
||||
)
|
||||
if r.status_code != requests.codes.ok:
|
||||
# We had a problem
|
||||
|
@ -215,7 +216,7 @@ class NotifyProwl(NotifyBase):
|
|||
|
||||
except requests.RequestException as e:
|
||||
self.logger.warning(
|
||||
'A Connection error occured sending Prowl notification.')
|
||||
'A Connection error occurred sending Prowl notification.')
|
||||
self.logger.debug('Socket Exception: %s' % str(e))
|
||||
|
||||
# Return; we're done
|
||||
|
@ -236,31 +237,30 @@ class NotifyProwl(NotifyBase):
|
|||
ProwlPriority.EMERGENCY: 'emergency',
|
||||
}
|
||||
|
||||
# Define any arguments set
|
||||
args = {
|
||||
'format': self.notify_format,
|
||||
'overflow': self.overflow_mode,
|
||||
# Define any URL parameters
|
||||
params = {
|
||||
'priority': 'normal' if self.priority not in _map
|
||||
else _map[self.priority],
|
||||
'verify': 'yes' if self.verify_certificate else 'no',
|
||||
}
|
||||
|
||||
return '{schema}://{apikey}/{providerkey}/?{args}'.format(
|
||||
# Extend our parameters
|
||||
params.update(self.url_parameters(privacy=privacy, *args, **kwargs))
|
||||
|
||||
return '{schema}://{apikey}/{providerkey}/?{params}'.format(
|
||||
schema=self.secure_protocol,
|
||||
apikey=self.pprint(self.apikey, privacy, safe=''),
|
||||
providerkey=self.pprint(self.providerkey, privacy, safe=''),
|
||||
args=NotifyProwl.urlencode(args),
|
||||
params=NotifyProwl.urlencode(params),
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def parse_url(url):
|
||||
"""
|
||||
Parses the URL and returns enough arguments that can allow
|
||||
us to substantiate this object.
|
||||
us to re-instantiate this object.
|
||||
|
||||
"""
|
||||
results = NotifyBase.parse_url(url)
|
||||
|
||||
results = NotifyBase.parse_url(url, verify_host=False)
|
||||
if not results:
|
||||
# We're done early as we couldn't load the results
|
||||
return results
|
||||
|
|
|
@ -28,7 +28,7 @@ from json import dumps
|
|||
from json import loads
|
||||
|
||||
from .NotifyBase import NotifyBase
|
||||
from ..utils import GET_EMAIL_RE
|
||||
from ..utils import is_email
|
||||
from ..common import NotifyType
|
||||
from ..utils import parse_list
|
||||
from ..utils import validate_regex
|
||||
|
@ -230,22 +230,29 @@ class NotifyPushBullet(NotifyBase):
|
|||
'body': body,
|
||||
}
|
||||
|
||||
if recipient is PUSHBULLET_SEND_TO_ALL:
|
||||
# Check if an email was defined
|
||||
match = is_email(recipient)
|
||||
if match:
|
||||
payload['email'] = match['full_email']
|
||||
self.logger.debug(
|
||||
"PushBullet recipient {} parsed as an email address"
|
||||
.format(recipient))
|
||||
|
||||
elif recipient is PUSHBULLET_SEND_TO_ALL:
|
||||
# Send to all
|
||||
pass
|
||||
|
||||
elif GET_EMAIL_RE.match(recipient):
|
||||
payload['email'] = recipient
|
||||
self.logger.debug(
|
||||
"Recipient '%s' is an email address" % recipient)
|
||||
|
||||
elif recipient[0] == '#':
|
||||
payload['channel_tag'] = recipient[1:]
|
||||
self.logger.debug("Recipient '%s' is a channel" % recipient)
|
||||
self.logger.debug(
|
||||
"PushBullet recipient {} parsed as a channel"
|
||||
.format(recipient))
|
||||
|
||||
else:
|
||||
payload['device_iden'] = recipient
|
||||
self.logger.debug("Recipient '%s' is a device" % recipient)
|
||||
self.logger.debug(
|
||||
"PushBullet recipient {} parsed as a device"
|
||||
.format(recipient))
|
||||
|
||||
okay, response = self._send(
|
||||
self.notify_url.format('pushes'), payload)
|
||||
|
@ -315,6 +322,7 @@ class NotifyPushBullet(NotifyBase):
|
|||
files=files,
|
||||
auth=auth,
|
||||
verify=self.verify_certificate,
|
||||
timeout=self.request_timeout,
|
||||
)
|
||||
|
||||
try:
|
||||
|
@ -352,14 +360,14 @@ class NotifyPushBullet(NotifyBase):
|
|||
|
||||
except requests.RequestException as e:
|
||||
self.logger.warning(
|
||||
'A Connection error occured communicating with PushBullet.')
|
||||
'A Connection error occurred communicating with PushBullet.')
|
||||
self.logger.debug('Socket Exception: %s' % str(e))
|
||||
|
||||
return False, response
|
||||
|
||||
except (OSError, IOError) as e:
|
||||
self.logger.warning(
|
||||
'An I/O error occured while reading {}.'.format(
|
||||
'An I/O error occurred while reading {}.'.format(
|
||||
payload.name if payload else 'attachment'))
|
||||
self.logger.debug('I/O Exception: %s' % str(e))
|
||||
return False, response
|
||||
|
@ -375,12 +383,8 @@ class NotifyPushBullet(NotifyBase):
|
|||
Returns the URL built dynamically based on specified arguments.
|
||||
"""
|
||||
|
||||
# Define any arguments set
|
||||
args = {
|
||||
'format': self.notify_format,
|
||||
'overflow': self.overflow_mode,
|
||||
'verify': 'yes' if self.verify_certificate else 'no',
|
||||
}
|
||||
# Our URL parameters
|
||||
params = self.url_parameters(privacy=privacy, *args, **kwargs)
|
||||
|
||||
targets = '/'.join([NotifyPushBullet.quote(x) for x in self.targets])
|
||||
if targets == PUSHBULLET_SEND_TO_ALL:
|
||||
|
@ -388,21 +392,20 @@ class NotifyPushBullet(NotifyBase):
|
|||
# it from the recipients list
|
||||
targets = ''
|
||||
|
||||
return '{schema}://{accesstoken}/{targets}/?{args}'.format(
|
||||
return '{schema}://{accesstoken}/{targets}/?{params}'.format(
|
||||
schema=self.secure_protocol,
|
||||
accesstoken=self.pprint(self.accesstoken, privacy, safe=''),
|
||||
targets=targets,
|
||||
args=NotifyPushBullet.urlencode(args))
|
||||
params=NotifyPushBullet.urlencode(params))
|
||||
|
||||
@staticmethod
|
||||
def parse_url(url):
|
||||
"""
|
||||
Parses the URL and returns enough arguments that can allow
|
||||
us to substantiate this object.
|
||||
us to re-instantiate this object.
|
||||
|
||||
"""
|
||||
results = NotifyBase.parse_url(url)
|
||||
|
||||
results = NotifyBase.parse_url(url, verify_host=False)
|
||||
if not results:
|
||||
# We're done early as we couldn't load the results
|
||||
return results
|
||||
|
|
|
@ -576,7 +576,7 @@ class NotifyPushSafer(NotifyBase):
|
|||
|
||||
except (OSError, IOError) as e:
|
||||
self.logger.warning(
|
||||
'An I/O error occured while reading {}.'.format(
|
||||
'An I/O error occurred while reading {}.'.format(
|
||||
attachment.name if attachment else 'attachment'))
|
||||
self.logger.debug('I/O Exception: %s' % str(e))
|
||||
return False
|
||||
|
@ -693,6 +693,7 @@ class NotifyPushSafer(NotifyBase):
|
|||
data=payload,
|
||||
headers=headers,
|
||||
verify=self.verify_certificate,
|
||||
timeout=self.request_timeout,
|
||||
)
|
||||
|
||||
try:
|
||||
|
@ -746,7 +747,7 @@ class NotifyPushSafer(NotifyBase):
|
|||
|
||||
except requests.RequestException as e:
|
||||
self.logger.warning(
|
||||
'A Connection error occured communicating with PushSafer.')
|
||||
'A Connection error occurred communicating with PushSafer.')
|
||||
self.logger.debug('Socket Exception: %s' % str(e))
|
||||
|
||||
return False, response
|
||||
|
@ -756,29 +757,25 @@ class NotifyPushSafer(NotifyBase):
|
|||
Returns the URL built dynamically based on specified arguments.
|
||||
"""
|
||||
|
||||
# Define any arguments set
|
||||
args = {
|
||||
'format': self.notify_format,
|
||||
'overflow': self.overflow_mode,
|
||||
'verify': 'yes' if self.verify_certificate else 'no',
|
||||
}
|
||||
# Our URL parameters
|
||||
params = self.url_parameters(privacy=privacy, *args, **kwargs)
|
||||
|
||||
if self.priority is not None:
|
||||
# Store our priority; but only if it was specified
|
||||
args['priority'] = \
|
||||
params['priority'] = \
|
||||
next((key for key, value in PUSHSAFER_PRIORITY_MAP.items()
|
||||
if value == self.priority),
|
||||
DEFAULT_PRIORITY) # pragma: no cover
|
||||
|
||||
if self.sound is not None:
|
||||
# Store our sound; but only if it was specified
|
||||
args['sound'] = \
|
||||
params['sound'] = \
|
||||
next((key for key, value in PUSHSAFER_SOUND_MAP.items()
|
||||
if value == self.sound), '') # pragma: no cover
|
||||
|
||||
if self.vibration is not None:
|
||||
# Store our vibration; but only if it was specified
|
||||
args['vibration'] = str(self.vibration)
|
||||
params['vibration'] = str(self.vibration)
|
||||
|
||||
targets = '/'.join([NotifyPushSafer.quote(x) for x in self.targets])
|
||||
if targets == PUSHSAFER_SEND_TO_ALL:
|
||||
|
@ -786,20 +783,20 @@ class NotifyPushSafer(NotifyBase):
|
|||
# it from the recipients list
|
||||
targets = ''
|
||||
|
||||
return '{schema}://{privatekey}/{targets}?{args}'.format(
|
||||
return '{schema}://{privatekey}/{targets}?{params}'.format(
|
||||
schema=self.secure_protocol if self.secure else self.protocol,
|
||||
privatekey=self.pprint(self.privatekey, privacy, safe=''),
|
||||
targets=targets,
|
||||
args=NotifyPushSafer.urlencode(args))
|
||||
params=NotifyPushSafer.urlencode(params))
|
||||
|
||||
@staticmethod
|
||||
def parse_url(url):
|
||||
"""
|
||||
Parses the URL and returns enough arguments that can allow
|
||||
us to substantiate this object.
|
||||
us to re-instantiate this object.
|
||||
|
||||
"""
|
||||
results = NotifyBase.parse_url(url)
|
||||
results = NotifyBase.parse_url(url, verify_host=False)
|
||||
if not results:
|
||||
# We're done early as we couldn't load the results
|
||||
return results
|
||||
|
|
|
@ -267,6 +267,7 @@ class NotifyPushed(NotifyBase):
|
|||
data=dumps(payload),
|
||||
headers=headers,
|
||||
verify=self.verify_certificate,
|
||||
timeout=self.request_timeout,
|
||||
)
|
||||
|
||||
if r.status_code != requests.codes.ok:
|
||||
|
@ -291,7 +292,7 @@ class NotifyPushed(NotifyBase):
|
|||
|
||||
except requests.RequestException as e:
|
||||
self.logger.warning(
|
||||
'A Connection error occured sending Pushed notification.')
|
||||
'A Connection error occurred sending Pushed notification.')
|
||||
self.logger.debug('Socket Exception: %s' % str(e))
|
||||
|
||||
# Return; we're done
|
||||
|
@ -304,14 +305,10 @@ class NotifyPushed(NotifyBase):
|
|||
Returns the URL built dynamically based on specified arguments.
|
||||
"""
|
||||
|
||||
# Define any arguments set
|
||||
args = {
|
||||
'format': self.notify_format,
|
||||
'overflow': self.overflow_mode,
|
||||
'verify': 'yes' if self.verify_certificate else 'no',
|
||||
}
|
||||
# Our URL parameters
|
||||
params = self.url_parameters(privacy=privacy, *args, **kwargs)
|
||||
|
||||
return '{schema}://{app_key}/{app_secret}/{targets}/?{args}'.format(
|
||||
return '{schema}://{app_key}/{app_secret}/{targets}/?{params}'.format(
|
||||
schema=self.secure_protocol,
|
||||
app_key=self.pprint(self.app_key, privacy, safe=''),
|
||||
app_secret=self.pprint(
|
||||
|
@ -323,17 +320,16 @@ class NotifyPushed(NotifyBase):
|
|||
# Users are prefixed with an @ symbol
|
||||
['@{}'.format(x) for x in self.users],
|
||||
)]),
|
||||
args=NotifyPushed.urlencode(args))
|
||||
params=NotifyPushed.urlencode(params))
|
||||
|
||||
@staticmethod
|
||||
def parse_url(url):
|
||||
"""
|
||||
Parses the URL and returns enough arguments that can allow
|
||||
us to substantiate this object.
|
||||
us to re-instantiate this object.
|
||||
|
||||
"""
|
||||
results = NotifyBase.parse_url(url)
|
||||
|
||||
results = NotifyBase.parse_url(url, verify_host=False)
|
||||
if not results:
|
||||
# We're done early as we couldn't load the results
|
||||
return results
|
||||
|
|
|
@ -60,10 +60,6 @@ class NotifyPushjet(NotifyBase):
|
|||
'{schema}://{host}/{secret_key}',
|
||||
'{schema}://{user}:{password}@{host}:{port}/{secret_key}',
|
||||
'{schema}://{user}:{password}@{host}/{secret_key}',
|
||||
|
||||
# Kept for backwards compatibility; will be depricated eventually
|
||||
'{schema}://{secret_key}@{host}',
|
||||
'{schema}://{secret_key}@{host}:{port}',
|
||||
)
|
||||
|
||||
# Define our tokens
|
||||
|
@ -123,12 +119,8 @@ class NotifyPushjet(NotifyBase):
|
|||
Returns the URL built dynamically based on specified arguments.
|
||||
"""
|
||||
|
||||
# Define any arguments set
|
||||
args = {
|
||||
'format': self.notify_format,
|
||||
'overflow': self.overflow_mode,
|
||||
'verify': 'yes' if self.verify_certificate else 'no',
|
||||
}
|
||||
# Our URL parameters
|
||||
params = self.url_parameters(privacy=privacy, *args, **kwargs)
|
||||
|
||||
default_port = 443 if self.secure else 80
|
||||
|
||||
|
@ -141,15 +133,16 @@ class NotifyPushjet(NotifyBase):
|
|||
self.password, privacy, mode=PrivacyMode.Secret, safe=''),
|
||||
)
|
||||
|
||||
return '{schema}://{auth}{hostname}{port}/{secret}/?{args}'.format(
|
||||
return '{schema}://{auth}{hostname}{port}/{secret}/?{params}'.format(
|
||||
schema=self.secure_protocol if self.secure else self.protocol,
|
||||
auth=auth,
|
||||
hostname=NotifyPushjet.quote(self.host, safe=''),
|
||||
# never encode hostname since we're expecting it to be a valid one
|
||||
hostname=self.host,
|
||||
port='' if self.port is None or self.port == default_port
|
||||
else ':{}'.format(self.port),
|
||||
secret=self.pprint(
|
||||
self.secret_key, privacy, mode=PrivacyMode.Secret, safe=''),
|
||||
args=NotifyPushjet.urlencode(args),
|
||||
params=NotifyPushjet.urlencode(params),
|
||||
)
|
||||
|
||||
def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs):
|
||||
|
@ -199,6 +192,7 @@ class NotifyPushjet(NotifyBase):
|
|||
headers=headers,
|
||||
auth=auth,
|
||||
verify=self.verify_certificate,
|
||||
timeout=self.request_timeout,
|
||||
)
|
||||
if r.status_code != requests.codes.ok:
|
||||
# We had a problem
|
||||
|
@ -222,7 +216,7 @@ class NotifyPushjet(NotifyBase):
|
|||
|
||||
except requests.RequestException as e:
|
||||
self.logger.warning(
|
||||
'A Connection error occured sending Pushjet '
|
||||
'A Connection error occurred sending Pushjet '
|
||||
'notification to %s.' % self.host)
|
||||
self.logger.debug('Socket Exception: %s' % str(e))
|
||||
|
||||
|
@ -235,7 +229,7 @@ class NotifyPushjet(NotifyBase):
|
|||
def parse_url(url):
|
||||
"""
|
||||
Parses the URL and returns enough arguments that can allow
|
||||
us to substantiate this object.
|
||||
us to re-instantiate this object.
|
||||
|
||||
Syntax:
|
||||
pjet://hostname/secret_key
|
||||
|
@ -246,16 +240,8 @@ class NotifyPushjet(NotifyBase):
|
|||
pjets://hostname:port/secret_key
|
||||
pjets://user:pass@hostname/secret_key
|
||||
pjets://user:pass@hostname:port/secret_key
|
||||
|
||||
Legacy (Depricated) Syntax:
|
||||
pjet://secret_key@hostname
|
||||
pjet://secret_key@hostname:port
|
||||
pjets://secret_key@hostname
|
||||
pjets://secret_key@hostname:port
|
||||
|
||||
"""
|
||||
results = NotifyBase.parse_url(url)
|
||||
|
||||
if not results:
|
||||
# We're done early as we couldn't load the results
|
||||
return results
|
||||
|
@ -276,22 +262,4 @@ class NotifyPushjet(NotifyBase):
|
|||
results['secret_key'] = \
|
||||
NotifyPushjet.unquote(results['qsd']['secret'])
|
||||
|
||||
if results.get('secret_key') is None:
|
||||
# Deprication Notice issued for v0.7.9
|
||||
NotifyPushjet.logger.deprecate(
|
||||
'The Pushjet URL contains secret_key in the user field'
|
||||
' which will be deprecated in an upcoming '
|
||||
'release. Please place this in the path of the URL instead.'
|
||||
)
|
||||
|
||||
# Store it as it's value based on the user field
|
||||
results['secret_key'] = \
|
||||
NotifyPushjet.unquote(results.get('user'))
|
||||
|
||||
# there is no way http-auth is enabled, be sure to unset the
|
||||
# current defined user (if present). This is done due to some
|
||||
# logic that takes place in the send() since we support http-auth.
|
||||
results['user'] = None
|
||||
results['password'] = None
|
||||
|
||||
return results
|
||||
|
|
|
@ -434,6 +434,7 @@ class NotifyPushover(NotifyBase):
|
|||
files=files,
|
||||
auth=auth,
|
||||
verify=self.verify_certificate,
|
||||
timeout=self.request_timeout,
|
||||
)
|
||||
|
||||
if r.status_code != requests.codes.ok:
|
||||
|
@ -461,7 +462,7 @@ class NotifyPushover(NotifyBase):
|
|||
|
||||
except requests.RequestException as e:
|
||||
self.logger.warning(
|
||||
'A Connection error occured sending Pushover:%s ' % (
|
||||
'A Connection error occurred sending Pushover:%s ' % (
|
||||
payload['device']) + 'notification.'
|
||||
)
|
||||
self.logger.debug('Socket Exception: %s' % str(e))
|
||||
|
@ -470,7 +471,7 @@ class NotifyPushover(NotifyBase):
|
|||
|
||||
except (OSError, IOError) as e:
|
||||
self.logger.warning(
|
||||
'An I/O error occured while reading {}.'.format(
|
||||
'An I/O error occurred while reading {}.'.format(
|
||||
attach.name if attach else 'attachment'))
|
||||
self.logger.debug('I/O Exception: %s' % str(e))
|
||||
return False
|
||||
|
@ -496,19 +497,20 @@ class NotifyPushover(NotifyBase):
|
|||
PushoverPriority.EMERGENCY: 'emergency',
|
||||
}
|
||||
|
||||
# Define any arguments set
|
||||
args = {
|
||||
'format': self.notify_format,
|
||||
'overflow': self.overflow_mode,
|
||||
# Define any URL parameters
|
||||
params = {
|
||||
'priority':
|
||||
_map[self.template_args['priority']['default']]
|
||||
if self.priority not in _map else _map[self.priority],
|
||||
'verify': 'yes' if self.verify_certificate else 'no',
|
||||
}
|
||||
|
||||
# Only add expire and retry for emergency messages,
|
||||
# pushover ignores for all other priorities
|
||||
if self.priority == PushoverPriority.EMERGENCY:
|
||||
args.update({'expire': self.expire, 'retry': self.retry})
|
||||
params.update({'expire': self.expire, 'retry': self.retry})
|
||||
|
||||
# Extend our parameters
|
||||
params.update(self.url_parameters(privacy=privacy, *args, **kwargs))
|
||||
|
||||
# Escape our devices
|
||||
devices = '/'.join([NotifyPushover.quote(x, safe='')
|
||||
|
@ -519,22 +521,21 @@ class NotifyPushover(NotifyBase):
|
|||
# it from the devices list
|
||||
devices = ''
|
||||
|
||||
return '{schema}://{user_key}@{token}/{devices}/?{args}'.format(
|
||||
return '{schema}://{user_key}@{token}/{devices}/?{params}'.format(
|
||||
schema=self.secure_protocol,
|
||||
user_key=self.pprint(self.user_key, privacy, safe=''),
|
||||
token=self.pprint(self.token, privacy, safe=''),
|
||||
devices=devices,
|
||||
args=NotifyPushover.urlencode(args))
|
||||
params=NotifyPushover.urlencode(params))
|
||||
|
||||
@staticmethod
|
||||
def parse_url(url):
|
||||
"""
|
||||
Parses the URL and returns enough arguments that can allow
|
||||
us to substantiate this object.
|
||||
us to re-instantiate this object.
|
||||
|
||||
"""
|
||||
results = NotifyBase.parse_url(url)
|
||||
|
||||
results = NotifyBase.parse_url(url, verify_host=False)
|
||||
if not results:
|
||||
# We're done early as we couldn't load the results
|
||||
return results
|
||||
|
|
|
@ -285,15 +285,15 @@ class NotifyRocketChat(NotifyBase):
|
|||
Returns the URL built dynamically based on specified arguments.
|
||||
"""
|
||||
|
||||
# Define any arguments set
|
||||
args = {
|
||||
'format': self.notify_format,
|
||||
'overflow': self.overflow_mode,
|
||||
'verify': 'yes' if self.verify_certificate else 'no',
|
||||
# Define any URL parameters
|
||||
params = {
|
||||
'avatar': 'yes' if self.avatar else 'no',
|
||||
'mode': self.mode,
|
||||
}
|
||||
|
||||
# Extend our parameters
|
||||
params.update(self.url_parameters(privacy=privacy, *args, **kwargs))
|
||||
|
||||
# Determine Authentication
|
||||
if self.mode == RocketChatAuthMode.BASIC:
|
||||
auth = '{user}:{password}@'.format(
|
||||
|
@ -310,10 +310,11 @@ class NotifyRocketChat(NotifyBase):
|
|||
|
||||
default_port = 443 if self.secure else 80
|
||||
|
||||
return '{schema}://{auth}{hostname}{port}/{targets}/?{args}'.format(
|
||||
return '{schema}://{auth}{hostname}{port}/{targets}/?{params}'.format(
|
||||
schema=self.secure_protocol if self.secure else self.protocol,
|
||||
auth=auth,
|
||||
hostname=NotifyRocketChat.quote(self.host, safe=''),
|
||||
# never encode hostname since we're expecting it to be a valid one
|
||||
hostname=self.host,
|
||||
port='' if self.port is None or self.port == default_port
|
||||
else ':{}'.format(self.port),
|
||||
targets='/'.join(
|
||||
|
@ -325,7 +326,7 @@ class NotifyRocketChat(NotifyBase):
|
|||
# Users
|
||||
['@{}'.format(x) for x in self.users],
|
||||
)]),
|
||||
args=NotifyRocketChat.urlencode(args),
|
||||
params=NotifyRocketChat.urlencode(params),
|
||||
)
|
||||
|
||||
def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs):
|
||||
|
@ -476,6 +477,7 @@ class NotifyRocketChat(NotifyBase):
|
|||
data=payload,
|
||||
headers=headers,
|
||||
verify=self.verify_certificate,
|
||||
timeout=self.request_timeout,
|
||||
)
|
||||
if r.status_code != requests.codes.ok:
|
||||
# We had a problem
|
||||
|
@ -502,7 +504,7 @@ class NotifyRocketChat(NotifyBase):
|
|||
|
||||
except requests.RequestException as e:
|
||||
self.logger.warning(
|
||||
'A Connection error occured sending Rocket.Chat '
|
||||
'A Connection error occurred sending Rocket.Chat '
|
||||
'{}:notification.'.format(self.mode))
|
||||
self.logger.debug('Socket Exception: %s' % str(e))
|
||||
|
||||
|
@ -529,6 +531,7 @@ class NotifyRocketChat(NotifyBase):
|
|||
api_url,
|
||||
data=payload,
|
||||
verify=self.verify_certificate,
|
||||
timeout=self.request_timeout,
|
||||
)
|
||||
if r.status_code != requests.codes.ok:
|
||||
# We had a problem
|
||||
|
@ -570,13 +573,13 @@ class NotifyRocketChat(NotifyBase):
|
|||
# - TypeError = r.content is None
|
||||
# - AttributeError = r is None
|
||||
self.logger.warning(
|
||||
'A commuication error occured authenticating {} on '
|
||||
'A commuication error occurred authenticating {} on '
|
||||
'Rocket.Chat.'.format(self.user))
|
||||
return False
|
||||
|
||||
except requests.RequestException as e:
|
||||
self.logger.warning(
|
||||
'A connection error occured authenticating {} on '
|
||||
'A connection error occurred authenticating {} on '
|
||||
'Rocket.Chat.'.format(self.user))
|
||||
self.logger.debug('Socket Exception: %s' % str(e))
|
||||
return False
|
||||
|
@ -595,6 +598,7 @@ class NotifyRocketChat(NotifyBase):
|
|||
api_url,
|
||||
headers=self.headers,
|
||||
verify=self.verify_certificate,
|
||||
timeout=self.request_timeout,
|
||||
)
|
||||
if r.status_code != requests.codes.ok:
|
||||
# We had a problem
|
||||
|
@ -622,7 +626,7 @@ class NotifyRocketChat(NotifyBase):
|
|||
|
||||
except requests.RequestException as e:
|
||||
self.logger.warning(
|
||||
'A Connection error occured logging off the '
|
||||
'A Connection error occurred logging off the '
|
||||
'Rocket.Chat server')
|
||||
self.logger.debug('Socket Exception: %s' % str(e))
|
||||
return False
|
||||
|
@ -633,7 +637,7 @@ class NotifyRocketChat(NotifyBase):
|
|||
def parse_url(url):
|
||||
"""
|
||||
Parses the URL and returns enough arguments that can allow
|
||||
us to substantiate this object.
|
||||
us to re-instantiate this object.
|
||||
|
||||
"""
|
||||
|
||||
|
@ -665,7 +669,6 @@ class NotifyRocketChat(NotifyBase):
|
|||
)
|
||||
|
||||
results = NotifyBase.parse_url(url)
|
||||
|
||||
if not results:
|
||||
# We're done early as we couldn't load the results
|
||||
return results
|
||||
|
|
|
@ -236,6 +236,7 @@ class NotifyRyver(NotifyBase):
|
|||
data=dumps(payload),
|
||||
headers=headers,
|
||||
verify=self.verify_certificate,
|
||||
timeout=self.request_timeout,
|
||||
)
|
||||
|
||||
if r.status_code != requests.codes.ok:
|
||||
|
@ -260,7 +261,7 @@ class NotifyRyver(NotifyBase):
|
|||
|
||||
except requests.RequestException as e:
|
||||
self.logger.warning(
|
||||
'A Connection error occured sending Ryver:%s ' % (
|
||||
'A Connection error occurred sending Ryver:%s ' % (
|
||||
self.organization) + 'notification.'
|
||||
)
|
||||
self.logger.debug('Socket Exception: %s' % str(e))
|
||||
|
@ -273,15 +274,15 @@ class NotifyRyver(NotifyBase):
|
|||
Returns the URL built dynamically based on specified arguments.
|
||||
"""
|
||||
|
||||
# Define any arguments set
|
||||
args = {
|
||||
'format': self.notify_format,
|
||||
'overflow': self.overflow_mode,
|
||||
# Define any URL parameters
|
||||
params = {
|
||||
'image': 'yes' if self.include_image else 'no',
|
||||
'mode': self.mode,
|
||||
'verify': 'yes' if self.verify_certificate else 'no',
|
||||
}
|
||||
|
||||
# Extend our parameters
|
||||
params.update(self.url_parameters(privacy=privacy, *args, **kwargs))
|
||||
|
||||
# Determine if there is a botname present
|
||||
botname = ''
|
||||
if self.user:
|
||||
|
@ -289,24 +290,23 @@ class NotifyRyver(NotifyBase):
|
|||
botname=NotifyRyver.quote(self.user, safe=''),
|
||||
)
|
||||
|
||||
return '{schema}://{botname}{organization}/{token}/?{args}'.format(
|
||||
return '{schema}://{botname}{organization}/{token}/?{params}'.format(
|
||||
schema=self.secure_protocol,
|
||||
botname=botname,
|
||||
organization=NotifyRyver.quote(self.organization, safe=''),
|
||||
token=self.pprint(self.token, privacy, safe=''),
|
||||
args=NotifyRyver.urlencode(args),
|
||||
params=NotifyRyver.urlencode(params),
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def parse_url(url):
|
||||
"""
|
||||
Parses the URL and returns enough arguments that can allow
|
||||
us to substantiate this object.
|
||||
us to re-instantiate this object.
|
||||
|
||||
"""
|
||||
|
||||
results = NotifyBase.parse_url(url)
|
||||
|
||||
results = NotifyBase.parse_url(url, verify_host=False)
|
||||
if not results:
|
||||
# We're done early as we couldn't load the results
|
||||
return results
|
||||
|
@ -323,19 +323,8 @@ class NotifyRyver(NotifyBase):
|
|||
# no token
|
||||
results['token'] = None
|
||||
|
||||
if 'webhook' in results['qsd']:
|
||||
# Deprication Notice issued for v0.7.5
|
||||
NotifyRyver.logger.deprecate(
|
||||
'The Ryver URL contains the parameter '
|
||||
'"webhook=" which will be deprecated in an upcoming '
|
||||
'release. Please use "mode=" instead.'
|
||||
)
|
||||
|
||||
# use mode= for consistency with the other plugins but we also
|
||||
# support webhook= for backwards compatibility.
|
||||
results['mode'] = results['qsd'].get(
|
||||
'mode', results['qsd'].get(
|
||||
'webhook', RyverWebhookMode.RYVER))
|
||||
# Retrieve the mode
|
||||
results['mode'] = results['qsd'].get('mode', RyverWebhookMode.RYVER)
|
||||
|
||||
# use image= for consistency with the other plugins
|
||||
results['include_image'] = \
|
||||
|
@ -352,15 +341,15 @@ class NotifyRyver(NotifyBase):
|
|||
result = re.match(
|
||||
r'^https?://(?P<org>[A-Z0-9_-]+)\.ryver\.com/application/webhook/'
|
||||
r'(?P<webhook_token>[A-Z0-9]+)/?'
|
||||
r'(?P<args>\?.+)?$', url, re.I)
|
||||
r'(?P<params>\?.+)?$', url, re.I)
|
||||
|
||||
if result:
|
||||
return NotifyRyver.parse_url(
|
||||
'{schema}://{org}/{webhook_token}/{args}'.format(
|
||||
'{schema}://{org}/{webhook_token}/{params}'.format(
|
||||
schema=NotifyRyver.secure_protocol,
|
||||
org=result.group('org'),
|
||||
webhook_token=result.group('webhook_token'),
|
||||
args='' if not result.group('args')
|
||||
else result.group('args')))
|
||||
params='' if not result.group('params')
|
||||
else result.group('params')))
|
||||
|
||||
return None
|
||||
|
|
|
@ -342,6 +342,7 @@ class NotifySNS(NotifyBase):
|
|||
data=payload,
|
||||
headers=headers,
|
||||
verify=self.verify_certificate,
|
||||
timeout=self.request_timeout,
|
||||
)
|
||||
|
||||
if r.status_code != requests.codes.ok:
|
||||
|
@ -368,7 +369,7 @@ class NotifySNS(NotifyBase):
|
|||
|
||||
except requests.RequestException as e:
|
||||
self.logger.warning(
|
||||
'A Connection error occured sending AWS '
|
||||
'A Connection error occurred sending AWS '
|
||||
'notification to "%s".' % (to),
|
||||
)
|
||||
self.logger.debug('Socket Exception: %s' % str(e))
|
||||
|
@ -579,15 +580,11 @@ class NotifySNS(NotifyBase):
|
|||
Returns the URL built dynamically based on specified arguments.
|
||||
"""
|
||||
|
||||
# Define any arguments set
|
||||
args = {
|
||||
'format': self.notify_format,
|
||||
'overflow': self.overflow_mode,
|
||||
'verify': 'yes' if self.verify_certificate else 'no',
|
||||
}
|
||||
# Our URL parameters
|
||||
params = self.url_parameters(privacy=privacy, *args, **kwargs)
|
||||
|
||||
return '{schema}://{key_id}/{key_secret}/{region}/{targets}/'\
|
||||
'?{args}'.format(
|
||||
'?{params}'.format(
|
||||
schema=self.secure_protocol,
|
||||
key_id=self.pprint(self.aws_access_key_id, privacy, safe=''),
|
||||
key_secret=self.pprint(
|
||||
|
@ -601,18 +598,17 @@ class NotifySNS(NotifyBase):
|
|||
# Topics are prefixed with a pound/hashtag symbol
|
||||
['#{}'.format(x) for x in self.topics],
|
||||
)]),
|
||||
args=NotifySNS.urlencode(args),
|
||||
params=NotifySNS.urlencode(params),
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def parse_url(url):
|
||||
"""
|
||||
Parses the URL and returns enough arguments that can allow
|
||||
us to substantiate this object.
|
||||
us to re-instantiate this object.
|
||||
|
||||
"""
|
||||
results = NotifyBase.parse_url(url)
|
||||
|
||||
results = NotifyBase.parse_url(url, verify_host=False)
|
||||
if not results:
|
||||
# We're done early as we couldn't load the results
|
||||
return results
|
||||
|
|
|
@ -50,7 +50,7 @@ from .NotifyBase import NotifyBase
|
|||
from ..common import NotifyFormat
|
||||
from ..common import NotifyType
|
||||
from ..utils import parse_list
|
||||
from ..utils import GET_EMAIL_RE
|
||||
from ..utils import is_email
|
||||
from ..utils import validate_regex
|
||||
from ..AppriseLocale import gettext_lazy as _
|
||||
|
||||
|
@ -170,18 +170,15 @@ class NotifySendGrid(NotifyBase):
|
|||
self.logger.warning(msg)
|
||||
raise TypeError(msg)
|
||||
|
||||
self.from_email = from_email
|
||||
try:
|
||||
result = GET_EMAIL_RE.match(self.from_email)
|
||||
if not result:
|
||||
# let outer exception handle this
|
||||
raise TypeError
|
||||
|
||||
except (TypeError, AttributeError):
|
||||
msg = 'Invalid ~From~ email specified: {}'.format(self.from_email)
|
||||
result = is_email(from_email)
|
||||
if not result:
|
||||
msg = 'Invalid ~From~ email specified: {}'.format(from_email)
|
||||
self.logger.warning(msg)
|
||||
raise TypeError(msg)
|
||||
|
||||
# Store email address
|
||||
self.from_email = result['full_email']
|
||||
|
||||
# Acquire Targets (To Emails)
|
||||
self.targets = list()
|
||||
|
||||
|
@ -201,8 +198,9 @@ class NotifySendGrid(NotifyBase):
|
|||
# Validate recipients (to:) and drop bad ones:
|
||||
for recipient in parse_list(targets):
|
||||
|
||||
if GET_EMAIL_RE.match(recipient):
|
||||
self.targets.append(recipient)
|
||||
result = is_email(recipient)
|
||||
if result:
|
||||
self.targets.append(result['full_email'])
|
||||
continue
|
||||
|
||||
self.logger.warning(
|
||||
|
@ -213,8 +211,9 @@ class NotifySendGrid(NotifyBase):
|
|||
# Validate recipients (cc:) and drop bad ones:
|
||||
for recipient in parse_list(cc):
|
||||
|
||||
if GET_EMAIL_RE.match(recipient):
|
||||
self.cc.add(recipient)
|
||||
result = is_email(recipient)
|
||||
if result:
|
||||
self.cc.add(result['full_email'])
|
||||
continue
|
||||
|
||||
self.logger.warning(
|
||||
|
@ -225,8 +224,9 @@ class NotifySendGrid(NotifyBase):
|
|||
# Validate recipients (bcc:) and drop bad ones:
|
||||
for recipient in parse_list(bcc):
|
||||
|
||||
if GET_EMAIL_RE.match(recipient):
|
||||
self.bcc.add(recipient)
|
||||
result = is_email(recipient)
|
||||
if result:
|
||||
self.bcc.add(result['full_email'])
|
||||
continue
|
||||
|
||||
self.logger.warning(
|
||||
|
@ -245,41 +245,38 @@ class NotifySendGrid(NotifyBase):
|
|||
Returns the URL built dynamically based on specified arguments.
|
||||
"""
|
||||
|
||||
# Define any arguments set
|
||||
args = {
|
||||
'format': self.notify_format,
|
||||
'overflow': self.overflow_mode,
|
||||
'verify': 'yes' if self.verify_certificate else 'no',
|
||||
}
|
||||
# Our URL parameters
|
||||
params = self.url_parameters(privacy=privacy, *args, **kwargs)
|
||||
|
||||
if len(self.cc) > 0:
|
||||
# Handle our Carbon Copy Addresses
|
||||
args['cc'] = ','.join(self.cc)
|
||||
params['cc'] = ','.join(self.cc)
|
||||
|
||||
if len(self.bcc) > 0:
|
||||
# Handle our Blind Carbon Copy Addresses
|
||||
args['bcc'] = ','.join(self.bcc)
|
||||
params['bcc'] = ','.join(self.bcc)
|
||||
|
||||
if self.template:
|
||||
# Handle our Template ID if if was specified
|
||||
args['template'] = self.template
|
||||
params['template'] = self.template
|
||||
|
||||
# Append our template_data into our args
|
||||
args.update({'+{}'.format(k): v
|
||||
for k, v in self.template_data.items()})
|
||||
# Append our template_data into our parameter list
|
||||
params.update(
|
||||
{'+{}'.format(k): v for k, v in self.template_data.items()})
|
||||
|
||||
# a simple boolean check as to whether we display our target emails
|
||||
# or not
|
||||
has_targets = \
|
||||
not (len(self.targets) == 1 and self.targets[0] == self.from_email)
|
||||
|
||||
return '{schema}://{apikey}:{from_email}/{targets}?{args}'.format(
|
||||
return '{schema}://{apikey}:{from_email}/{targets}?{params}'.format(
|
||||
schema=self.secure_protocol,
|
||||
apikey=self.pprint(self.apikey, privacy, safe=''),
|
||||
from_email=self.quote(self.from_email, safe='@'),
|
||||
# never encode email since it plays a huge role in our hostname
|
||||
from_email=self.from_email,
|
||||
targets='' if not has_targets else '/'.join(
|
||||
[NotifySendGrid.quote(x, safe='') for x in self.targets]),
|
||||
args=NotifySendGrid.urlencode(args),
|
||||
params=NotifySendGrid.urlencode(params),
|
||||
)
|
||||
|
||||
def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs):
|
||||
|
@ -361,6 +358,7 @@ class NotifySendGrid(NotifyBase):
|
|||
data=dumps(payload),
|
||||
headers=headers,
|
||||
verify=self.verify_certificate,
|
||||
timeout=self.request_timeout,
|
||||
)
|
||||
if r.status_code not in (
|
||||
requests.codes.ok, requests.codes.accepted):
|
||||
|
@ -390,7 +388,7 @@ class NotifySendGrid(NotifyBase):
|
|||
|
||||
except requests.RequestException as e:
|
||||
self.logger.warning(
|
||||
'A Connection error occured sending SendGrid '
|
||||
'A Connection error occurred sending SendGrid '
|
||||
'notification to {}.'.format(target))
|
||||
self.logger.debug('Socket Exception: %s' % str(e))
|
||||
|
||||
|
@ -404,7 +402,7 @@ class NotifySendGrid(NotifyBase):
|
|||
def parse_url(url):
|
||||
"""
|
||||
Parses the URL and returns enough arguments that can allow
|
||||
us to substantiate this object.
|
||||
us to re-instantiate this object.
|
||||
|
||||
"""
|
||||
|
||||
|
|
|
@ -142,14 +142,6 @@ class NotifySimplePush(NotifyBase):
|
|||
# Default Event Name
|
||||
self.event = None
|
||||
|
||||
# Encrypt Message (providing support is available)
|
||||
if self.password and self.user and not CRYPTOGRAPHY_AVAILABLE:
|
||||
# Provide the end user at least some notification that they're
|
||||
# not getting what they asked for
|
||||
self.logger.warning(
|
||||
'SimplePush extended encryption is not supported by this '
|
||||
'system.')
|
||||
|
||||
# Used/cached in _encrypt() function
|
||||
self._iv = None
|
||||
self._iv_hex = None
|
||||
|
@ -189,6 +181,15 @@ class NotifySimplePush(NotifyBase):
|
|||
Perform SimplePush Notification
|
||||
"""
|
||||
|
||||
# Encrypt Message (providing support is available)
|
||||
if self.password and self.user and not CRYPTOGRAPHY_AVAILABLE:
|
||||
# Provide the end user at least some notification that they're
|
||||
# not getting what they asked for
|
||||
self.logger.warning(
|
||||
"Authenticated SimplePush Notifications are not supported by "
|
||||
"this system; `pip install cryptography`.")
|
||||
return False
|
||||
|
||||
headers = {
|
||||
'User-Agent': self.app_id,
|
||||
'Content-type': "application/x-www-form-urlencoded",
|
||||
|
@ -236,6 +237,7 @@ class NotifySimplePush(NotifyBase):
|
|||
data=payload,
|
||||
headers=headers,
|
||||
verify=self.verify_certificate,
|
||||
timeout=self.request_timeout,
|
||||
)
|
||||
|
||||
# Get our SimplePush response (if it's possible)
|
||||
|
@ -272,7 +274,7 @@ class NotifySimplePush(NotifyBase):
|
|||
|
||||
except requests.RequestException as e:
|
||||
self.logger.warning(
|
||||
'A Connection error occured sending SimplePush notification.')
|
||||
'A Connection error occurred sending SimplePush notification.')
|
||||
self.logger.debug('Socket Exception: %s' % str(e))
|
||||
|
||||
# Return; we're done
|
||||
|
@ -285,15 +287,11 @@ class NotifySimplePush(NotifyBase):
|
|||
Returns the URL built dynamically based on specified arguments.
|
||||
"""
|
||||
|
||||
# Define any arguments set
|
||||
args = {
|
||||
'format': self.notify_format,
|
||||
'overflow': self.overflow_mode,
|
||||
'verify': 'yes' if self.verify_certificate else 'no',
|
||||
}
|
||||
# Our URL parameters
|
||||
params = self.url_parameters(privacy=privacy, *args, **kwargs)
|
||||
|
||||
if self.event:
|
||||
args['event'] = self.event
|
||||
params['event'] = self.event
|
||||
|
||||
# Determine Authentication
|
||||
auth = ''
|
||||
|
@ -305,21 +303,21 @@ class NotifySimplePush(NotifyBase):
|
|||
self.password, privacy, mode=PrivacyMode.Secret, safe=''),
|
||||
)
|
||||
|
||||
return '{schema}://{auth}{apikey}/?{args}'.format(
|
||||
return '{schema}://{auth}{apikey}/?{params}'.format(
|
||||
schema=self.secure_protocol,
|
||||
auth=auth,
|
||||
apikey=self.pprint(self.apikey, privacy, safe=''),
|
||||
args=NotifySimplePush.urlencode(args),
|
||||
params=NotifySimplePush.urlencode(params),
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def parse_url(url):
|
||||
"""
|
||||
Parses the URL and returns enough arguments that can allow
|
||||
us to substantiate this object.
|
||||
us to re-instantiate this object.
|
||||
|
||||
"""
|
||||
results = NotifyBase.parse_url(url)
|
||||
results = NotifyBase.parse_url(url, verify_host=False)
|
||||
if not results:
|
||||
# We're done early as we couldn't load the results
|
||||
return results
|
||||
|
|
|
@ -322,6 +322,7 @@ class NotifySinch(NotifyBase):
|
|||
data=json.dumps(payload),
|
||||
headers=headers,
|
||||
verify=self.verify_certificate,
|
||||
timeout=self.request_timeout,
|
||||
)
|
||||
|
||||
# The responsne might look like:
|
||||
|
@ -383,7 +384,7 @@ class NotifySinch(NotifyBase):
|
|||
|
||||
except requests.RequestException as e:
|
||||
self.logger.warning(
|
||||
'A Connection error occured sending Sinch:%s ' % (
|
||||
'A Connection error occurred sending Sinch:%s ' % (
|
||||
target) + 'notification.'
|
||||
)
|
||||
self.logger.debug('Socket Exception: %s' % str(e))
|
||||
|
@ -399,15 +400,15 @@ class NotifySinch(NotifyBase):
|
|||
Returns the URL built dynamically based on specified arguments.
|
||||
"""
|
||||
|
||||
# Define any arguments set
|
||||
args = {
|
||||
'format': self.notify_format,
|
||||
'overflow': self.overflow_mode,
|
||||
'verify': 'yes' if self.verify_certificate else 'no',
|
||||
# Define any URL parameters
|
||||
params = {
|
||||
'region': self.region,
|
||||
}
|
||||
|
||||
return '{schema}://{spi}:{token}@{source}/{targets}/?{args}'.format(
|
||||
# Extend our parameters
|
||||
params.update(self.url_parameters(privacy=privacy, *args, **kwargs))
|
||||
|
||||
return '{schema}://{spi}:{token}@{source}/{targets}/?{params}'.format(
|
||||
schema=self.secure_protocol,
|
||||
spi=self.pprint(
|
||||
self.service_plan_id, privacy, mode=PrivacyMode.Tail, safe=''),
|
||||
|
@ -415,13 +416,13 @@ class NotifySinch(NotifyBase):
|
|||
source=NotifySinch.quote(self.source, safe=''),
|
||||
targets='/'.join(
|
||||
[NotifySinch.quote(x, safe='') for x in self.targets]),
|
||||
args=NotifySinch.urlencode(args))
|
||||
params=NotifySinch.urlencode(params))
|
||||
|
||||
@staticmethod
|
||||
def parse_url(url):
|
||||
"""
|
||||
Parses the URL and returns enough arguments that can allow
|
||||
us to substantiate this object.
|
||||
us to re-instantiate this object.
|
||||
|
||||
"""
|
||||
results = NotifyBase.parse_url(url, verify_host=False)
|
||||
|
|
|
@ -505,6 +505,7 @@ class NotifySlack(NotifyBase):
|
|||
headers=headers,
|
||||
files=files,
|
||||
verify=self.verify_certificate,
|
||||
timeout=self.request_timeout,
|
||||
)
|
||||
|
||||
if r.status_code != requests.codes.ok:
|
||||
|
@ -622,14 +623,14 @@ class NotifySlack(NotifyBase):
|
|||
# }
|
||||
except requests.RequestException as e:
|
||||
self.logger.warning(
|
||||
'A Connection error occured posting {}to Slack.'.format(
|
||||
'A Connection error occurred posting {}to Slack.'.format(
|
||||
attach.name if attach else ''))
|
||||
self.logger.debug('Socket Exception: %s' % str(e))
|
||||
return False
|
||||
|
||||
except (OSError, IOError) as e:
|
||||
self.logger.warning(
|
||||
'An I/O error occured while reading {}.'.format(
|
||||
'An I/O error occurred while reading {}.'.format(
|
||||
attach.name if attach else 'attachment'))
|
||||
self.logger.debug('I/O Exception: %s' % str(e))
|
||||
return False
|
||||
|
@ -648,15 +649,15 @@ class NotifySlack(NotifyBase):
|
|||
Returns the URL built dynamically based on specified arguments.
|
||||
"""
|
||||
|
||||
# Define any arguments set
|
||||
args = {
|
||||
'format': self.notify_format,
|
||||
'overflow': self.overflow_mode,
|
||||
# Define any URL parameters
|
||||
params = {
|
||||
'image': 'yes' if self.include_image else 'no',
|
||||
'footer': 'yes' if self.include_footer else 'no',
|
||||
'verify': 'yes' if self.verify_certificate else 'no',
|
||||
}
|
||||
|
||||
# Extend our parameters
|
||||
params.update(self.url_parameters(privacy=privacy, *args, **kwargs))
|
||||
|
||||
if self.mode == SlackMode.WEBHOOK:
|
||||
# Determine if there is a botname present
|
||||
botname = ''
|
||||
|
@ -666,7 +667,7 @@ class NotifySlack(NotifyBase):
|
|||
)
|
||||
|
||||
return '{schema}://{botname}{token_a}/{token_b}/{token_c}/'\
|
||||
'{targets}/?{args}'.format(
|
||||
'{targets}/?{params}'.format(
|
||||
schema=self.secure_protocol,
|
||||
botname=botname,
|
||||
token_a=self.pprint(self.token_a, privacy, safe=''),
|
||||
|
@ -675,23 +676,23 @@ class NotifySlack(NotifyBase):
|
|||
targets='/'.join(
|
||||
[NotifySlack.quote(x, safe='')
|
||||
for x in self.channels]),
|
||||
args=NotifySlack.urlencode(args),
|
||||
params=NotifySlack.urlencode(params),
|
||||
)
|
||||
# else -> self.mode == SlackMode.BOT:
|
||||
return '{schema}://{access_token}/{targets}/'\
|
||||
'?{args}'.format(
|
||||
'?{params}'.format(
|
||||
schema=self.secure_protocol,
|
||||
access_token=self.pprint(self.access_token, privacy, safe=''),
|
||||
targets='/'.join(
|
||||
[NotifySlack.quote(x, safe='') for x in self.channels]),
|
||||
args=NotifySlack.urlencode(args),
|
||||
params=NotifySlack.urlencode(params),
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def parse_url(url):
|
||||
"""
|
||||
Parses the URL and returns enough arguments that can allow
|
||||
us to substantiate this object.
|
||||
us to re-instantiate this object.
|
||||
|
||||
"""
|
||||
results = NotifyBase.parse_url(url, verify_host=False)
|
||||
|
@ -760,16 +761,16 @@ class NotifySlack(NotifyBase):
|
|||
r'(?P<token_a>[A-Z0-9]+)/'
|
||||
r'(?P<token_b>[A-Z0-9]+)/'
|
||||
r'(?P<token_c>[A-Z0-9]+)/?'
|
||||
r'(?P<args>\?.+)?$', url, re.I)
|
||||
r'(?P<params>\?.+)?$', url, re.I)
|
||||
|
||||
if result:
|
||||
return NotifySlack.parse_url(
|
||||
'{schema}://{token_a}/{token_b}/{token_c}/{args}'.format(
|
||||
'{schema}://{token_a}/{token_b}/{token_c}/{params}'.format(
|
||||
schema=NotifySlack.secure_protocol,
|
||||
token_a=result.group('token_a'),
|
||||
token_b=result.group('token_b'),
|
||||
token_c=result.group('token_c'),
|
||||
args='' if not result.group('args')
|
||||
else result.group('args')))
|
||||
params='' if not result.group('params')
|
||||
else result.group('params')))
|
||||
|
||||
return None
|
||||
|
|
|
@ -233,32 +233,33 @@ class NotifySyslog(NotifyBase):
|
|||
Returns the URL built dynamically based on specified arguments.
|
||||
"""
|
||||
|
||||
# Define any arguments set
|
||||
args = {
|
||||
# Define any URL parameters
|
||||
params = {
|
||||
'logperror': 'yes' if self.log_perror else 'no',
|
||||
'logpid': 'yes' if self.log_pid else 'no',
|
||||
'format': self.notify_format,
|
||||
'overflow': self.overflow_mode,
|
||||
'verify': 'yes' if self.verify_certificate else 'no',
|
||||
}
|
||||
|
||||
return '{schema}://{facility}/?{args}'.format(
|
||||
# Extend our parameters
|
||||
params.update(self.url_parameters(privacy=privacy, *args, **kwargs))
|
||||
|
||||
return '{schema}://{facility}/?{params}'.format(
|
||||
facility=self.template_tokens['facility']['default']
|
||||
if self.facility not in SYSLOG_FACILITY_RMAP
|
||||
else SYSLOG_FACILITY_RMAP[self.facility],
|
||||
schema=self.secure_protocol,
|
||||
args=NotifySyslog.urlencode(args),
|
||||
params=NotifySyslog.urlencode(params),
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def parse_url(url):
|
||||
"""
|
||||
Parses the URL and returns enough arguments that can allow
|
||||
us to substantiate this object.
|
||||
us to re-instantiate this object.
|
||||
|
||||
"""
|
||||
results = NotifyBase.parse_url(url, verify_host=False)
|
||||
if not results:
|
||||
# We're done early as we couldn't load the results
|
||||
return results
|
||||
|
||||
# if specified; save hostname into facility
|
||||
|
|
|
@ -145,6 +145,7 @@ class NotifyTechulusPush(NotifyBase):
|
|||
data=dumps(payload),
|
||||
headers=headers,
|
||||
verify=self.verify_certificate,
|
||||
timeout=self.request_timeout,
|
||||
)
|
||||
if r.status_code not in (
|
||||
requests.codes.ok, requests.codes.no_content):
|
||||
|
@ -171,7 +172,7 @@ class NotifyTechulusPush(NotifyBase):
|
|||
|
||||
except requests.RequestException as e:
|
||||
self.logger.warning(
|
||||
'A Connection error occured sending Techulus Push '
|
||||
'A Connection error occurred sending Techulus Push '
|
||||
'notification.'
|
||||
)
|
||||
self.logger.debug('Socket Exception: %s' % str(e))
|
||||
|
@ -185,28 +186,23 @@ class NotifyTechulusPush(NotifyBase):
|
|||
Returns the URL built dynamically based on specified arguments.
|
||||
"""
|
||||
|
||||
# Define any arguments set
|
||||
args = {
|
||||
'format': self.notify_format,
|
||||
'overflow': self.overflow_mode,
|
||||
'verify': 'yes' if self.verify_certificate else 'no',
|
||||
}
|
||||
# Our URL parameters
|
||||
params = self.url_parameters(privacy=privacy, *args, **kwargs)
|
||||
|
||||
return '{schema}://{apikey}/?{args}'.format(
|
||||
return '{schema}://{apikey}/?{params}'.format(
|
||||
schema=self.secure_protocol,
|
||||
apikey=self.pprint(self.apikey, privacy, safe=''),
|
||||
args=NotifyTechulusPush.urlencode(args),
|
||||
params=NotifyTechulusPush.urlencode(params),
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def parse_url(url):
|
||||
"""
|
||||
Parses the URL and returns enough arguments that can allow
|
||||
us to substantiate this object.
|
||||
us to re-instantiate this object.
|
||||
|
||||
"""
|
||||
results = NotifyBase.parse_url(url, verify_host=False)
|
||||
|
||||
if not results:
|
||||
# We're done early as we couldn't load the results
|
||||
return results
|
||||
|
|
|
@ -229,23 +229,19 @@ class NotifyTelegram(NotifyBase):
|
|||
# Parse our list
|
||||
self.targets = parse_list(targets)
|
||||
|
||||
# if detect_owner is set to True, we will attempt to determine who
|
||||
# the bot owner is based on the first person who messaged it. This
|
||||
# is not a fool proof way of doing things as over time Telegram removes
|
||||
# the message history for the bot. So what appears (later on) to be
|
||||
# the first message to it, maybe another user who sent it a message
|
||||
# much later. Users who set this flag should update their Apprise
|
||||
# URL later to directly include the user that we should message.
|
||||
self.detect_owner = detect_owner
|
||||
|
||||
if self.user:
|
||||
# Treat this as a channel too
|
||||
self.targets.append(self.user)
|
||||
|
||||
if len(self.targets) == 0 and self.detect_owner:
|
||||
_id = self.detect_bot_owner()
|
||||
if _id:
|
||||
# Store our id
|
||||
self.targets.append(str(_id))
|
||||
|
||||
if len(self.targets) == 0:
|
||||
err = 'No chat_id(s) were specified.'
|
||||
self.logger.warning(err)
|
||||
raise TypeError(err)
|
||||
|
||||
# Track whether or not we want to send an image with our notification
|
||||
# or not.
|
||||
self.include_image = include_image
|
||||
|
@ -325,6 +321,7 @@ class NotifyTelegram(NotifyBase):
|
|||
files=files,
|
||||
data=payload,
|
||||
verify=self.verify_certificate,
|
||||
timeout=self.request_timeout,
|
||||
)
|
||||
|
||||
if r.status_code != requests.codes.ok:
|
||||
|
@ -349,7 +346,7 @@ class NotifyTelegram(NotifyBase):
|
|||
|
||||
except requests.RequestException as e:
|
||||
self.logger.warning(
|
||||
'A connection error occured posting Telegram '
|
||||
'A connection error occurred posting Telegram '
|
||||
'attachment.')
|
||||
self.logger.debug('Socket Exception: %s' % str(e))
|
||||
|
||||
|
@ -393,6 +390,7 @@ class NotifyTelegram(NotifyBase):
|
|||
url,
|
||||
headers=headers,
|
||||
verify=self.verify_certificate,
|
||||
timeout=self.request_timeout,
|
||||
)
|
||||
|
||||
if r.status_code != requests.codes.ok:
|
||||
|
@ -436,12 +434,12 @@ class NotifyTelegram(NotifyBase):
|
|||
# - TypeError = r.content is None
|
||||
# - AttributeError = r is None
|
||||
self.logger.warning(
|
||||
'A communication error occured detecting the Telegram User.')
|
||||
'A communication error occurred detecting the Telegram User.')
|
||||
return 0
|
||||
|
||||
except requests.RequestException as e:
|
||||
self.logger.warning(
|
||||
'A connection error occured detecting the Telegram User.')
|
||||
'A connection error occurred detecting the Telegram User.')
|
||||
self.logger.debug('Socket Exception: %s' % str(e))
|
||||
return 0
|
||||
|
||||
|
@ -472,7 +470,7 @@ class NotifyTelegram(NotifyBase):
|
|||
entry = response['result'][0]
|
||||
_id = entry['message']['from'].get('id', 0)
|
||||
_user = entry['message']['from'].get('first_name')
|
||||
self.logger.info('Detected telegram user %s (userid=%d)' % (
|
||||
self.logger.info('Detected Telegram user %s (userid=%d)' % (
|
||||
_user, _id))
|
||||
# Return our detected userid
|
||||
return _id
|
||||
|
@ -488,6 +486,19 @@ class NotifyTelegram(NotifyBase):
|
|||
Perform Telegram Notification
|
||||
"""
|
||||
|
||||
if len(self.targets) == 0 and self.detect_owner:
|
||||
_id = self.detect_bot_owner()
|
||||
if _id:
|
||||
# Permanently store our id in our target list for next time
|
||||
self.targets.append(str(_id))
|
||||
self.logger.info(
|
||||
'Update your Telegram Apprise URL to read: '
|
||||
'{}'.format(self.url(privacy=True)))
|
||||
|
||||
if len(self.targets) == 0:
|
||||
self.logger.warning('There were not Telegram chat_ids to notify.')
|
||||
return False
|
||||
|
||||
headers = {
|
||||
'User-Agent': self.app_id,
|
||||
'Content-Type': 'application/json',
|
||||
|
@ -597,6 +608,7 @@ class NotifyTelegram(NotifyBase):
|
|||
data=dumps(payload),
|
||||
headers=headers,
|
||||
verify=self.verify_certificate,
|
||||
timeout=self.request_timeout,
|
||||
)
|
||||
|
||||
if r.status_code != requests.codes.ok:
|
||||
|
@ -631,7 +643,7 @@ class NotifyTelegram(NotifyBase):
|
|||
|
||||
except requests.RequestException as e:
|
||||
self.logger.warning(
|
||||
'A connection error occured sending Telegram:%s ' % (
|
||||
'A connection error occurred sending Telegram:%s ' % (
|
||||
payload['chat_id']) + 'notification.'
|
||||
)
|
||||
self.logger.debug('Socket Exception: %s' % str(e))
|
||||
|
@ -663,29 +675,29 @@ class NotifyTelegram(NotifyBase):
|
|||
Returns the URL built dynamically based on specified arguments.
|
||||
"""
|
||||
|
||||
# Define any arguments set
|
||||
args = {
|
||||
'format': self.notify_format,
|
||||
'overflow': self.overflow_mode,
|
||||
# Define any URL parameters
|
||||
params = {
|
||||
'image': self.include_image,
|
||||
'verify': 'yes' if self.verify_certificate else 'no',
|
||||
'detect': 'yes' if self.detect_owner else 'no',
|
||||
}
|
||||
|
||||
# Extend our parameters
|
||||
params.update(self.url_parameters(privacy=privacy, *args, **kwargs))
|
||||
|
||||
# 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(
|
||||
return '{schema}://{bot_token}/{targets}/?{params}'.format(
|
||||
schema=self.secure_protocol,
|
||||
bot_token=self.pprint(self.bot_token, privacy, safe=''),
|
||||
targets='/'.join(
|
||||
[NotifyTelegram.quote('@{}'.format(x)) for x in self.targets]),
|
||||
args=NotifyTelegram.urlencode(args))
|
||||
params=NotifyTelegram.urlencode(params))
|
||||
|
||||
@staticmethod
|
||||
def parse_url(url):
|
||||
"""
|
||||
Parses the URL and returns enough arguments that can allow
|
||||
us to substantiate this object.
|
||||
us to re-instantiate this object.
|
||||
|
||||
"""
|
||||
# This is a dirty hack; but it's the only work around to tgram://
|
||||
|
@ -718,17 +730,14 @@ class NotifyTelegram(NotifyBase):
|
|||
tgram.group('protocol'),
|
||||
tgram.group('prefix'),
|
||||
tgram.group('btoken_a'),
|
||||
tgram.group('remaining')))
|
||||
tgram.group('remaining')), verify_host=False)
|
||||
|
||||
else:
|
||||
# Try again
|
||||
results = NotifyBase.parse_url(
|
||||
'%s%s/%s' % (
|
||||
tgram.group('protocol'),
|
||||
tgram.group('btoken_a'),
|
||||
tgram.group('remaining'),
|
||||
),
|
||||
)
|
||||
results = NotifyBase.parse_url('%s%s/%s' % (
|
||||
tgram.group('protocol'),
|
||||
tgram.group('btoken_a'),
|
||||
tgram.group('remaining')), verify_host=False)
|
||||
|
||||
# The first token is stored in the hostname
|
||||
bot_token_a = NotifyTelegram.unquote(results['host'])
|
||||
|
|
|
@ -304,6 +304,7 @@ class NotifyTwilio(NotifyBase):
|
|||
data=payload,
|
||||
headers=headers,
|
||||
verify=self.verify_certificate,
|
||||
timeout=self.request_timeout,
|
||||
)
|
||||
|
||||
if r.status_code not in (
|
||||
|
@ -351,7 +352,7 @@ class NotifyTwilio(NotifyBase):
|
|||
|
||||
except requests.RequestException as e:
|
||||
self.logger.warning(
|
||||
'A Connection error occured sending Twilio:%s ' % (
|
||||
'A Connection error occurred sending Twilio:%s ' % (
|
||||
target) + 'notification.'
|
||||
)
|
||||
self.logger.debug('Socket Exception: %s' % str(e))
|
||||
|
@ -367,14 +368,10 @@ class NotifyTwilio(NotifyBase):
|
|||
Returns the URL built dynamically based on specified arguments.
|
||||
"""
|
||||
|
||||
# Define any arguments set
|
||||
args = {
|
||||
'format': self.notify_format,
|
||||
'overflow': self.overflow_mode,
|
||||
'verify': 'yes' if self.verify_certificate else 'no',
|
||||
}
|
||||
# Our URL parameters
|
||||
params = self.url_parameters(privacy=privacy, *args, **kwargs)
|
||||
|
||||
return '{schema}://{sid}:{token}@{source}/{targets}/?{args}'.format(
|
||||
return '{schema}://{sid}:{token}@{source}/{targets}/?{params}'.format(
|
||||
schema=self.secure_protocol,
|
||||
sid=self.pprint(
|
||||
self.account_sid, privacy, mode=PrivacyMode.Tail, safe=''),
|
||||
|
@ -382,13 +379,13 @@ class NotifyTwilio(NotifyBase):
|
|||
source=NotifyTwilio.quote(self.source, safe=''),
|
||||
targets='/'.join(
|
||||
[NotifyTwilio.quote(x, safe='') for x in self.targets]),
|
||||
args=NotifyTwilio.urlencode(args))
|
||||
params=NotifyTwilio.urlencode(params))
|
||||
|
||||
@staticmethod
|
||||
def parse_url(url):
|
||||
"""
|
||||
Parses the URL and returns enough arguments that can allow
|
||||
us to substantiate this object.
|
||||
us to re-instantiate this object.
|
||||
|
||||
"""
|
||||
results = NotifyBase.parse_url(url, verify_host=False)
|
||||
|
|
|
@ -36,7 +36,7 @@ from ..URLBase import PrivacyMode
|
|||
from ..common import NotifyFormat
|
||||
from ..common import NotifyType
|
||||
from ..utils import parse_list
|
||||
from ..utils import GET_EMAIL_RE
|
||||
from ..utils import is_email
|
||||
from ..AppriseLocale import gettext_lazy as _
|
||||
|
||||
|
||||
|
@ -140,12 +140,6 @@ class NotifyTwist(NotifyBase):
|
|||
# <workspace_id>:<channel_id>
|
||||
self.channel_ids = set()
|
||||
|
||||
# Initialize our Email Object
|
||||
self.email = email if email else '{}@{}'.format(
|
||||
self.user,
|
||||
self.host,
|
||||
)
|
||||
|
||||
# The token is None if we're not logged in and False if we
|
||||
# failed to log in. Otherwise it is set to the actual token
|
||||
self.token = None
|
||||
|
@ -171,26 +165,31 @@ class NotifyTwist(NotifyBase):
|
|||
# }
|
||||
self._cached_channels = dict()
|
||||
|
||||
try:
|
||||
result = GET_EMAIL_RE.match(self.email)
|
||||
if not result:
|
||||
# let outer exception handle this
|
||||
raise TypeError
|
||||
# Initialize our Email Object
|
||||
self.email = email if email else '{}@{}'.format(
|
||||
self.user,
|
||||
self.host,
|
||||
)
|
||||
|
||||
if email:
|
||||
# Force user/host to be that of the defined email for
|
||||
# consistency. This is very important for those initializing
|
||||
# this object with the the email object would could potentially
|
||||
# cause inconsistency to contents in the NotifyBase() object
|
||||
self.user = result.group('fulluser')
|
||||
self.host = result.group('domain')
|
||||
|
||||
except (TypeError, AttributeError):
|
||||
# Check if it is valid
|
||||
result = is_email(self.email)
|
||||
if not result:
|
||||
# let outer exception handle this
|
||||
msg = 'The Twist Auth email specified ({}) is invalid.'\
|
||||
.format(self.email)
|
||||
self.logger.warning(msg)
|
||||
raise TypeError(msg)
|
||||
|
||||
# Re-assign email based on what was parsed
|
||||
self.email = result['full_email']
|
||||
if email:
|
||||
# Force user/host to be that of the defined email for
|
||||
# consistency. This is very important for those initializing
|
||||
# this object with the the email object would could potentially
|
||||
# cause inconsistency to contents in the NotifyBase() object
|
||||
self.user = result['user']
|
||||
self.host = result['domain']
|
||||
|
||||
if not self.password:
|
||||
msg = 'No Twist password was specified with account: {}'\
|
||||
.format(self.email)
|
||||
|
@ -229,28 +228,25 @@ class NotifyTwist(NotifyBase):
|
|||
Returns the URL built dynamically based on specified arguments.
|
||||
"""
|
||||
|
||||
# Define any arguments set
|
||||
args = {
|
||||
'format': self.notify_format,
|
||||
'overflow': self.overflow_mode,
|
||||
'verify': 'yes' if self.verify_certificate else 'no',
|
||||
}
|
||||
# Our URL parameters
|
||||
params = self.url_parameters(privacy=privacy, *args, **kwargs)
|
||||
|
||||
return '{schema}://{password}:{user}@{host}/{targets}/?{args}'.format(
|
||||
schema=self.secure_protocol,
|
||||
password=self.pprint(
|
||||
self.password, privacy, mode=PrivacyMode.Secret, safe=''),
|
||||
user=self.quote(self.user, safe=''),
|
||||
host=self.host,
|
||||
targets='/'.join(
|
||||
[NotifyTwist.quote(x, safe='') for x in chain(
|
||||
# Channels are prefixed with a pound/hashtag symbol
|
||||
['#{}'.format(x) for x in self.channels],
|
||||
# Channel IDs
|
||||
self.channel_ids,
|
||||
)]),
|
||||
args=NotifyTwist.urlencode(args),
|
||||
)
|
||||
return '{schema}://{password}:{user}@{host}/{targets}/' \
|
||||
'?{params}'.format(
|
||||
schema=self.secure_protocol,
|
||||
password=self.pprint(
|
||||
self.password, privacy, mode=PrivacyMode.Secret, safe=''),
|
||||
user=self.quote(self.user, safe=''),
|
||||
host=self.host,
|
||||
targets='/'.join(
|
||||
[NotifyTwist.quote(x, safe='') for x in chain(
|
||||
# Channels are prefixed with a pound/hashtag symbol
|
||||
['#{}'.format(x) for x in self.channels],
|
||||
# Channel IDs
|
||||
self.channel_ids,
|
||||
)]),
|
||||
params=NotifyTwist.urlencode(params),
|
||||
)
|
||||
|
||||
def login(self):
|
||||
"""
|
||||
|
@ -640,7 +636,9 @@ class NotifyTwist(NotifyBase):
|
|||
api_url,
|
||||
data=payload,
|
||||
headers=headers,
|
||||
verify=self.verify_certificate)
|
||||
verify=self.verify_certificate,
|
||||
timeout=self.request_timeout,
|
||||
)
|
||||
|
||||
# Get our JSON content if it's possible
|
||||
try:
|
||||
|
@ -679,7 +677,9 @@ class NotifyTwist(NotifyBase):
|
|||
api_url,
|
||||
data=payload,
|
||||
headers=headers,
|
||||
verify=self.verify_certificate)
|
||||
verify=self.verify_certificate,
|
||||
timeout=self.request_timeout
|
||||
)
|
||||
|
||||
# Get our JSON content if it's possible
|
||||
try:
|
||||
|
@ -725,11 +725,10 @@ class NotifyTwist(NotifyBase):
|
|||
def parse_url(url):
|
||||
"""
|
||||
Parses the URL and returns enough arguments that can allow
|
||||
us to substantiate this object.
|
||||
us to re-instantiate this object.
|
||||
|
||||
"""
|
||||
results = NotifyBase.parse_url(url)
|
||||
|
||||
if not results:
|
||||
# We're done early as we couldn't load the results
|
||||
return results
|
||||
|
|
|
@ -73,9 +73,8 @@ class NotifyTwitter(NotifyBase):
|
|||
# The services URL
|
||||
service_url = 'https://twitter.com/'
|
||||
|
||||
# The default secure protocol is twitter. 'tweet' is left behind
|
||||
# for backwards compatibility of older apprise usage
|
||||
secure_protocol = ('twitter', 'tweet')
|
||||
# The default secure protocol is twitter.
|
||||
secure_protocol = 'twitter'
|
||||
|
||||
# A URL that takes you to the setup/help of the specific protocol
|
||||
setup_url = 'https://github.com/caronc/apprise/wiki/Notify_twitter'
|
||||
|
@ -510,7 +509,9 @@ class NotifyTwitter(NotifyBase):
|
|||
data=payload,
|
||||
headers=headers,
|
||||
auth=auth,
|
||||
verify=self.verify_certificate)
|
||||
verify=self.verify_certificate,
|
||||
timeout=self.request_timeout,
|
||||
)
|
||||
|
||||
if r.status_code != requests.codes.ok:
|
||||
# We had a problem
|
||||
|
@ -577,21 +578,21 @@ class NotifyTwitter(NotifyBase):
|
|||
Returns the URL built dynamically based on specified arguments.
|
||||
"""
|
||||
|
||||
# Define any arguments set
|
||||
args = {
|
||||
'format': self.notify_format,
|
||||
'overflow': self.overflow_mode,
|
||||
# Define any URL parameters
|
||||
params = {
|
||||
'mode': self.mode,
|
||||
'verify': 'yes' if self.verify_certificate else 'no',
|
||||
}
|
||||
|
||||
# Extend our parameters
|
||||
params.update(self.url_parameters(privacy=privacy, *args, **kwargs))
|
||||
|
||||
if len(self.targets) > 0:
|
||||
args['to'] = ','.join([NotifyTwitter.quote(x, safe='')
|
||||
for x in self.targets])
|
||||
params['to'] = ','.join(
|
||||
[NotifyTwitter.quote(x, safe='') for x in self.targets])
|
||||
|
||||
return '{schema}://{ckey}/{csecret}/{akey}/{asecret}' \
|
||||
'/{targets}/?{args}'.format(
|
||||
schema=self.secure_protocol[0],
|
||||
'/{targets}/?{params}'.format(
|
||||
schema=self.secure_protocol,
|
||||
ckey=self.pprint(self.ckey, privacy, safe=''),
|
||||
csecret=self.pprint(
|
||||
self.csecret, privacy, mode=PrivacyMode.Secret, safe=''),
|
||||
|
@ -601,17 +602,16 @@ class NotifyTwitter(NotifyBase):
|
|||
targets='/'.join(
|
||||
[NotifyTwitter.quote('@{}'.format(target), safe='')
|
||||
for target in self.targets]),
|
||||
args=NotifyTwitter.urlencode(args))
|
||||
params=NotifyTwitter.urlencode(params))
|
||||
|
||||
@staticmethod
|
||||
def parse_url(url):
|
||||
"""
|
||||
Parses the URL and returns enough arguments that can allow
|
||||
us to substantiate this object.
|
||||
us to re-instantiate this object.
|
||||
|
||||
"""
|
||||
results = NotifyBase.parse_url(url, verify_host=False)
|
||||
|
||||
if not results:
|
||||
# We're done early as we couldn't load the results
|
||||
return results
|
||||
|
@ -662,9 +662,4 @@ class NotifyTwitter(NotifyBase):
|
|||
results['targets'] += \
|
||||
NotifyTwitter.parse_list(results['qsd']['to'])
|
||||
|
||||
if results.get('schema', 'twitter').lower() == 'tweet':
|
||||
# Deprication Notice issued for v0.7.9
|
||||
NotifyTwitter.logger.deprecate(
|
||||
'tweet:// has been replaced by twitter://')
|
||||
|
||||
return results
|
||||
|
|
|
@ -168,6 +168,7 @@ class NotifyWebexTeams(NotifyBase):
|
|||
data=dumps(payload),
|
||||
headers=headers,
|
||||
verify=self.verify_certificate,
|
||||
timeout=self.request_timeout,
|
||||
)
|
||||
if r.status_code not in (
|
||||
requests.codes.ok, requests.codes.no_content):
|
||||
|
@ -194,7 +195,7 @@ class NotifyWebexTeams(NotifyBase):
|
|||
|
||||
except requests.RequestException as e:
|
||||
self.logger.warning(
|
||||
'A Connection error occured sending Webex Teams '
|
||||
'A Connection error occurred sending Webex Teams '
|
||||
'notification.'
|
||||
)
|
||||
self.logger.debug('Socket Exception: %s' % str(e))
|
||||
|
@ -208,28 +209,23 @@ class NotifyWebexTeams(NotifyBase):
|
|||
Returns the URL built dynamically based on specified arguments.
|
||||
"""
|
||||
|
||||
# Define any arguments set
|
||||
args = {
|
||||
'format': self.notify_format,
|
||||
'overflow': self.overflow_mode,
|
||||
'verify': 'yes' if self.verify_certificate else 'no',
|
||||
}
|
||||
# Our URL parameters
|
||||
params = self.url_parameters(privacy=privacy, *args, **kwargs)
|
||||
|
||||
return '{schema}://{token}/?{args}'.format(
|
||||
return '{schema}://{token}/?{params}'.format(
|
||||
schema=self.secure_protocol,
|
||||
token=self.pprint(self.token, privacy, safe=''),
|
||||
args=NotifyWebexTeams.urlencode(args),
|
||||
params=NotifyWebexTeams.urlencode(params),
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def parse_url(url):
|
||||
"""
|
||||
Parses the URL and returns enough arguments that can allow
|
||||
us to substantiate this object.
|
||||
us to re-instantiate this object.
|
||||
|
||||
"""
|
||||
results = NotifyBase.parse_url(url, verify_host=False)
|
||||
|
||||
if not results:
|
||||
# We're done early as we couldn't load the results
|
||||
return results
|
||||
|
@ -248,14 +244,14 @@ class NotifyWebexTeams(NotifyBase):
|
|||
result = re.match(
|
||||
r'^https?://api\.ciscospark\.com/v[1-9][0-9]*/webhooks/incoming/'
|
||||
r'(?P<webhook_token>[A-Z0-9_-]+)/?'
|
||||
r'(?P<args>\?.+)?$', url, re.I)
|
||||
r'(?P<params>\?.+)?$', url, re.I)
|
||||
|
||||
if result:
|
||||
return NotifyWebexTeams.parse_url(
|
||||
'{schema}://{webhook_token}/{args}'.format(
|
||||
'{schema}://{webhook_token}/{params}'.format(
|
||||
schema=NotifyWebexTeams.secure_protocol,
|
||||
webhook_token=result.group('webhook_token'),
|
||||
args='' if not result.group('args')
|
||||
else result.group('args')))
|
||||
params='' if not result.group('params')
|
||||
else result.group('params')))
|
||||
|
||||
return None
|
||||
|
|
|
@ -48,7 +48,7 @@ try:
|
|||
|
||||
except ImportError:
|
||||
# No problem; we just simply can't support this plugin because we're
|
||||
# either using Linux, or simply do not have pypiwin32 installed.
|
||||
# either using Linux, or simply do not have pywin32 installed.
|
||||
pass
|
||||
|
||||
|
||||
|
@ -91,7 +91,7 @@ class NotifyWindows(NotifyBase):
|
|||
|
||||
# Define object templates
|
||||
templates = (
|
||||
'{schema}://_/',
|
||||
'{schema}://',
|
||||
)
|
||||
|
||||
# Define our template arguments
|
||||
|
@ -146,7 +146,8 @@ class NotifyWindows(NotifyBase):
|
|||
|
||||
if not self._enabled:
|
||||
self.logger.warning(
|
||||
"Windows Notifications are not supported by this system.")
|
||||
"Windows Notifications are not supported by this system; "
|
||||
"`pip install pywin32`.")
|
||||
return False
|
||||
|
||||
# Always call throttle before any remote server i/o is made
|
||||
|
@ -222,18 +223,18 @@ class NotifyWindows(NotifyBase):
|
|||
Returns the URL built dynamically based on specified arguments.
|
||||
"""
|
||||
|
||||
# Define any arguments set
|
||||
args = {
|
||||
'format': self.notify_format,
|
||||
'overflow': self.overflow_mode,
|
||||
# Define any URL parameters
|
||||
params = {
|
||||
'image': 'yes' if self.include_image else 'no',
|
||||
'duration': str(self.duration),
|
||||
'verify': 'yes' if self.verify_certificate else 'no',
|
||||
}
|
||||
|
||||
return '{schema}://_/?{args}'.format(
|
||||
# Extend our parameters
|
||||
params.update(self.url_parameters(privacy=privacy, *args, **kwargs))
|
||||
|
||||
return '{schema}://?{params}'.format(
|
||||
schema=self.protocol,
|
||||
args=NotifyWindows.urlencode(args),
|
||||
params=NotifyWindows.urlencode(params),
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
|
@ -245,19 +246,7 @@ class NotifyWindows(NotifyBase):
|
|||
|
||||
"""
|
||||
|
||||
results = NotifyBase.parse_url(url)
|
||||
if not results:
|
||||
results = {
|
||||
'schema': NotifyWindows.protocol,
|
||||
'user': None,
|
||||
'password': None,
|
||||
'port': None,
|
||||
'host': '_',
|
||||
'fullpath': None,
|
||||
'path': None,
|
||||
'url': url,
|
||||
'qsd': {},
|
||||
}
|
||||
results = NotifyBase.parse_url(url, verify_host=False)
|
||||
|
||||
# Include images with our message
|
||||
results['include_image'] = \
|
||||
|
|
|
@ -73,9 +73,6 @@ class NotifyXBMC(NotifyBase):
|
|||
# Allows the user to specify the NotifyImageSize object
|
||||
image_size = NotifyImageSize.XY_128
|
||||
|
||||
# The number of seconds to display the popup for
|
||||
default_popup_duration_sec = 12
|
||||
|
||||
# XBMC default protocol version (v2)
|
||||
xbmc_remote_protocol = 2
|
||||
|
||||
|
@ -137,8 +134,9 @@ class NotifyXBMC(NotifyBase):
|
|||
super(NotifyXBMC, self).__init__(**kwargs)
|
||||
|
||||
# Number of seconds to display notification for
|
||||
self.duration = self.default_popup_duration_sec \
|
||||
if not (isinstance(duration, int) and duration > 0) else duration
|
||||
self.duration = self.template_args['duration']['default'] \
|
||||
if not (isinstance(duration, int) and
|
||||
self.template_args['duration']['min'] > 0) else duration
|
||||
|
||||
# Build our schema
|
||||
self.schema = 'https' if self.secure else 'http'
|
||||
|
@ -264,6 +262,7 @@ class NotifyXBMC(NotifyBase):
|
|||
headers=headers,
|
||||
auth=auth,
|
||||
verify=self.verify_certificate,
|
||||
timeout=self.request_timeout,
|
||||
)
|
||||
if r.status_code != requests.codes.ok:
|
||||
# We had a problem
|
||||
|
@ -287,7 +286,7 @@ class NotifyXBMC(NotifyBase):
|
|||
|
||||
except requests.RequestException as e:
|
||||
self.logger.warning(
|
||||
'A Connection error occured sending XBMC/KODI '
|
||||
'A Connection error occurred sending XBMC/KODI '
|
||||
'notification.'
|
||||
)
|
||||
self.logger.debug('Socket Exception: %s' % str(e))
|
||||
|
@ -302,15 +301,15 @@ class NotifyXBMC(NotifyBase):
|
|||
Returns the URL built dynamically based on specified arguments.
|
||||
"""
|
||||
|
||||
# Define any arguments set
|
||||
args = {
|
||||
'format': self.notify_format,
|
||||
'overflow': self.overflow_mode,
|
||||
# Define any URL parameters
|
||||
params = {
|
||||
'image': 'yes' if self.include_image else 'no',
|
||||
'duration': str(self.duration),
|
||||
'verify': 'yes' if self.verify_certificate else 'no',
|
||||
}
|
||||
|
||||
# Extend our parameters
|
||||
params.update(self.url_parameters(privacy=privacy, *args, **kwargs))
|
||||
|
||||
# Determine Authentication
|
||||
auth = ''
|
||||
if self.user and self.password:
|
||||
|
@ -331,20 +330,21 @@ class NotifyXBMC(NotifyBase):
|
|||
# Append 's' to schema
|
||||
default_schema += 's'
|
||||
|
||||
return '{schema}://{auth}{hostname}{port}/?{args}'.format(
|
||||
return '{schema}://{auth}{hostname}{port}/?{params}'.format(
|
||||
schema=default_schema,
|
||||
auth=auth,
|
||||
hostname=NotifyXBMC.quote(self.host, safe=''),
|
||||
# never encode hostname since we're expecting it to be a valid one
|
||||
hostname=self.host,
|
||||
port='' if not self.port or self.port == default_port
|
||||
else ':{}'.format(self.port),
|
||||
args=NotifyXBMC.urlencode(args),
|
||||
params=NotifyXBMC.urlencode(params),
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def parse_url(url):
|
||||
"""
|
||||
Parses the URL and returns enough arguments that can allow
|
||||
us to substantiate this object.
|
||||
us to re-instantiate this object.
|
||||
|
||||
"""
|
||||
results = NotifyBase.parse_url(url)
|
||||
|
|
|
@ -143,15 +143,11 @@ class NotifyXML(NotifyBase):
|
|||
Returns the URL built dynamically based on specified arguments.
|
||||
"""
|
||||
|
||||
# Define any arguments set
|
||||
args = {
|
||||
'format': self.notify_format,
|
||||
'overflow': self.overflow_mode,
|
||||
'verify': 'yes' if self.verify_certificate else 'no',
|
||||
}
|
||||
# Store our defined headers into our URL parameters
|
||||
params = {'+{}'.format(k): v for k, v in self.headers.items()}
|
||||
|
||||
# Append our headers into our args
|
||||
args.update({'+{}'.format(k): v for k, v in self.headers.items()})
|
||||
# Extend our parameters
|
||||
params.update(self.url_parameters(privacy=privacy, *args, **kwargs))
|
||||
|
||||
# Determine Authentication
|
||||
auth = ''
|
||||
|
@ -168,14 +164,15 @@ class NotifyXML(NotifyBase):
|
|||
|
||||
default_port = 443 if self.secure else 80
|
||||
|
||||
return '{schema}://{auth}{hostname}{port}{fullpath}/?{args}'.format(
|
||||
return '{schema}://{auth}{hostname}{port}{fullpath}/?{params}'.format(
|
||||
schema=self.secure_protocol if self.secure else self.protocol,
|
||||
auth=auth,
|
||||
hostname=NotifyXML.quote(self.host, safe=''),
|
||||
# never encode hostname since we're expecting it to be a valid one
|
||||
hostname=self.host,
|
||||
port='' if self.port is None or self.port == default_port
|
||||
else ':{}'.format(self.port),
|
||||
fullpath=NotifyXML.quote(self.fullpath, safe='/'),
|
||||
args=NotifyXML.urlencode(args),
|
||||
params=NotifyXML.urlencode(params),
|
||||
)
|
||||
|
||||
def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs):
|
||||
|
@ -234,6 +231,7 @@ class NotifyXML(NotifyBase):
|
|||
headers=headers,
|
||||
auth=auth,
|
||||
verify=self.verify_certificate,
|
||||
timeout=self.request_timeout,
|
||||
)
|
||||
if r.status_code != requests.codes.ok:
|
||||
# We had a problem
|
||||
|
@ -257,7 +255,7 @@ class NotifyXML(NotifyBase):
|
|||
|
||||
except requests.RequestException as e:
|
||||
self.logger.warning(
|
||||
'A Connection error occured sending XML '
|
||||
'A Connection error occurred sending XML '
|
||||
'notification to %s.' % self.host)
|
||||
self.logger.debug('Socket Exception: %s' % str(e))
|
||||
|
||||
|
@ -270,11 +268,10 @@ class NotifyXML(NotifyBase):
|
|||
def parse_url(url):
|
||||
"""
|
||||
Parses the URL and returns enough arguments that can allow
|
||||
us to substantiate this object.
|
||||
us to re-instantiate this object.
|
||||
|
||||
"""
|
||||
results = NotifyBase.parse_url(url)
|
||||
|
||||
if not results:
|
||||
# We're done early as we couldn't load the results
|
||||
return results
|
||||
|
|
|
@ -272,20 +272,16 @@ class NotifyXMPP(NotifyBase):
|
|||
Returns the URL built dynamically based on specified arguments.
|
||||
"""
|
||||
|
||||
# Define any arguments set
|
||||
args = {
|
||||
'format': self.notify_format,
|
||||
'overflow': self.overflow_mode,
|
||||
'verify': 'yes' if self.verify_certificate else 'no',
|
||||
}
|
||||
# Our URL parameters
|
||||
params = self.url_parameters(privacy=privacy, *args, **kwargs)
|
||||
|
||||
if self.jid:
|
||||
args['jid'] = self.jid
|
||||
params['jid'] = self.jid
|
||||
|
||||
if self.xep:
|
||||
# xep are integers, so we need to just iterate over a list and
|
||||
# switch them to a string
|
||||
args['xep'] = ','.join([str(xep) for xep in self.xep])
|
||||
params['xep'] = ','.join([str(xep) for xep in self.xep])
|
||||
|
||||
# Target JID(s) can clash with our existing paths, so we just use comma
|
||||
# and/or space as a delimiters - %20 = space
|
||||
|
@ -307,25 +303,25 @@ class NotifyXMPP(NotifyBase):
|
|||
self.password if self.password else self.user, privacy,
|
||||
mode=PrivacyMode.Secret, safe='')
|
||||
|
||||
return '{schema}://{auth}@{hostname}{port}/{jids}?{args}'.format(
|
||||
return '{schema}://{auth}@{hostname}{port}/{jids}?{params}'.format(
|
||||
auth=auth,
|
||||
schema=default_schema,
|
||||
hostname=NotifyXMPP.quote(self.host, safe=''),
|
||||
# never encode hostname since we're expecting it to be a valid one
|
||||
hostname=self.host,
|
||||
port='' if not self.port or self.port == default_port
|
||||
else ':{}'.format(self.port),
|
||||
jids=jids,
|
||||
args=NotifyXMPP.urlencode(args),
|
||||
params=NotifyXMPP.urlencode(params),
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def parse_url(url):
|
||||
"""
|
||||
Parses the URL and returns enough arguments that can allow
|
||||
us to substantiate this object.
|
||||
us to re-instantiate this object.
|
||||
|
||||
"""
|
||||
results = NotifyBase.parse_url(url)
|
||||
|
||||
if not results:
|
||||
# We're done early as we couldn't load the results
|
||||
return results
|
||||
|
|
|
@ -62,7 +62,7 @@ from .NotifyBase import NotifyBase
|
|||
from ..common import NotifyType
|
||||
from ..utils import parse_list
|
||||
from ..utils import validate_regex
|
||||
from ..utils import GET_EMAIL_RE
|
||||
from ..utils import is_email
|
||||
from ..AppriseLocale import gettext_lazy as _
|
||||
|
||||
# A Valid Bot Name
|
||||
|
@ -260,7 +260,8 @@ class NotifyZulip(NotifyBase):
|
|||
targets = list(self.targets)
|
||||
while len(targets):
|
||||
target = targets.pop(0)
|
||||
if GET_EMAIL_RE.match(target):
|
||||
result = is_email(target)
|
||||
if result:
|
||||
# Send a private message
|
||||
payload['type'] = 'private'
|
||||
else:
|
||||
|
@ -268,7 +269,7 @@ class NotifyZulip(NotifyBase):
|
|||
payload['type'] = 'stream'
|
||||
|
||||
# Set our target
|
||||
payload['to'] = target
|
||||
payload['to'] = target if not result else result['full_email']
|
||||
|
||||
self.logger.debug('Zulip POST URL: %s (cert_verify=%r)' % (
|
||||
url, self.verify_certificate,
|
||||
|
@ -284,6 +285,7 @@ class NotifyZulip(NotifyBase):
|
|||
headers=headers,
|
||||
auth=auth,
|
||||
verify=self.verify_certificate,
|
||||
timeout=self.request_timeout,
|
||||
)
|
||||
if r.status_code != requests.codes.ok:
|
||||
# We had a problem
|
||||
|
@ -312,7 +314,7 @@ class NotifyZulip(NotifyBase):
|
|||
|
||||
except requests.RequestException as e:
|
||||
self.logger.warning(
|
||||
'A Connection error occured sending Zulip '
|
||||
'A Connection error occurred sending Zulip '
|
||||
'notification to {}.'.format(target))
|
||||
self.logger.debug('Socket Exception: %s' % str(e))
|
||||
|
||||
|
@ -327,12 +329,8 @@ class NotifyZulip(NotifyBase):
|
|||
Returns the URL built dynamically based on specified arguments.
|
||||
"""
|
||||
|
||||
# Define any arguments set
|
||||
args = {
|
||||
'format': self.notify_format,
|
||||
'overflow': self.overflow_mode,
|
||||
'verify': 'yes' if self.verify_certificate else 'no',
|
||||
}
|
||||
# Our URL parameters
|
||||
params = self.url_parameters(privacy=privacy, *args, **kwargs)
|
||||
|
||||
# simplify our organization in our URL if we can
|
||||
organization = '{}{}'.format(
|
||||
|
@ -341,25 +339,24 @@ class NotifyZulip(NotifyBase):
|
|||
if self.hostname != self.default_hostname else '')
|
||||
|
||||
return '{schema}://{botname}@{org}/{token}/' \
|
||||
'{targets}?{args}'.format(
|
||||
'{targets}?{params}'.format(
|
||||
schema=self.secure_protocol,
|
||||
botname=NotifyZulip.quote(self.botname, safe=''),
|
||||
org=NotifyZulip.quote(organization, safe=''),
|
||||
token=self.pprint(self.token, privacy, safe=''),
|
||||
targets='/'.join(
|
||||
[NotifyZulip.quote(x, safe='') for x in self.targets]),
|
||||
args=NotifyZulip.urlencode(args),
|
||||
params=NotifyZulip.urlencode(params),
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def parse_url(url):
|
||||
"""
|
||||
Parses the URL and returns enough arguments that can allow
|
||||
us to substantiate this object.
|
||||
us to re-instantiate this object.
|
||||
|
||||
"""
|
||||
results = NotifyBase.parse_url(url)
|
||||
|
||||
results = NotifyBase.parse_url(url, verify_host=False)
|
||||
if not results:
|
||||
# We're done early as we couldn't load the results
|
||||
return results
|
||||
|
|
|
@ -23,17 +23,16 @@
|
|||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
# THE SOFTWARE.
|
||||
|
||||
import os
|
||||
import six
|
||||
import re
|
||||
import copy
|
||||
|
||||
from os import listdir
|
||||
from os.path import dirname
|
||||
from os.path import abspath
|
||||
|
||||
# Used for testing
|
||||
from . import NotifyEmail as NotifyEmailBase
|
||||
from .NotifyGrowl import gntp
|
||||
from .NotifyXMPP import SleekXmppAdapter
|
||||
|
||||
# NotifyBase object is passed in as a module not class
|
||||
|
@ -45,6 +44,7 @@ from ..common import NotifyType
|
|||
from ..common import NOTIFY_TYPES
|
||||
from ..utils import parse_list
|
||||
from ..utils import GET_SCHEMA_RE
|
||||
from ..logger import logger
|
||||
from ..AppriseLocale import gettext_lazy as _
|
||||
from ..AppriseLocale import LazyTranslation
|
||||
|
||||
|
@ -62,9 +62,6 @@ __all__ = [
|
|||
# Tokenizer
|
||||
'url_to_dict',
|
||||
|
||||
# gntp (used for NotifyGrowl Testing)
|
||||
'gntp',
|
||||
|
||||
# sleekxmpp access points (used for NotifyXMPP Testing)
|
||||
'SleekXmppAdapter',
|
||||
]
|
||||
|
@ -85,7 +82,7 @@ def __load_matrix(path=abspath(dirname(__file__)), name='apprise.plugins'):
|
|||
# The .py extension is optional as we support loading directories too
|
||||
module_re = re.compile(r'^(?P<name>Notify[a-z0-9]+)(\.py)?$', re.I)
|
||||
|
||||
for f in listdir(path):
|
||||
for f in os.listdir(path):
|
||||
match = module_re.match(f)
|
||||
if not match:
|
||||
# keep going
|
||||
|
@ -131,29 +128,39 @@ def __load_matrix(path=abspath(dirname(__file__)), name='apprise.plugins'):
|
|||
# Load our module into memory so it's accessible to all
|
||||
globals()[plugin_name] = plugin
|
||||
|
||||
# Load protocol(s) if defined
|
||||
proto = getattr(plugin, 'protocol', None)
|
||||
if isinstance(proto, six.string_types):
|
||||
if proto not in SCHEMA_MAP:
|
||||
SCHEMA_MAP[proto] = plugin
|
||||
fn = getattr(plugin, 'schemas', None)
|
||||
try:
|
||||
schemas = set([]) if not callable(fn) else fn(plugin)
|
||||
|
||||
elif isinstance(proto, (set, list, tuple)):
|
||||
# Support iterables list types
|
||||
for p in proto:
|
||||
if p not in SCHEMA_MAP:
|
||||
SCHEMA_MAP[p] = plugin
|
||||
except TypeError:
|
||||
# Python v2.x support where functions associated with classes
|
||||
# were considered bound to them and could not be called prior
|
||||
# to the classes initialization. This code can be dropped
|
||||
# once Python v2.x support is dropped. The below code introduces
|
||||
# replication as it already exists and is tested in
|
||||
# URLBase.schemas()
|
||||
schemas = set([])
|
||||
for key in ('protocol', 'secure_protocol'):
|
||||
schema = getattr(plugin, key, None)
|
||||
if isinstance(schema, six.string_types):
|
||||
schemas.add(schema)
|
||||
|
||||
# Load secure protocol(s) if defined
|
||||
protos = getattr(plugin, 'secure_protocol', None)
|
||||
if isinstance(protos, six.string_types):
|
||||
if protos not in SCHEMA_MAP:
|
||||
SCHEMA_MAP[protos] = plugin
|
||||
elif isinstance(schema, (set, list, tuple)):
|
||||
# Support iterables list types
|
||||
for s in schema:
|
||||
if isinstance(s, six.string_types):
|
||||
schemas.add(s)
|
||||
|
||||
if isinstance(protos, (set, list, tuple)):
|
||||
# Support iterables list types
|
||||
for p in protos:
|
||||
if p not in SCHEMA_MAP:
|
||||
SCHEMA_MAP[p] = plugin
|
||||
# map our schema to our plugin
|
||||
for schema in schemas:
|
||||
if schema in SCHEMA_MAP:
|
||||
logger.error(
|
||||
"Notification schema ({}) mismatch detected - {} to {}"
|
||||
.format(schema, SCHEMA_MAP[schema], plugin))
|
||||
continue
|
||||
|
||||
# Assign plugin
|
||||
SCHEMA_MAP[schema] = plugin
|
||||
|
||||
return SCHEMA_MAP
|
||||
|
||||
|
@ -452,6 +459,7 @@ def url_to_dict(url):
|
|||
schema = GET_SCHEMA_RE.match(_url)
|
||||
if schema is None:
|
||||
# Not a valid URL; take an early exit
|
||||
logger.error('Unsupported URL: {}'.format(url))
|
||||
return None
|
||||
|
||||
# Ensure our schema is always in lower case
|
||||
|
@ -466,10 +474,28 @@ def url_to_dict(url):
|
|||
for r in MODULE_MAP.values()
|
||||
if r['plugin'].parse_native_url(_url) is not None),
|
||||
None)
|
||||
|
||||
if not results:
|
||||
logger.error('Unparseable URL {}'.format(url))
|
||||
return None
|
||||
|
||||
logger.trace('URL {} unpacked as:{}{}'.format(
|
||||
url, os.linesep, os.linesep.join(
|
||||
['{}="{}"'.format(k, v) for k, v in results.items()])))
|
||||
|
||||
else:
|
||||
# Parse our url details of the server object as dictionary
|
||||
# containing all of the information parsed from our URL
|
||||
results = SCHEMA_MAP[schema].parse_url(_url)
|
||||
if not results:
|
||||
logger.error('Unparseable {} URL {}'.format(
|
||||
SCHEMA_MAP[schema].service_name, url))
|
||||
return None
|
||||
|
||||
logger.trace('{} URL {} unpacked as:{}{}'.format(
|
||||
SCHEMA_MAP[schema].service_name, url,
|
||||
os.linesep, os.linesep.join(
|
||||
['{}="{}"'.format(k, v) for k, v in results.items()])))
|
||||
|
||||
# Return our results
|
||||
return results
|
||||
|
|
|
@ -104,40 +104,129 @@ GET_SCHEMA_RE = re.compile(r'\s*(?P<schema>[a-z0-9]{2,9})://.*$', re.I)
|
|||
|
||||
# Regular expression based and expanded from:
|
||||
# http://www.regular-expressions.info/email.html
|
||||
# Extended to support colon (:) delimiter for parsing names from the URL
|
||||
# such as:
|
||||
# - 'Optional Name':user@example.com
|
||||
# - 'Optional Name' <user@example.com>
|
||||
#
|
||||
# The expression also parses the general email as well such as:
|
||||
# - user@example.com
|
||||
# - label+user@example.com
|
||||
GET_EMAIL_RE = re.compile(
|
||||
r"(?P<fulluser>((?P<label>[^+]+)\+)?"
|
||||
r"(?P<userid>[a-z0-9$%=_~-]+"
|
||||
r"(?:\.[a-z0-9$%+=_~-]+)"
|
||||
r"*))@(?P<domain>(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+"
|
||||
r"[a-z0-9](?:[a-z0-9-]*"
|
||||
r"[a-z0-9]))?",
|
||||
re.IGNORECASE,
|
||||
)
|
||||
r'((?P<name>[^:<]+)?[:<\s]+)?'
|
||||
r'(?P<full_email>((?P<label>[^+]+)\+)?'
|
||||
r'(?P<email>(?P<userid>[a-z0-9$%=_~-]+'
|
||||
r'(?:\.[a-z0-9$%+=_~-]+)'
|
||||
r'*)@(?P<domain>('
|
||||
r'(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+'
|
||||
r'[a-z0-9](?:[a-z0-9-]*[a-z0-9]))|'
|
||||
r'[a-z0-9][a-z0-9-]{5,})))'
|
||||
r'\s*>?', re.IGNORECASE)
|
||||
|
||||
# Regular expression used to extract a phone number
|
||||
GET_PHONE_NO_RE = re.compile(r'^\+?(?P<phone>[0-9\s)(+-]+)\s*$')
|
||||
|
||||
# Regular expression used to destinguish between multiple URLs
|
||||
URL_DETECTION_RE = re.compile(
|
||||
r'([a-z0-9]+?:\/\/.*?)[\s,]*(?=$|[a-z0-9]+?:\/\/)', re.I)
|
||||
r'([a-z0-9]+?:\/\/.*?)(?=$|[\s,]+[a-z0-9]{2,9}?:\/\/)', re.I)
|
||||
|
||||
EMAIL_DETECTION_RE = re.compile(
|
||||
r'[\s,]*([^@]+@.*?)(?=$|[\s,]+'
|
||||
+ r'(?:[^:<]+?[:<\s]+?)?'
|
||||
r'[^@\s,]+@[^\s,]+)',
|
||||
re.IGNORECASE)
|
||||
|
||||
# validate_regex() utilizes this mapping to track and re-use pre-complied
|
||||
# regular expressions
|
||||
REGEX_VALIDATE_LOOKUP = {}
|
||||
|
||||
|
||||
def is_hostname(hostname):
|
||||
def is_ipaddr(addr, ipv4=True, ipv6=True):
|
||||
"""
|
||||
Validates against IPV4 and IPV6 IP Addresses
|
||||
"""
|
||||
|
||||
if ipv4:
|
||||
# Based on https://stackoverflow.com/questions/5284147/\
|
||||
# validating-ipv4-addresses-with-regexp
|
||||
re_ipv4 = re.compile(
|
||||
r'^(?P<ip>((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}'
|
||||
r'(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?))$'
|
||||
)
|
||||
match = re_ipv4.match(addr)
|
||||
if match is not None:
|
||||
# Return our matched IP
|
||||
return match.group('ip')
|
||||
|
||||
if ipv6:
|
||||
# Based on https://stackoverflow.com/questions/53497/\
|
||||
# regular-expression-that-matches-valid-ipv6-addresses
|
||||
#
|
||||
# IPV6 URLs should be enclosed in square brackets when placed on a URL
|
||||
# Source: https://tools.ietf.org/html/rfc2732
|
||||
# - For this reason, they are additionally checked for existance
|
||||
re_ipv6 = re.compile(
|
||||
r'\[?(?P<ip>(([0-9a-f]{1,4}:){7,7}[0-9a-f]{1,4}|([0-9a-f]{1,4}:)'
|
||||
r'{1,7}:|([0-9a-f]{1,4}:){1,6}:[0-9a-f]{1,4}|([0-9a-f]{1,4}:){1,5}'
|
||||
r'(:[0-9a-f]{1,4}){1,2}|([0-9a-f]{1,4}:){1,4}'
|
||||
r'(:[0-9a-f]{1,4}){1,3}|([0-9a-f]{1,4}:){1,3}'
|
||||
r'(:[0-9a-f]{1,4}){1,4}|([0-9a-f]{1,4}:){1,2}'
|
||||
r'(:[0-9a-f]{1,4}){1,5}|[0-9a-f]{1,4}:'
|
||||
r'((:[0-9a-f]{1,4}){1,6})|:((:[0-9a-f]{1,4}){1,7}|:)|'
|
||||
r'fe80:(:[0-9a-f]{0,4}){0,4}%[0-9a-z]{1,}|::'
|
||||
r'(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]'
|
||||
r'|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|'
|
||||
r'1{0,1}[0-9]){0,1}[0-9])|([0-9a-f]{1,4}:){1,4}:((25[0-5]|'
|
||||
r'(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|'
|
||||
r'1{0,1}[0-9]){0,1}[0-9])))\]?', re.I,
|
||||
)
|
||||
|
||||
match = re_ipv6.match(addr)
|
||||
if match is not None:
|
||||
# Return our matched IP between square brackets since that is
|
||||
# required for URL formatting as per RFC 2732.
|
||||
return '[{}]'.format(match.group('ip'))
|
||||
|
||||
# There was no match
|
||||
return False
|
||||
|
||||
|
||||
def is_hostname(hostname, ipv4=True, ipv6=True):
|
||||
"""
|
||||
Validate hostname
|
||||
"""
|
||||
if len(hostname) > 255 or len(hostname) == 0:
|
||||
# The entire hostname, including the delimiting dots, has a maximum of 253
|
||||
# ASCII characters.
|
||||
if len(hostname) > 253 or len(hostname) == 0:
|
||||
return False
|
||||
|
||||
# Strip trailling period on hostname (if one exists)
|
||||
if hostname[-1] == ".":
|
||||
hostname = hostname[:-1]
|
||||
|
||||
allowed = re.compile(r'(?!-)[A-Z\d_-]{1,63}(?<!-)$', re.IGNORECASE)
|
||||
return all(allowed.match(x) for x in hostname.split("."))
|
||||
# Split our hostname up
|
||||
labels = hostname.split(".")
|
||||
|
||||
# ipv4 check
|
||||
if len(labels) == 4 and re.match(r'[0-9.]+', hostname):
|
||||
return is_ipaddr(hostname, ipv4=ipv4, ipv6=False)
|
||||
|
||||
# - RFC 1123 permits hostname labels to start with digits
|
||||
# - digit must be followed by alpha/numeric so we don't end up
|
||||
# processing IP addresses here
|
||||
# - Hostnames can ony be comprised of alpha-numeric characters and the
|
||||
# hyphen (-) character.
|
||||
# - Hostnames can not start with the hyphen (-) character.
|
||||
# - labels can not exceed 63 characters
|
||||
allowed = re.compile(
|
||||
r'(?!-)[a-z0-9][a-z0-9-]{1,62}(?<!-)$',
|
||||
re.IGNORECASE,
|
||||
)
|
||||
|
||||
if not all(allowed.match(x) for x in labels):
|
||||
return is_ipaddr(hostname, ipv4=ipv4, ipv6=ipv6)
|
||||
|
||||
return hostname
|
||||
|
||||
|
||||
def is_email(address):
|
||||
|
@ -152,11 +241,33 @@ def is_email(address):
|
|||
"""
|
||||
|
||||
try:
|
||||
return GET_EMAIL_RE.match(address) is not None
|
||||
match = GET_EMAIL_RE.match(address)
|
||||
|
||||
except TypeError:
|
||||
# invalid syntax
|
||||
# not parseable content
|
||||
return False
|
||||
|
||||
if match:
|
||||
return {
|
||||
# The name parsed from the URL (if one exists)
|
||||
'name': '' if match.group('name') is None
|
||||
else match.group('name').strip(),
|
||||
# The email address
|
||||
'email': match.group('email'),
|
||||
# The full email address (includes label if specified)
|
||||
'full_email': match.group('full_email'),
|
||||
# The label (if specified) e.g: label+user@example.com
|
||||
'label': '' if match.group('label') is None
|
||||
else match.group('label').strip(),
|
||||
# The user (which does not include the label) from the email
|
||||
# parsed.
|
||||
'user': match.group('userid'),
|
||||
# The domain associated with the email address
|
||||
'domain': match.group('domain'),
|
||||
}
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def tidy_path(path):
|
||||
"""take a filename and or directory and attempts to tidy it up by removing
|
||||
|
@ -384,30 +495,22 @@ def parse_url(url, default_schema='http', verify_host=True):
|
|||
# and it's already assigned
|
||||
pass
|
||||
|
||||
try:
|
||||
(result['host'], result['port']) = \
|
||||
re.split(r'[:]+', result['host'])[:2]
|
||||
# Max port is 65535 so (1,5 digits)
|
||||
match = re.search(
|
||||
r'^(?P<host>.+):(?P<port>[1-9][0-9]{0,4})$', result['host'])
|
||||
if match:
|
||||
# Separate our port from our hostname (if port is detected)
|
||||
result['host'] = match.group('host')
|
||||
result['port'] = int(match.group('port'))
|
||||
|
||||
except ValueError:
|
||||
# no problem then, user only exists
|
||||
# and it's already assigned
|
||||
pass
|
||||
|
||||
if result['port']:
|
||||
try:
|
||||
result['port'] = int(result['port'])
|
||||
|
||||
except (ValueError, TypeError):
|
||||
# Invalid Port Specified
|
||||
if verify_host:
|
||||
# Verify and Validate our hostname
|
||||
result['host'] = is_hostname(result['host'])
|
||||
if not result['host']:
|
||||
# Nothing more we can do without a hostname; give the user
|
||||
# some indication as to what went wrong
|
||||
return None
|
||||
|
||||
if result['port'] == 0:
|
||||
result['port'] = None
|
||||
|
||||
if verify_host and not is_hostname(result['host']):
|
||||
# Nothing more we can do without a hostname
|
||||
return None
|
||||
|
||||
# Re-assemble cleaned up version of the url
|
||||
result['url'] = '%s://' % result['schema']
|
||||
if isinstance(result['user'], six.string_types):
|
||||
|
@ -469,26 +572,76 @@ def parse_bool(arg, default=False):
|
|||
return bool(arg)
|
||||
|
||||
|
||||
def split_urls(urls):
|
||||
def parse_emails(*args, **kwargs):
|
||||
"""
|
||||
Takes a string containing URLs separated by comma's and/or spaces and
|
||||
returns a list.
|
||||
"""
|
||||
|
||||
try:
|
||||
results = URL_DETECTION_RE.findall(urls)
|
||||
# for Python 2.7 support, store_unparsable is not in the url above
|
||||
# as just parse_emails(*args, store_unparseable=True) since it is
|
||||
# an invalid syntax. This is the workaround to be backards compatible:
|
||||
store_unparseable = kwargs.get('store_unparseable', True)
|
||||
|
||||
except TypeError:
|
||||
results = []
|
||||
result = []
|
||||
for arg in args:
|
||||
if isinstance(arg, six.string_types) and arg:
|
||||
_result = EMAIL_DETECTION_RE.findall(arg)
|
||||
if _result:
|
||||
result += _result
|
||||
|
||||
if len(results) > 0 and results[len(results) - 1][-1] != urls[-1]:
|
||||
# we always want to save the end of url URL if we can; This handles
|
||||
# cases where there is actually a comma (,) at the end of a single URL
|
||||
# that would have otherwise got lost when our regex passed over it.
|
||||
results[len(results) - 1] += \
|
||||
re.match(r'.*?([\s,]+)?$', urls).group(1).rstrip()
|
||||
elif not _result and store_unparseable:
|
||||
# we had content passed into us that was lost because it was
|
||||
# so poorly formatted that it didn't even come close to
|
||||
# meeting the regular expression we defined. We intentially
|
||||
# keep it as part of our result set so that parsing done
|
||||
# at a higher level can at least report this to the end user
|
||||
# and hopefully give them some indication as to what they
|
||||
# may have done wrong.
|
||||
result += \
|
||||
[x for x in filter(bool, re.split(STRING_DELIMITERS, arg))]
|
||||
|
||||
return results
|
||||
elif isinstance(arg, (set, list, tuple)):
|
||||
# Use recursion to handle the list of Emails
|
||||
result += parse_emails(*arg, store_unparseable=store_unparseable)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def parse_urls(*args, **kwargs):
|
||||
"""
|
||||
Takes a string containing URLs separated by comma's and/or spaces and
|
||||
returns a list.
|
||||
"""
|
||||
|
||||
# for Python 2.7 support, store_unparsable is not in the url above
|
||||
# as just parse_urls(*args, store_unparseable=True) since it is
|
||||
# an invalid syntax. This is the workaround to be backards compatible:
|
||||
store_unparseable = kwargs.get('store_unparseable', True)
|
||||
|
||||
result = []
|
||||
for arg in args:
|
||||
if isinstance(arg, six.string_types) and arg:
|
||||
_result = URL_DETECTION_RE.findall(arg)
|
||||
if _result:
|
||||
result += _result
|
||||
|
||||
elif not _result and store_unparseable:
|
||||
# we had content passed into us that was lost because it was
|
||||
# so poorly formatted that it didn't even come close to
|
||||
# meeting the regular expression we defined. We intentially
|
||||
# keep it as part of our result set so that parsing done
|
||||
# at a higher level can at least report this to the end user
|
||||
# and hopefully give them some indication as to what they
|
||||
# may have done wrong.
|
||||
result += \
|
||||
[x for x in filter(bool, re.split(STRING_DELIMITERS, arg))]
|
||||
|
||||
elif isinstance(arg, (set, list, tuple)):
|
||||
# Use recursion to handle the list of URLs
|
||||
result += parse_urls(*arg, store_unparseable=store_unparseable)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def parse_list(*args):
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
apprise=0.8.5
|
||||
apprise=0.8.8
|
||||
apscheduler=3.5.1
|
||||
babelfish=0.5.5
|
||||
backports.functools-lru-cache=1.5
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue