mirror of
https://github.com/morpheus65535/bazarr.git
synced 2024-09-20 15:35:58 +08:00
Upgraded Apprise to 1.1.0 version.
This commit is contained in:
parent
61ef236d04
commit
04b095995c
|
@ -24,18 +24,14 @@
|
||||||
# THE SOFTWARE.
|
# THE SOFTWARE.
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import six
|
|
||||||
from itertools import chain
|
from itertools import chain
|
||||||
from .common import NotifyType
|
from . import common
|
||||||
from .common import MATCH_ALL_TAG
|
|
||||||
from .common import MATCH_ALWAYS_TAG
|
|
||||||
from .conversion import convert_between
|
from .conversion import convert_between
|
||||||
from .utils import is_exclusive_match
|
from .utils import is_exclusive_match
|
||||||
from .utils import parse_list
|
from .utils import parse_list
|
||||||
from .utils import parse_urls
|
from .utils import parse_urls
|
||||||
from .utils import cwe312_url
|
from .utils import cwe312_url
|
||||||
from .logger import logger
|
from .logger import logger
|
||||||
|
|
||||||
from .AppriseAsset import AppriseAsset
|
from .AppriseAsset import AppriseAsset
|
||||||
from .AppriseConfig import AppriseConfig
|
from .AppriseConfig import AppriseConfig
|
||||||
from .AppriseAttachment import AppriseAttachment
|
from .AppriseAttachment import AppriseAttachment
|
||||||
|
@ -47,13 +43,13 @@ from .plugins.NotifyBase import NotifyBase
|
||||||
from . import plugins
|
from . import plugins
|
||||||
from . import __version__
|
from . import __version__
|
||||||
|
|
||||||
# Python v3+ support code made importable so it can remain backwards
|
# Python v3+ support code made importable, so it can remain backwards
|
||||||
# compatible with Python v2
|
# compatible with Python v2
|
||||||
|
# TODO: Review after dropping support for Python 2.
|
||||||
from . import py3compat
|
from . import py3compat
|
||||||
ASYNCIO_SUPPORT = not six.PY2
|
|
||||||
|
|
||||||
|
|
||||||
class Apprise(object):
|
class Apprise:
|
||||||
"""
|
"""
|
||||||
Our Notification Manager
|
Our Notification Manager
|
||||||
|
|
||||||
|
@ -127,7 +123,7 @@ class Apprise(object):
|
||||||
# Prepare our Asset Object
|
# Prepare our Asset Object
|
||||||
asset = asset if isinstance(asset, AppriseAsset) else AppriseAsset()
|
asset = asset if isinstance(asset, AppriseAsset) else AppriseAsset()
|
||||||
|
|
||||||
if isinstance(url, six.string_types):
|
if isinstance(url, str):
|
||||||
# Acquire our url tokens
|
# Acquire our url tokens
|
||||||
results = plugins.url_to_dict(
|
results = plugins.url_to_dict(
|
||||||
url, secure_logging=asset.secure_logging)
|
url, secure_logging=asset.secure_logging)
|
||||||
|
@ -141,7 +137,7 @@ class Apprise(object):
|
||||||
# We already have our result set
|
# We already have our result set
|
||||||
results = url
|
results = url
|
||||||
|
|
||||||
if results.get('schema') not in plugins.SCHEMA_MAP:
|
if results.get('schema') not in common.NOTIFY_SCHEMA_MAP:
|
||||||
# schema is a mandatory dictionary item as it is the only way
|
# schema is a mandatory dictionary item as it is the only way
|
||||||
# we can index into our loaded plugins
|
# we can index into our loaded plugins
|
||||||
logger.error('Dictionary does not include a "schema" entry.')
|
logger.error('Dictionary does not include a "schema" entry.')
|
||||||
|
@ -164,7 +160,7 @@ class Apprise(object):
|
||||||
type(url))
|
type(url))
|
||||||
return None
|
return None
|
||||||
|
|
||||||
if not plugins.SCHEMA_MAP[results['schema']].enabled:
|
if not common.NOTIFY_SCHEMA_MAP[results['schema']].enabled:
|
||||||
#
|
#
|
||||||
# First Plugin Enable Check (Pre Initialization)
|
# First Plugin Enable Check (Pre Initialization)
|
||||||
#
|
#
|
||||||
|
@ -184,12 +180,13 @@ class Apprise(object):
|
||||||
try:
|
try:
|
||||||
# Attempt to create an instance of our plugin using the parsed
|
# Attempt to create an instance of our plugin using the parsed
|
||||||
# URL information
|
# URL information
|
||||||
plugin = plugins.SCHEMA_MAP[results['schema']](**results)
|
plugin = common.NOTIFY_SCHEMA_MAP[results['schema']](**results)
|
||||||
|
|
||||||
# Create log entry of loaded URL
|
# Create log entry of loaded URL
|
||||||
logger.debug(
|
logger.debug(
|
||||||
'Loaded {} URL: {}'.format(
|
'Loaded {} URL: {}'.format(
|
||||||
plugins.SCHEMA_MAP[results['schema']].service_name,
|
common.
|
||||||
|
NOTIFY_SCHEMA_MAP[results['schema']].service_name,
|
||||||
plugin.url(privacy=asset.secure_logging)))
|
plugin.url(privacy=asset.secure_logging)))
|
||||||
|
|
||||||
except Exception:
|
except Exception:
|
||||||
|
@ -200,14 +197,15 @@ class Apprise(object):
|
||||||
# the arguments are invalid or can not be used.
|
# the arguments are invalid or can not be used.
|
||||||
logger.error(
|
logger.error(
|
||||||
'Could not load {} URL: {}'.format(
|
'Could not load {} URL: {}'.format(
|
||||||
plugins.SCHEMA_MAP[results['schema']].service_name,
|
common.
|
||||||
|
NOTIFY_SCHEMA_MAP[results['schema']].service_name,
|
||||||
loggable_url))
|
loggable_url))
|
||||||
return None
|
return None
|
||||||
|
|
||||||
else:
|
else:
|
||||||
# Attempt to create an instance of our plugin using the parsed
|
# Attempt to create an instance of our plugin using the parsed
|
||||||
# URL information but don't wrap it in a try catch
|
# URL information but don't wrap it in a try catch
|
||||||
plugin = plugins.SCHEMA_MAP[results['schema']](**results)
|
plugin = common.NOTIFY_SCHEMA_MAP[results['schema']](**results)
|
||||||
|
|
||||||
if not plugin.enabled:
|
if not plugin.enabled:
|
||||||
#
|
#
|
||||||
|
@ -248,7 +246,7 @@ class Apprise(object):
|
||||||
# prepare default asset
|
# prepare default asset
|
||||||
asset = self.asset
|
asset = self.asset
|
||||||
|
|
||||||
if isinstance(servers, six.string_types):
|
if isinstance(servers, str):
|
||||||
# build our server list
|
# build our server list
|
||||||
servers = parse_urls(servers)
|
servers = parse_urls(servers)
|
||||||
if len(servers) == 0:
|
if len(servers) == 0:
|
||||||
|
@ -276,7 +274,7 @@ class Apprise(object):
|
||||||
self.servers.append(_server)
|
self.servers.append(_server)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
elif not isinstance(_server, (six.string_types, dict)):
|
elif not isinstance(_server, (str, dict)):
|
||||||
logger.error(
|
logger.error(
|
||||||
"An invalid notification (type={}) was specified.".format(
|
"An invalid notification (type={}) was specified.".format(
|
||||||
type(_server)))
|
type(_server)))
|
||||||
|
@ -305,9 +303,9 @@ class Apprise(object):
|
||||||
"""
|
"""
|
||||||
self.servers[:] = []
|
self.servers[:] = []
|
||||||
|
|
||||||
def find(self, tag=MATCH_ALL_TAG, match_always=True):
|
def find(self, tag=common.MATCH_ALL_TAG, match_always=True):
|
||||||
"""
|
"""
|
||||||
Returns an list of all servers matching against the tag specified.
|
Returns a list of all servers matching against the tag specified.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
@ -323,7 +321,7 @@ class Apprise(object):
|
||||||
|
|
||||||
# A match_always flag allows us to pick up on our 'any' keyword
|
# A match_always flag allows us to pick up on our 'any' keyword
|
||||||
# and notify these services under all circumstances
|
# and notify these services under all circumstances
|
||||||
match_always = MATCH_ALWAYS_TAG if match_always else None
|
match_always = common.MATCH_ALWAYS_TAG if match_always else None
|
||||||
|
|
||||||
# Iterate over our loaded plugins
|
# Iterate over our loaded plugins
|
||||||
for entry in self.servers:
|
for entry in self.servers:
|
||||||
|
@ -338,23 +336,24 @@ class Apprise(object):
|
||||||
for server in servers:
|
for server in servers:
|
||||||
# Apply our tag matching based on our defined logic
|
# Apply our tag matching based on our defined logic
|
||||||
if is_exclusive_match(
|
if is_exclusive_match(
|
||||||
logic=tag, data=server.tags, match_all=MATCH_ALL_TAG,
|
logic=tag, data=server.tags,
|
||||||
|
match_all=common.MATCH_ALL_TAG,
|
||||||
match_always=match_always):
|
match_always=match_always):
|
||||||
yield server
|
yield server
|
||||||
return
|
return
|
||||||
|
|
||||||
def notify(self, body, title='', notify_type=NotifyType.INFO,
|
def notify(self, body, title='', notify_type=common.NotifyType.INFO,
|
||||||
body_format=None, tag=MATCH_ALL_TAG, match_always=True,
|
body_format=None, tag=common.MATCH_ALL_TAG, match_always=True,
|
||||||
attach=None, interpret_escapes=None):
|
attach=None, interpret_escapes=None):
|
||||||
"""
|
"""
|
||||||
Send a notification to all of the plugins previously loaded.
|
Send a notification to all the plugins previously loaded.
|
||||||
|
|
||||||
If the body_format specified is NotifyFormat.MARKDOWN, it will
|
If the body_format specified is NotifyFormat.MARKDOWN, it will
|
||||||
be converted to HTML if the Notification type expects this.
|
be converted to HTML if the Notification type expects this.
|
||||||
|
|
||||||
if the tag is specified (either a string or a set/list/tuple
|
if the tag is specified (either a string or a set/list/tuple
|
||||||
of strings), then only the notifications flagged with that
|
of strings), then only the notifications flagged with that
|
||||||
tagged value are notified. By default all added services
|
tagged value are notified. By default, all added services
|
||||||
are notified (tag=MATCH_ALL_TAG)
|
are notified (tag=MATCH_ALL_TAG)
|
||||||
|
|
||||||
This function returns True if all notifications were successfully
|
This function returns True if all notifications were successfully
|
||||||
|
@ -363,60 +362,33 @@ class Apprise(object):
|
||||||
simply having empty configuration files that were read.
|
simply having empty configuration files that were read.
|
||||||
|
|
||||||
Attach can contain a list of attachment URLs. attach can also be
|
Attach can contain a list of attachment URLs. attach can also be
|
||||||
represented by a an AttachBase() (or list of) object(s). This
|
represented by an AttachBase() (or list of) object(s). This
|
||||||
identifies the products you wish to notify
|
identifies the products you wish to notify
|
||||||
|
|
||||||
Set interpret_escapes to True if you want to pre-escape a string
|
Set interpret_escapes to True if you want to pre-escape a string
|
||||||
such as turning a \n into an actual new line, etc.
|
such as turning a \n into an actual new line, etc.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if ASYNCIO_SUPPORT:
|
return py3compat.asyncio.tosync(
|
||||||
return py3compat.asyncio.tosync(
|
self.async_notify(
|
||||||
self.async_notify(
|
body, title,
|
||||||
body, title,
|
notify_type=notify_type, body_format=body_format,
|
||||||
notify_type=notify_type, body_format=body_format,
|
tag=tag, match_always=match_always, attach=attach,
|
||||||
tag=tag, match_always=match_always, attach=attach,
|
interpret_escapes=interpret_escapes,
|
||||||
interpret_escapes=interpret_escapes,
|
),
|
||||||
),
|
debug=self.debug
|
||||||
debug=self.debug
|
)
|
||||||
)
|
|
||||||
|
|
||||||
else:
|
|
||||||
try:
|
|
||||||
results = list(
|
|
||||||
self._notifyall(
|
|
||||||
Apprise._notifyhandler,
|
|
||||||
body, title,
|
|
||||||
notify_type=notify_type, body_format=body_format,
|
|
||||||
tag=tag, attach=attach,
|
|
||||||
interpret_escapes=interpret_escapes,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
except TypeError:
|
|
||||||
# No notifications sent, and there was an internal error.
|
|
||||||
return False
|
|
||||||
|
|
||||||
else:
|
|
||||||
if len(results) > 0:
|
|
||||||
# All notifications sent, return False if any failed.
|
|
||||||
return all(results)
|
|
||||||
|
|
||||||
else:
|
|
||||||
# No notifications sent.
|
|
||||||
return None
|
|
||||||
|
|
||||||
def async_notify(self, *args, **kwargs):
|
def async_notify(self, *args, **kwargs):
|
||||||
"""
|
"""
|
||||||
Send a notification to all of the plugins previously loaded, for
|
Send a notification to all the plugins previously loaded, for
|
||||||
asynchronous callers. This method is an async method that should be
|
asynchronous callers. This method is an async method that should be
|
||||||
awaited on, even if it is missing the async keyword in its signature.
|
awaited on, even if it is missing the async keyword in its signature.
|
||||||
(This is omitted to preserve syntax compatibility with Python 2.)
|
(This is omitted to preserve syntax compatibility with Python 2.)
|
||||||
|
|
||||||
The arguments are identical to those of Apprise.notify(). This method
|
The arguments are identical to those of Apprise.notify().
|
||||||
is not available in Python 2.
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
"""
|
||||||
try:
|
try:
|
||||||
coroutines = list(
|
coroutines = list(
|
||||||
self._notifyall(
|
self._notifyall(
|
||||||
|
@ -424,7 +396,7 @@ class Apprise(object):
|
||||||
|
|
||||||
except TypeError:
|
except TypeError:
|
||||||
# No notifications sent, and there was an internal error.
|
# No notifications sent, and there was an internal error.
|
||||||
return py3compat.asyncio.toasyncwrap(False)
|
return py3compat.asyncio.toasyncwrapvalue(False)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
if len(coroutines) > 0:
|
if len(coroutines) > 0:
|
||||||
|
@ -433,7 +405,7 @@ class Apprise(object):
|
||||||
|
|
||||||
else:
|
else:
|
||||||
# No notifications sent.
|
# No notifications sent.
|
||||||
return py3compat.asyncio.toasyncwrap(None)
|
return py3compat.asyncio.toasyncwrapvalue(None)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _notifyhandler(server, **kwargs):
|
def _notifyhandler(server, **kwargs):
|
||||||
|
@ -470,13 +442,14 @@ class Apprise(object):
|
||||||
# Send the notification immediately, and wrap the result in a
|
# Send the notification immediately, and wrap the result in a
|
||||||
# coroutine.
|
# coroutine.
|
||||||
status = Apprise._notifyhandler(server, **kwargs)
|
status = Apprise._notifyhandler(server, **kwargs)
|
||||||
return py3compat.asyncio.toasyncwrap(status)
|
return py3compat.asyncio.toasyncwrapvalue(status)
|
||||||
|
|
||||||
def _notifyall(self, handler, body, title='', notify_type=NotifyType.INFO,
|
def _notifyall(self, handler, body, title='',
|
||||||
body_format=None, tag=MATCH_ALL_TAG, match_always=True,
|
notify_type=common.NotifyType.INFO, body_format=None,
|
||||||
attach=None, interpret_escapes=None):
|
tag=common.MATCH_ALL_TAG, match_always=True, attach=None,
|
||||||
|
interpret_escapes=None):
|
||||||
"""
|
"""
|
||||||
Creates notifications for all of the plugins loaded.
|
Creates notifications for all the plugins loaded.
|
||||||
|
|
||||||
Returns a generator that calls handler for each notification. The first
|
Returns a generator that calls handler for each notification. The first
|
||||||
and only argument supplied to handler is the server, and the keyword
|
and only argument supplied to handler is the server, and the keyword
|
||||||
|
@ -485,7 +458,7 @@ class Apprise(object):
|
||||||
|
|
||||||
if len(self) == 0:
|
if len(self) == 0:
|
||||||
# Nothing to notify
|
# Nothing to notify
|
||||||
msg = "There are service(s) to notify"
|
msg = "There are no service(s) to notify"
|
||||||
logger.error(msg)
|
logger.error(msg)
|
||||||
raise TypeError(msg)
|
raise TypeError(msg)
|
||||||
|
|
||||||
|
@ -495,23 +468,11 @@ class Apprise(object):
|
||||||
raise TypeError(msg)
|
raise TypeError(msg)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if six.PY2:
|
if title and isinstance(title, bytes):
|
||||||
# Python 2.7 encoding support isn't the greatest, so we try
|
title = title.decode(self.asset.encoding)
|
||||||
# to ensure that we're ALWAYS dealing with unicode characters
|
|
||||||
# prior to entrying the next part. This is especially required
|
|
||||||
# for Markdown support
|
|
||||||
if title and isinstance(title, str): # noqa: F821
|
|
||||||
title = title.decode(self.asset.encoding)
|
|
||||||
|
|
||||||
if body and isinstance(body, str): # noqa: F821
|
if body and isinstance(body, bytes):
|
||||||
body = body.decode(self.asset.encoding)
|
body = body.decode(self.asset.encoding)
|
||||||
|
|
||||||
else: # Python 3+
|
|
||||||
if title and isinstance(title, bytes): # noqa: F821
|
|
||||||
title = title.decode(self.asset.encoding)
|
|
||||||
|
|
||||||
if body and isinstance(body, bytes): # noqa: F821
|
|
||||||
body = body.decode(self.asset.encoding)
|
|
||||||
|
|
||||||
except UnicodeDecodeError:
|
except UnicodeDecodeError:
|
||||||
msg = 'The content passed into Apprise was not of encoding ' \
|
msg = 'The content passed into Apprise was not of encoding ' \
|
||||||
|
@ -579,43 +540,12 @@ class Apprise(object):
|
||||||
.encode('ascii', 'backslashreplace')\
|
.encode('ascii', 'backslashreplace')\
|
||||||
.decode('unicode-escape')
|
.decode('unicode-escape')
|
||||||
|
|
||||||
except UnicodeDecodeError: # pragma: no cover
|
|
||||||
# This occurs using a very old verion of Python 2.7
|
|
||||||
# such as the one that ships with CentOS/RedHat 7.x
|
|
||||||
# (v2.7.5).
|
|
||||||
conversion_body_map[server.notify_format] = \
|
|
||||||
conversion_body_map[server.notify_format] \
|
|
||||||
.decode('string_escape')
|
|
||||||
|
|
||||||
conversion_title_map[server.notify_format] = \
|
|
||||||
conversion_title_map[server.notify_format] \
|
|
||||||
.decode('string_escape')
|
|
||||||
|
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
# Must be of string type
|
# Must be of string type
|
||||||
msg = 'Failed to escape message body'
|
msg = 'Failed to escape message body'
|
||||||
logger.error(msg)
|
logger.error(msg)
|
||||||
raise TypeError(msg)
|
raise TypeError(msg)
|
||||||
|
|
||||||
if six.PY2:
|
|
||||||
# Python 2.7 strings must be encoded as utf-8 for
|
|
||||||
# consistency across all platforms
|
|
||||||
if conversion_body_map[server.notify_format] and \
|
|
||||||
isinstance(
|
|
||||||
conversion_body_map[server.notify_format],
|
|
||||||
unicode): # noqa: F821
|
|
||||||
conversion_body_map[server.notify_format] = \
|
|
||||||
conversion_body_map[server.notify_format]\
|
|
||||||
.encode('utf-8')
|
|
||||||
|
|
||||||
if conversion_title_map[server.notify_format] and \
|
|
||||||
isinstance(
|
|
||||||
conversion_title_map[server.notify_format],
|
|
||||||
unicode): # noqa: F821
|
|
||||||
conversion_title_map[server.notify_format] = \
|
|
||||||
conversion_title_map[server.notify_format]\
|
|
||||||
.encode('utf-8')
|
|
||||||
|
|
||||||
yield handler(
|
yield handler(
|
||||||
server,
|
server,
|
||||||
body=conversion_body_map[server.notify_format],
|
body=conversion_body_map[server.notify_format],
|
||||||
|
@ -641,7 +571,7 @@ class Apprise(object):
|
||||||
'asset': self.asset.details(),
|
'asset': self.asset.details(),
|
||||||
}
|
}
|
||||||
|
|
||||||
for plugin in set(plugins.SCHEMA_MAP.values()):
|
for plugin in set(common.NOTIFY_SCHEMA_MAP.values()):
|
||||||
# Iterate over our hashed plugins and dynamically build details on
|
# Iterate over our hashed plugins and dynamically build details on
|
||||||
# their status:
|
# their status:
|
||||||
|
|
||||||
|
@ -650,7 +580,10 @@ class Apprise(object):
|
||||||
'service_url': getattr(plugin, 'service_url', None),
|
'service_url': getattr(plugin, 'service_url', None),
|
||||||
'setup_url': getattr(plugin, 'setup_url', None),
|
'setup_url': getattr(plugin, 'setup_url', None),
|
||||||
# Placeholder - populated below
|
# Placeholder - populated below
|
||||||
'details': None
|
'details': None,
|
||||||
|
# Differentiat between what is a custom loaded plugin and
|
||||||
|
# which is native.
|
||||||
|
'category': getattr(plugin, 'category', None)
|
||||||
}
|
}
|
||||||
|
|
||||||
# Standard protocol(s) should be None or a tuple
|
# Standard protocol(s) should be None or a tuple
|
||||||
|
@ -665,12 +598,12 @@ class Apprise(object):
|
||||||
|
|
||||||
# Standard protocol(s) should be None or a tuple
|
# Standard protocol(s) should be None or a tuple
|
||||||
protocols = getattr(plugin, 'protocol', None)
|
protocols = getattr(plugin, 'protocol', None)
|
||||||
if isinstance(protocols, six.string_types):
|
if isinstance(protocols, str):
|
||||||
protocols = (protocols, )
|
protocols = (protocols, )
|
||||||
|
|
||||||
# Secure protocol(s) should be None or a tuple
|
# Secure protocol(s) should be None or a tuple
|
||||||
secure_protocols = getattr(plugin, 'secure_protocol', None)
|
secure_protocols = getattr(plugin, 'secure_protocol', None)
|
||||||
if isinstance(secure_protocols, six.string_types):
|
if isinstance(secure_protocols, str):
|
||||||
secure_protocols = (secure_protocols, )
|
secure_protocols = (secure_protocols, )
|
||||||
|
|
||||||
# Add our protocol details to our content
|
# Add our protocol details to our content
|
||||||
|
@ -775,15 +708,8 @@ class Apprise(object):
|
||||||
|
|
||||||
def __bool__(self):
|
def __bool__(self):
|
||||||
"""
|
"""
|
||||||
Allows the Apprise object to be wrapped in an Python 3.x based 'if
|
Allows the Apprise object to be wrapped in an 'if statement'.
|
||||||
statement'. True is returned if at least one service has been loaded.
|
True is returned if at least one service has been loaded.
|
||||||
"""
|
|
||||||
return len(self) > 0
|
|
||||||
|
|
||||||
def __nonzero__(self):
|
|
||||||
"""
|
|
||||||
Allows the Apprise object to be wrapped in an Python 2.x based 'if
|
|
||||||
statement'. True is returned if at least one service has been loaded.
|
|
||||||
"""
|
"""
|
||||||
return len(self) > 0
|
return len(self) > 0
|
||||||
|
|
||||||
|
@ -803,7 +729,3 @@ class Apprise(object):
|
||||||
"""
|
"""
|
||||||
return sum([1 if not isinstance(s, (ConfigBase, AppriseConfig))
|
return sum([1 if not isinstance(s, (ConfigBase, AppriseConfig))
|
||||||
else len(s.servers()) for s in self.servers])
|
else len(s.servers()) for s in self.servers])
|
||||||
|
|
||||||
|
|
||||||
if six.PY2:
|
|
||||||
del Apprise.async_notify
|
|
||||||
|
|
|
@ -58,6 +58,5 @@ class Apprise:
|
||||||
def pop(self, index: int) -> ConfigBase: ...
|
def pop(self, index: int) -> ConfigBase: ...
|
||||||
def __getitem__(self, index: int) -> ConfigBase: ...
|
def __getitem__(self, index: int) -> ConfigBase: ...
|
||||||
def __bool__(self) -> bool: ...
|
def __bool__(self) -> bool: ...
|
||||||
def __nonzero__(self) -> bool: ...
|
|
||||||
def __iter__(self) -> Iterator[ConfigBase]: ...
|
def __iter__(self) -> Iterator[ConfigBase]: ...
|
||||||
def __len__(self) -> int: ...
|
def __len__(self) -> int: ...
|
|
@ -30,15 +30,20 @@ from os.path import dirname
|
||||||
from os.path import isfile
|
from os.path import isfile
|
||||||
from os.path import abspath
|
from os.path import abspath
|
||||||
from .common import NotifyType
|
from .common import NotifyType
|
||||||
|
from .utils import module_detection
|
||||||
|
|
||||||
|
|
||||||
class AppriseAsset(object):
|
class AppriseAsset:
|
||||||
"""
|
"""
|
||||||
Provides a supplimentary class that can be used to provide extra
|
Provides a supplimentary class that can be used to provide extra
|
||||||
information and details that can be used by Apprise such as providing
|
information and details that can be used by Apprise such as providing
|
||||||
an alternate location to where images/icons can be found and the
|
an alternate location to where images/icons can be found and the
|
||||||
URL masks.
|
URL masks.
|
||||||
|
|
||||||
|
Any variable that starts with an underscore (_) can only be initialized
|
||||||
|
by this class manually and will/can not be parsed from a configuration
|
||||||
|
file.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
# Application Identifier
|
# Application Identifier
|
||||||
app_id = 'Apprise'
|
app_id = 'Apprise'
|
||||||
|
@ -102,8 +107,8 @@ class AppriseAsset(object):
|
||||||
# - NotifyFormat.HTML
|
# - NotifyFormat.HTML
|
||||||
# - None
|
# - None
|
||||||
#
|
#
|
||||||
# If no format is specified (hence None), then no special pre-formating
|
# If no format is specified (hence None), then no special pre-formatting
|
||||||
# actions will take place during a notificaton. This has been and always
|
# actions will take place during a notification. This has been and always
|
||||||
# will be the default.
|
# will be the default.
|
||||||
body_format = None
|
body_format = None
|
||||||
|
|
||||||
|
@ -132,6 +137,10 @@ class AppriseAsset(object):
|
||||||
# that you leave this option as is otherwise.
|
# that you leave this option as is otherwise.
|
||||||
secure_logging = True
|
secure_logging = True
|
||||||
|
|
||||||
|
# Optionally specify one or more path to attempt to scan for Python modules
|
||||||
|
# By default, no paths are scanned.
|
||||||
|
__plugin_paths = []
|
||||||
|
|
||||||
# All internal/system flags are prefixed with an underscore (_)
|
# All internal/system flags are prefixed with an underscore (_)
|
||||||
# These can only be initialized using Python libraries and are not picked
|
# These can only be initialized using Python libraries and are not picked
|
||||||
# up from (yaml) configuration files (if set)
|
# up from (yaml) configuration files (if set)
|
||||||
|
@ -146,7 +155,7 @@ class AppriseAsset(object):
|
||||||
# A unique identifer we can use to associate our calling source
|
# A unique identifer we can use to associate our calling source
|
||||||
_uid = str(uuid4())
|
_uid = str(uuid4())
|
||||||
|
|
||||||
def __init__(self, **kwargs):
|
def __init__(self, plugin_paths=None, **kwargs):
|
||||||
"""
|
"""
|
||||||
Asset Initialization
|
Asset Initialization
|
||||||
|
|
||||||
|
@ -160,6 +169,10 @@ class AppriseAsset(object):
|
||||||
|
|
||||||
setattr(self, key, value)
|
setattr(self, key, value)
|
||||||
|
|
||||||
|
if plugin_paths:
|
||||||
|
# Load any decorated modules if defined
|
||||||
|
module_detection(plugin_paths)
|
||||||
|
|
||||||
def color(self, notify_type, color_type=None):
|
def color(self, notify_type, color_type=None):
|
||||||
"""
|
"""
|
||||||
Returns an HTML mapped color based on passed in notify type
|
Returns an HTML mapped color based on passed in notify type
|
||||||
|
|
|
@ -23,18 +23,17 @@
|
||||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
# THE SOFTWARE.
|
# THE SOFTWARE.
|
||||||
|
|
||||||
import six
|
|
||||||
|
|
||||||
from . import attachment
|
from . import attachment
|
||||||
from . import URLBase
|
from . import URLBase
|
||||||
from .AppriseAsset import AppriseAsset
|
from .AppriseAsset import AppriseAsset
|
||||||
from .logger import logger
|
from .logger import logger
|
||||||
from .common import ContentLocation
|
from .common import ContentLocation
|
||||||
from .common import CONTENT_LOCATIONS
|
from .common import CONTENT_LOCATIONS
|
||||||
|
from .common import ATTACHMENT_SCHEMA_MAP
|
||||||
from .utils import GET_SCHEMA_RE
|
from .utils import GET_SCHEMA_RE
|
||||||
|
|
||||||
|
|
||||||
class AppriseAttachment(object):
|
class AppriseAttachment:
|
||||||
"""
|
"""
|
||||||
Our Apprise Attachment File Manager
|
Our Apprise Attachment File Manager
|
||||||
|
|
||||||
|
@ -142,7 +141,7 @@ class AppriseAttachment(object):
|
||||||
self.attachments.append(attachments)
|
self.attachments.append(attachments)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
elif isinstance(attachments, six.string_types):
|
elif isinstance(attachments, str):
|
||||||
# Save our path
|
# Save our path
|
||||||
attachments = (attachments, )
|
attachments = (attachments, )
|
||||||
|
|
||||||
|
@ -161,7 +160,7 @@ class AppriseAttachment(object):
|
||||||
return_status = False
|
return_status = False
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if isinstance(_attachment, six.string_types):
|
if isinstance(_attachment, str):
|
||||||
logger.debug("Loading attachment: {}".format(_attachment))
|
logger.debug("Loading attachment: {}".format(_attachment))
|
||||||
# Instantiate ourselves an object, this function throws or
|
# Instantiate ourselves an object, this function throws or
|
||||||
# returns None if it fails
|
# returns None if it fails
|
||||||
|
@ -225,13 +224,13 @@ class AppriseAttachment(object):
|
||||||
schema = schema.group('schema').lower()
|
schema = schema.group('schema').lower()
|
||||||
|
|
||||||
# Some basic validation
|
# Some basic validation
|
||||||
if schema not in attachment.SCHEMA_MAP:
|
if schema not in ATTACHMENT_SCHEMA_MAP:
|
||||||
logger.warning('Unsupported schema {}.'.format(schema))
|
logger.warning('Unsupported schema {}.'.format(schema))
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# Parse our url details of the server object as dictionary containing
|
# Parse our url details of the server object as dictionary containing
|
||||||
# all of the information parsed from our URL
|
# all of the information parsed from our URL
|
||||||
results = attachment.SCHEMA_MAP[schema].parse_url(url)
|
results = ATTACHMENT_SCHEMA_MAP[schema].parse_url(url)
|
||||||
|
|
||||||
if not results:
|
if not results:
|
||||||
# Failed to parse the server URL
|
# Failed to parse the server URL
|
||||||
|
@ -251,7 +250,7 @@ class AppriseAttachment(object):
|
||||||
# Attempt to create an instance of our plugin using the parsed
|
# Attempt to create an instance of our plugin using the parsed
|
||||||
# URL information
|
# URL information
|
||||||
attach_plugin = \
|
attach_plugin = \
|
||||||
attachment.SCHEMA_MAP[results['schema']](**results)
|
ATTACHMENT_SCHEMA_MAP[results['schema']](**results)
|
||||||
|
|
||||||
except Exception:
|
except Exception:
|
||||||
# the arguments are invalid or can not be used.
|
# the arguments are invalid or can not be used.
|
||||||
|
@ -261,7 +260,7 @@ class AppriseAttachment(object):
|
||||||
else:
|
else:
|
||||||
# Attempt to create an instance of our plugin using the parsed
|
# Attempt to create an instance of our plugin using the parsed
|
||||||
# URL information but don't wrap it in a try catch
|
# URL information but don't wrap it in a try catch
|
||||||
attach_plugin = attachment.SCHEMA_MAP[results['schema']](**results)
|
attach_plugin = ATTACHMENT_SCHEMA_MAP[results['schema']](**results)
|
||||||
|
|
||||||
return attach_plugin
|
return attach_plugin
|
||||||
|
|
||||||
|
@ -295,15 +294,8 @@ class AppriseAttachment(object):
|
||||||
|
|
||||||
def __bool__(self):
|
def __bool__(self):
|
||||||
"""
|
"""
|
||||||
Allows the Apprise object to be wrapped in an Python 3.x based 'if
|
Allows the Apprise object to be wrapped in an 'if statement'.
|
||||||
statement'. True is returned if at least one service has been loaded.
|
True is returned if at least one service has been loaded.
|
||||||
"""
|
|
||||||
return True if self.attachments else False
|
|
||||||
|
|
||||||
def __nonzero__(self):
|
|
||||||
"""
|
|
||||||
Allows the Apprise object to be wrapped in an Python 2.x based 'if
|
|
||||||
statement'. True is returned if at least one service has been loaded.
|
|
||||||
"""
|
"""
|
||||||
return True if self.attachments else False
|
return True if self.attachments else False
|
||||||
|
|
||||||
|
|
|
@ -33,6 +33,5 @@ class AppriseAttachment:
|
||||||
def pop(self, index: int = ...) -> AttachBase: ...
|
def pop(self, index: int = ...) -> AttachBase: ...
|
||||||
def __getitem__(self, index: int) -> AttachBase: ...
|
def __getitem__(self, index: int) -> AttachBase: ...
|
||||||
def __bool__(self) -> bool: ...
|
def __bool__(self) -> bool: ...
|
||||||
def __nonzero__(self) -> bool: ...
|
|
||||||
def __iter__(self) -> Iterator[AttachBase]: ...
|
def __iter__(self) -> Iterator[AttachBase]: ...
|
||||||
def __len__(self) -> int: ...
|
def __len__(self) -> int: ...
|
|
@ -23,23 +23,19 @@
|
||||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
# THE SOFTWARE.
|
# THE SOFTWARE.
|
||||||
|
|
||||||
import six
|
|
||||||
|
|
||||||
from . import config
|
from . import config
|
||||||
from . import ConfigBase
|
from . import ConfigBase
|
||||||
from . import CONFIG_FORMATS
|
from . import CONFIG_FORMATS
|
||||||
from . import URLBase
|
from . import URLBase
|
||||||
from .AppriseAsset import AppriseAsset
|
from .AppriseAsset import AppriseAsset
|
||||||
|
from . import common
|
||||||
from .common import MATCH_ALL_TAG
|
|
||||||
from .common import MATCH_ALWAYS_TAG
|
|
||||||
from .utils import GET_SCHEMA_RE
|
from .utils import GET_SCHEMA_RE
|
||||||
from .utils import parse_list
|
from .utils import parse_list
|
||||||
from .utils import is_exclusive_match
|
from .utils import is_exclusive_match
|
||||||
from .logger import logger
|
from .logger import logger
|
||||||
|
|
||||||
|
|
||||||
class AppriseConfig(object):
|
class AppriseConfig:
|
||||||
"""
|
"""
|
||||||
Our Apprise Configuration File Manager
|
Our Apprise Configuration File Manager
|
||||||
|
|
||||||
|
@ -171,7 +167,7 @@ class AppriseConfig(object):
|
||||||
self.configs.append(configs)
|
self.configs.append(configs)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
elif isinstance(configs, six.string_types):
|
elif isinstance(configs, str):
|
||||||
# Save our path
|
# Save our path
|
||||||
configs = (configs, )
|
configs = (configs, )
|
||||||
|
|
||||||
|
@ -189,7 +185,7 @@ class AppriseConfig(object):
|
||||||
self.configs.append(_config)
|
self.configs.append(_config)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
elif not isinstance(_config, six.string_types):
|
elif not isinstance(_config, str):
|
||||||
logger.warning(
|
logger.warning(
|
||||||
"An invalid configuration (type={}) was specified.".format(
|
"An invalid configuration (type={}) was specified.".format(
|
||||||
type(_config)))
|
type(_config)))
|
||||||
|
@ -243,7 +239,7 @@ class AppriseConfig(object):
|
||||||
# prepare default asset
|
# prepare default asset
|
||||||
asset = self.asset
|
asset = self.asset
|
||||||
|
|
||||||
if not isinstance(content, six.string_types):
|
if not isinstance(content, str):
|
||||||
logger.warning(
|
logger.warning(
|
||||||
"An invalid configuration (type={}) was specified.".format(
|
"An invalid configuration (type={}) was specified.".format(
|
||||||
type(content)))
|
type(content)))
|
||||||
|
@ -267,7 +263,8 @@ class AppriseConfig(object):
|
||||||
# Return our status
|
# Return our status
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def servers(self, tag=MATCH_ALL_TAG, match_always=True, *args, **kwargs):
|
def servers(self, tag=common.MATCH_ALL_TAG, match_always=True, *args,
|
||||||
|
**kwargs):
|
||||||
"""
|
"""
|
||||||
Returns all of our servers dynamically build based on parsed
|
Returns all of our servers dynamically build based on parsed
|
||||||
configuration.
|
configuration.
|
||||||
|
@ -285,7 +282,7 @@ class AppriseConfig(object):
|
||||||
|
|
||||||
# A match_always flag allows us to pick up on our 'any' keyword
|
# A match_always flag allows us to pick up on our 'any' keyword
|
||||||
# and notify these services under all circumstances
|
# and notify these services under all circumstances
|
||||||
match_always = MATCH_ALWAYS_TAG if match_always else None
|
match_always = common.MATCH_ALWAYS_TAG if match_always else None
|
||||||
|
|
||||||
# Build our tag setup
|
# Build our tag setup
|
||||||
# - top level entries are treated as an 'or'
|
# - top level entries are treated as an 'or'
|
||||||
|
@ -303,7 +300,7 @@ class AppriseConfig(object):
|
||||||
|
|
||||||
# Apply our tag matching based on our defined logic
|
# Apply our tag matching based on our defined logic
|
||||||
if is_exclusive_match(
|
if is_exclusive_match(
|
||||||
logic=tag, data=entry.tags, match_all=MATCH_ALL_TAG,
|
logic=tag, data=entry.tags, match_all=common.MATCH_ALL_TAG,
|
||||||
match_always=match_always):
|
match_always=match_always):
|
||||||
# Build ourselves a list of services dynamically and return the
|
# Build ourselves a list of services dynamically and return the
|
||||||
# as a list
|
# as a list
|
||||||
|
@ -334,13 +331,13 @@ class AppriseConfig(object):
|
||||||
schema = schema.group('schema').lower()
|
schema = schema.group('schema').lower()
|
||||||
|
|
||||||
# Some basic validation
|
# Some basic validation
|
||||||
if schema not in config.SCHEMA_MAP:
|
if schema not in common.CONFIG_SCHEMA_MAP:
|
||||||
logger.warning('Unsupported schema {}.'.format(schema))
|
logger.warning('Unsupported schema {}.'.format(schema))
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# Parse our url details of the server object as dictionary containing
|
# Parse our url details of the server object as dictionary containing
|
||||||
# all of the information parsed from our URL
|
# all of the information parsed from our URL
|
||||||
results = config.SCHEMA_MAP[schema].parse_url(url)
|
results = common.CONFIG_SCHEMA_MAP[schema].parse_url(url)
|
||||||
|
|
||||||
if not results:
|
if not results:
|
||||||
# Failed to parse the server URL
|
# Failed to parse the server URL
|
||||||
|
@ -368,7 +365,8 @@ class AppriseConfig(object):
|
||||||
try:
|
try:
|
||||||
# Attempt to create an instance of our plugin using the parsed
|
# Attempt to create an instance of our plugin using the parsed
|
||||||
# URL information
|
# URL information
|
||||||
cfg_plugin = config.SCHEMA_MAP[results['schema']](**results)
|
cfg_plugin = \
|
||||||
|
common.CONFIG_SCHEMA_MAP[results['schema']](**results)
|
||||||
|
|
||||||
except Exception:
|
except Exception:
|
||||||
# the arguments are invalid or can not be used.
|
# the arguments are invalid or can not be used.
|
||||||
|
@ -378,7 +376,7 @@ class AppriseConfig(object):
|
||||||
else:
|
else:
|
||||||
# Attempt to create an instance of our plugin using the parsed
|
# Attempt to create an instance of our plugin using the parsed
|
||||||
# URL information but don't wrap it in a try catch
|
# URL information but don't wrap it in a try catch
|
||||||
cfg_plugin = config.SCHEMA_MAP[results['schema']](**results)
|
cfg_plugin = common.CONFIG_SCHEMA_MAP[results['schema']](**results)
|
||||||
|
|
||||||
return cfg_plugin
|
return cfg_plugin
|
||||||
|
|
||||||
|
@ -432,15 +430,8 @@ class AppriseConfig(object):
|
||||||
|
|
||||||
def __bool__(self):
|
def __bool__(self):
|
||||||
"""
|
"""
|
||||||
Allows the Apprise object to be wrapped in an Python 3.x based 'if
|
Allows the Apprise object to be wrapped in an 'if statement'.
|
||||||
statement'. True is returned if at least one service has been loaded.
|
True is returned if at least one service has been loaded.
|
||||||
"""
|
|
||||||
return True if self.configs else False
|
|
||||||
|
|
||||||
def __nonzero__(self):
|
|
||||||
"""
|
|
||||||
Allows the Apprise object to be wrapped in an Python 2.x based 'if
|
|
||||||
statement'. True is returned if at least one service has been loaded.
|
|
||||||
"""
|
"""
|
||||||
return True if self.configs else False
|
return True if self.configs else False
|
||||||
|
|
||||||
|
|
|
@ -44,6 +44,5 @@ class AppriseConfig:
|
||||||
def pop(self, index: int = ...) -> ConfigBase: ...
|
def pop(self, index: int = ...) -> ConfigBase: ...
|
||||||
def __getitem__(self, index: int) -> ConfigBase: ...
|
def __getitem__(self, index: int) -> ConfigBase: ...
|
||||||
def __bool__(self) -> bool: ...
|
def __bool__(self) -> bool: ...
|
||||||
def __nonzero__(self) -> bool: ...
|
|
||||||
def __iter__(self) -> Iterator[ConfigBase]: ...
|
def __iter__(self) -> Iterator[ConfigBase]: ...
|
||||||
def __len__(self) -> int: ...
|
def __len__(self) -> int: ...
|
|
@ -23,7 +23,6 @@
|
||||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
# THE SOFTWARE.
|
# THE SOFTWARE.
|
||||||
|
|
||||||
import six
|
|
||||||
import ctypes
|
import ctypes
|
||||||
import locale
|
import locale
|
||||||
import contextlib
|
import contextlib
|
||||||
|
@ -52,18 +51,11 @@ try:
|
||||||
except ImportError:
|
except ImportError:
|
||||||
# gettext isn't available; no problem, just fall back to using
|
# gettext isn't available; no problem, just fall back to using
|
||||||
# the library features without multi-language support.
|
# the library features without multi-language support.
|
||||||
try:
|
import builtins
|
||||||
# Python v2.7
|
builtins.__dict__['_'] = lambda x: x # pragma: no branch
|
||||||
import __builtin__
|
|
||||||
__builtin__.__dict__['_'] = lambda x: x # pragma: no branch
|
|
||||||
|
|
||||||
except ImportError:
|
|
||||||
# Python v3.4+
|
|
||||||
import builtins
|
|
||||||
builtins.__dict__['_'] = lambda x: x # pragma: no branch
|
|
||||||
|
|
||||||
|
|
||||||
class LazyTranslation(object):
|
class LazyTranslation:
|
||||||
"""
|
"""
|
||||||
Doesn't translate anything until str() or unicode() references
|
Doesn't translate anything until str() or unicode() references
|
||||||
are made.
|
are made.
|
||||||
|
@ -89,7 +81,7 @@ def gettext_lazy(text):
|
||||||
return LazyTranslation(text=text)
|
return LazyTranslation(text=text)
|
||||||
|
|
||||||
|
|
||||||
class AppriseLocale(object):
|
class AppriseLocale:
|
||||||
"""
|
"""
|
||||||
A wrapper class to gettext so that we can manipulate multiple lanaguages
|
A wrapper class to gettext so that we can manipulate multiple lanaguages
|
||||||
on the fly if required.
|
on the fly if required.
|
||||||
|
@ -186,7 +178,7 @@ class AppriseLocale(object):
|
||||||
"""
|
"""
|
||||||
# We want to only use the 2 character version of this language
|
# We want to only use the 2 character version of this language
|
||||||
# hence en_CA becomes en, en_US becomes en.
|
# hence en_CA becomes en, en_US becomes en.
|
||||||
if not isinstance(lang, six.string_types):
|
if not isinstance(lang, str):
|
||||||
if detect_fallback is False:
|
if detect_fallback is False:
|
||||||
# no detection enabled; we're done
|
# no detection enabled; we're done
|
||||||
return None
|
return None
|
||||||
|
|
|
@ -24,26 +24,17 @@
|
||||||
# THE SOFTWARE.
|
# THE SOFTWARE.
|
||||||
|
|
||||||
import re
|
import re
|
||||||
import six
|
|
||||||
from .logger import logger
|
from .logger import logger
|
||||||
from time import sleep
|
from time import sleep
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from xml.sax.saxutils import escape as sax_escape
|
from xml.sax.saxutils import escape as sax_escape
|
||||||
|
|
||||||
try:
|
from urllib.parse import unquote as _unquote
|
||||||
# Python 2.7
|
from urllib.parse import quote as _quote
|
||||||
from urllib import unquote as _unquote
|
|
||||||
from urllib import quote as _quote
|
|
||||||
from urllib import urlencode as _urlencode
|
|
||||||
|
|
||||||
except ImportError:
|
|
||||||
# Python 3.x
|
|
||||||
from urllib.parse import unquote as _unquote
|
|
||||||
from urllib.parse import quote as _quote
|
|
||||||
from urllib.parse import urlencode as _urlencode
|
|
||||||
|
|
||||||
from .AppriseLocale import gettext_lazy as _
|
from .AppriseLocale import gettext_lazy as _
|
||||||
from .AppriseAsset import AppriseAsset
|
from .AppriseAsset import AppriseAsset
|
||||||
|
from .utils import urlencode
|
||||||
from .utils import parse_url
|
from .utils import parse_url
|
||||||
from .utils import parse_bool
|
from .utils import parse_bool
|
||||||
from .utils import parse_list
|
from .utils import parse_list
|
||||||
|
@ -53,7 +44,7 @@ from .utils import parse_phone_no
|
||||||
PATHSPLIT_LIST_DELIM = re.compile(r'[ \t\r\n,\\/]+')
|
PATHSPLIT_LIST_DELIM = re.compile(r'[ \t\r\n,\\/]+')
|
||||||
|
|
||||||
|
|
||||||
class PrivacyMode(object):
|
class PrivacyMode:
|
||||||
# Defines different privacy modes strings can be printed as
|
# Defines different privacy modes strings can be printed as
|
||||||
# Astrisk sets 4 of them: e.g. ****
|
# Astrisk sets 4 of them: e.g. ****
|
||||||
# This is used for passwords
|
# This is used for passwords
|
||||||
|
@ -78,7 +69,7 @@ HTML_LOOKUP = {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class URLBase(object):
|
class URLBase:
|
||||||
"""
|
"""
|
||||||
This is the base class for all URL Manipulation
|
This is the base class for all URL Manipulation
|
||||||
"""
|
"""
|
||||||
|
@ -346,7 +337,7 @@ class URLBase(object):
|
||||||
Returns:
|
Returns:
|
||||||
str: The escaped html
|
str: The escaped html
|
||||||
"""
|
"""
|
||||||
if not isinstance(html, six.string_types) or not html:
|
if not isinstance(html, str) or not html:
|
||||||
return ''
|
return ''
|
||||||
|
|
||||||
# Escape HTML
|
# Escape HTML
|
||||||
|
@ -359,7 +350,7 @@ class URLBase(object):
|
||||||
.replace(u' ', u' ')
|
.replace(u' ', u' ')
|
||||||
|
|
||||||
if convert_new_lines:
|
if convert_new_lines:
|
||||||
return escaped.replace(u'\n', u'<br/>')
|
return escaped.replace(u'\n', u'<br/>')
|
||||||
|
|
||||||
return escaped
|
return escaped
|
||||||
|
|
||||||
|
@ -370,7 +361,7 @@ class URLBase(object):
|
||||||
encoding and errors parameters specify how to decode percent-encoded
|
encoding and errors parameters specify how to decode percent-encoded
|
||||||
sequences.
|
sequences.
|
||||||
|
|
||||||
Wrapper to Python's unquote while remaining compatible with both
|
Wrapper to Python's `unquote` while remaining compatible with both
|
||||||
Python 2 & 3 since the reference to this function changed between
|
Python 2 & 3 since the reference to this function changed between
|
||||||
versions.
|
versions.
|
||||||
|
|
||||||
|
@ -389,20 +380,14 @@ class URLBase(object):
|
||||||
if not content:
|
if not content:
|
||||||
return ''
|
return ''
|
||||||
|
|
||||||
try:
|
return _unquote(content, encoding=encoding, errors=errors)
|
||||||
# Python v3.x
|
|
||||||
return _unquote(content, encoding=encoding, errors=errors)
|
|
||||||
|
|
||||||
except TypeError:
|
|
||||||
# Python v2.7
|
|
||||||
return _unquote(content)
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def quote(content, safe='/', encoding=None, errors=None):
|
def quote(content, safe='/', encoding=None, errors=None):
|
||||||
""" Replaces single character non-ascii characters and URI specific
|
""" Replaces single character non-ascii characters and URI specific
|
||||||
ones by their %xx code.
|
ones by their %xx code.
|
||||||
|
|
||||||
Wrapper to Python's unquote while remaining compatible with both
|
Wrapper to Python's `quote` while remaining compatible with both
|
||||||
Python 2 & 3 since the reference to this function changed between
|
Python 2 & 3 since the reference to this function changed between
|
||||||
versions.
|
versions.
|
||||||
|
|
||||||
|
@ -422,13 +407,7 @@ class URLBase(object):
|
||||||
if not content:
|
if not content:
|
||||||
return ''
|
return ''
|
||||||
|
|
||||||
try:
|
return _quote(content, safe=safe, encoding=encoding, errors=errors)
|
||||||
# Python v3.x
|
|
||||||
return _quote(content, safe=safe, encoding=encoding, errors=errors)
|
|
||||||
|
|
||||||
except TypeError:
|
|
||||||
# Python v2.7
|
|
||||||
return _quote(content, safe=safe)
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def pprint(content, privacy=True, mode=PrivacyMode.Outer,
|
def pprint(content, privacy=True, mode=PrivacyMode.Outer,
|
||||||
|
@ -457,7 +436,7 @@ class URLBase(object):
|
||||||
# Return 4 Asterisks
|
# Return 4 Asterisks
|
||||||
return '****'
|
return '****'
|
||||||
|
|
||||||
if not isinstance(content, six.string_types) or not content:
|
if not isinstance(content, str) or not content:
|
||||||
# Nothing more to do
|
# Nothing more to do
|
||||||
return ''
|
return ''
|
||||||
|
|
||||||
|
@ -472,7 +451,7 @@ class URLBase(object):
|
||||||
def urlencode(query, doseq=False, safe='', encoding=None, errors=None):
|
def urlencode(query, doseq=False, safe='', encoding=None, errors=None):
|
||||||
"""Convert a mapping object or a sequence of two-element tuples
|
"""Convert a mapping object or a sequence of two-element tuples
|
||||||
|
|
||||||
Wrapper to Python's unquote while remaining compatible with both
|
Wrapper to Python's `urlencode` while remaining compatible with both
|
||||||
Python 2 & 3 since the reference to this function changed between
|
Python 2 & 3 since the reference to this function changed between
|
||||||
versions.
|
versions.
|
||||||
|
|
||||||
|
@ -497,17 +476,8 @@ class URLBase(object):
|
||||||
Returns:
|
Returns:
|
||||||
str: The escaped parameters returned as a string
|
str: The escaped parameters returned as a string
|
||||||
"""
|
"""
|
||||||
# Tidy query by eliminating any records set to None
|
return urlencode(
|
||||||
_query = {k: v for (k, v) in query.items() if v is not None}
|
query, doseq=doseq, safe=safe, encoding=encoding, errors=errors)
|
||||||
try:
|
|
||||||
# Python v3.x
|
|
||||||
return _urlencode(
|
|
||||||
_query, doseq=doseq, safe=safe, encoding=encoding,
|
|
||||||
errors=errors)
|
|
||||||
|
|
||||||
except TypeError:
|
|
||||||
# Python v2.7
|
|
||||||
return _urlencode(_query)
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def split_path(path, unquote=True):
|
def split_path(path, unquote=True):
|
||||||
|
@ -585,11 +555,6 @@ class URLBase(object):
|
||||||
# Nothing further to do
|
# Nothing further to do
|
||||||
return []
|
return []
|
||||||
|
|
||||||
except AttributeError:
|
|
||||||
# This exception ONLY gets thrown under Python v2.7 if an
|
|
||||||
# object() is passed in place of the content
|
|
||||||
return []
|
|
||||||
|
|
||||||
content = parse_phone_no(content)
|
content = parse_phone_no(content)
|
||||||
|
|
||||||
return content
|
return content
|
||||||
|
@ -687,6 +652,9 @@ class URLBase(object):
|
||||||
if 'cto' in results['qsd']:
|
if 'cto' in results['qsd']:
|
||||||
results['socket_connect_timeout'] = results['qsd']['cto']
|
results['socket_connect_timeout'] = results['qsd']['cto']
|
||||||
|
|
||||||
|
if 'port' in results['qsd']:
|
||||||
|
results['port'] = results['qsd']['port']
|
||||||
|
|
||||||
return results
|
return results
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
|
@ -721,13 +689,13 @@ class URLBase(object):
|
||||||
|
|
||||||
for key in ('protocol', 'secure_protocol'):
|
for key in ('protocol', 'secure_protocol'):
|
||||||
schema = getattr(self, key, None)
|
schema = getattr(self, key, None)
|
||||||
if isinstance(schema, six.string_types):
|
if isinstance(schema, str):
|
||||||
schemas.add(schema)
|
schemas.add(schema)
|
||||||
|
|
||||||
elif isinstance(schema, (set, list, tuple)):
|
elif isinstance(schema, (set, list, tuple)):
|
||||||
# Support iterables list types
|
# Support iterables list types
|
||||||
for s in schema:
|
for s in schema:
|
||||||
if isinstance(s, six.string_types):
|
if isinstance(s, str):
|
||||||
schemas.add(s)
|
schemas.add(s)
|
||||||
|
|
||||||
return schemas
|
return schemas
|
||||||
|
|
|
@ -24,7 +24,7 @@
|
||||||
# THE SOFTWARE.
|
# THE SOFTWARE.
|
||||||
|
|
||||||
__title__ = 'Apprise'
|
__title__ = 'Apprise'
|
||||||
__version__ = '0.9.8.3'
|
__version__ = '1.1.0'
|
||||||
__author__ = 'Chris Caron'
|
__author__ = 'Chris Caron'
|
||||||
__license__ = 'MIT'
|
__license__ = 'MIT'
|
||||||
__copywrite__ = 'Copyright (C) 2022 Chris Caron <lead2gold@gmail.com>'
|
__copywrite__ = 'Copyright (C) 2022 Chris Caron <lead2gold@gmail.com>'
|
||||||
|
@ -57,6 +57,8 @@ from .AppriseAsset import AppriseAsset
|
||||||
from .AppriseConfig import AppriseConfig
|
from .AppriseConfig import AppriseConfig
|
||||||
from .AppriseAttachment import AppriseAttachment
|
from .AppriseAttachment import AppriseAttachment
|
||||||
|
|
||||||
|
from . import decorators
|
||||||
|
|
||||||
# Inherit our logging with our additional entries added to it
|
# Inherit our logging with our additional entries added to it
|
||||||
from .logger import logging
|
from .logger import logging
|
||||||
from .logger import logger
|
from .logger import logger
|
||||||
|
@ -78,6 +80,9 @@ __all__ = [
|
||||||
'ContentLocation', 'CONTENT_LOCATIONS',
|
'ContentLocation', 'CONTENT_LOCATIONS',
|
||||||
'PrivacyMode',
|
'PrivacyMode',
|
||||||
|
|
||||||
|
# Decorator
|
||||||
|
'decorators',
|
||||||
|
|
||||||
# Logging
|
# Logging
|
||||||
'logging', 'logger', 'LogCapture',
|
'logging', 'logger', 'LogCapture',
|
||||||
]
|
]
|
||||||
|
|
|
@ -367,14 +367,7 @@ class AttachBase(URLBase):
|
||||||
|
|
||||||
def __bool__(self):
|
def __bool__(self):
|
||||||
"""
|
"""
|
||||||
Allows the Apprise object to be wrapped in an Python 3.x based 'if
|
Allows the Apprise object to be wrapped in an based 'if statement'.
|
||||||
statement'. True is returned if our content was downloaded correctly.
|
True is returned if our content was downloaded correctly.
|
||||||
"""
|
|
||||||
return True if self.path else False
|
|
||||||
|
|
||||||
def __nonzero__(self):
|
|
||||||
"""
|
|
||||||
Allows the Apprise object to be wrapped in an Python 2.x based 'if
|
|
||||||
statement'. True is returned if our content was downloaded correctly.
|
|
||||||
"""
|
"""
|
||||||
return True if self.path else False
|
return True if self.path else False
|
||||||
|
|
|
@ -34,4 +34,3 @@ class AttachBase:
|
||||||
) -> Dict[str, Any]: ...
|
) -> Dict[str, Any]: ...
|
||||||
def __len__(self) -> int: ...
|
def __len__(self) -> int: ...
|
||||||
def __bool__(self) -> bool: ...
|
def __bool__(self) -> bool: ...
|
||||||
def __nonzero__(self) -> bool: ...
|
|
|
@ -25,7 +25,6 @@
|
||||||
|
|
||||||
import re
|
import re
|
||||||
import os
|
import os
|
||||||
import six
|
|
||||||
import requests
|
import requests
|
||||||
from tempfile import NamedTemporaryFile
|
from tempfile import NamedTemporaryFile
|
||||||
from .AttachBase import AttachBase
|
from .AttachBase import AttachBase
|
||||||
|
@ -67,7 +66,7 @@ class AttachHTTP(AttachBase):
|
||||||
self.schema = 'https' if self.secure else 'http'
|
self.schema = 'https' if self.secure else 'http'
|
||||||
|
|
||||||
self.fullpath = kwargs.get('fullpath')
|
self.fullpath = kwargs.get('fullpath')
|
||||||
if not isinstance(self.fullpath, six.string_types):
|
if not isinstance(self.fullpath, str):
|
||||||
self.fullpath = '/'
|
self.fullpath = '/'
|
||||||
|
|
||||||
self.headers = {}
|
self.headers = {}
|
||||||
|
|
|
@ -23,15 +23,12 @@
|
||||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
# THE SOFTWARE.
|
# THE SOFTWARE.
|
||||||
|
|
||||||
import six
|
|
||||||
import re
|
import re
|
||||||
|
|
||||||
from os import listdir
|
from os import listdir
|
||||||
from os.path import dirname
|
from os.path import dirname
|
||||||
from os.path import abspath
|
from os.path import abspath
|
||||||
|
from ..common import ATTACHMENT_SCHEMA_MAP
|
||||||
# Maintains a mapping of all of the attachment services
|
|
||||||
SCHEMA_MAP = {}
|
|
||||||
|
|
||||||
__all__ = []
|
__all__ = []
|
||||||
|
|
||||||
|
@ -90,29 +87,29 @@ def __load_matrix(path=abspath(dirname(__file__)), name='apprise.attachment'):
|
||||||
|
|
||||||
# Load protocol(s) if defined
|
# Load protocol(s) if defined
|
||||||
proto = getattr(plugin, 'protocol', None)
|
proto = getattr(plugin, 'protocol', None)
|
||||||
if isinstance(proto, six.string_types):
|
if isinstance(proto, str):
|
||||||
if proto not in SCHEMA_MAP:
|
if proto not in ATTACHMENT_SCHEMA_MAP:
|
||||||
SCHEMA_MAP[proto] = plugin
|
ATTACHMENT_SCHEMA_MAP[proto] = plugin
|
||||||
|
|
||||||
elif isinstance(proto, (set, list, tuple)):
|
elif isinstance(proto, (set, list, tuple)):
|
||||||
# Support iterables list types
|
# Support iterables list types
|
||||||
for p in proto:
|
for p in proto:
|
||||||
if p not in SCHEMA_MAP:
|
if p not in ATTACHMENT_SCHEMA_MAP:
|
||||||
SCHEMA_MAP[p] = plugin
|
ATTACHMENT_SCHEMA_MAP[p] = plugin
|
||||||
|
|
||||||
# Load secure protocol(s) if defined
|
# Load secure protocol(s) if defined
|
||||||
protos = getattr(plugin, 'secure_protocol', None)
|
protos = getattr(plugin, 'secure_protocol', None)
|
||||||
if isinstance(protos, six.string_types):
|
if isinstance(protos, str):
|
||||||
if protos not in SCHEMA_MAP:
|
if protos not in ATTACHMENT_SCHEMA_MAP:
|
||||||
SCHEMA_MAP[protos] = plugin
|
ATTACHMENT_SCHEMA_MAP[protos] = plugin
|
||||||
|
|
||||||
if isinstance(protos, (set, list, tuple)):
|
if isinstance(protos, (set, list, tuple)):
|
||||||
# Support iterables list types
|
# Support iterables list types
|
||||||
for p in protos:
|
for p in protos:
|
||||||
if p not in SCHEMA_MAP:
|
if p not in ATTACHMENT_SCHEMA_MAP:
|
||||||
SCHEMA_MAP[p] = plugin
|
ATTACHMENT_SCHEMA_MAP[p] = plugin
|
||||||
|
|
||||||
return SCHEMA_MAP
|
return ATTACHMENT_SCHEMA_MAP
|
||||||
|
|
||||||
|
|
||||||
# Dynamically build our schema base
|
# Dynamically build our schema base
|
||||||
|
|
|
@ -26,12 +26,12 @@
|
||||||
import click
|
import click
|
||||||
import logging
|
import logging
|
||||||
import platform
|
import platform
|
||||||
import six
|
|
||||||
import sys
|
import sys
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
|
|
||||||
from os.path import isfile
|
from os.path import isfile
|
||||||
|
from os.path import exists
|
||||||
from os.path import expanduser
|
from os.path import expanduser
|
||||||
from os.path import expandvars
|
from os.path import expandvars
|
||||||
|
|
||||||
|
@ -40,6 +40,7 @@ from . import NotifyFormat
|
||||||
from . import Apprise
|
from . import Apprise
|
||||||
from . import AppriseAsset
|
from . import AppriseAsset
|
||||||
from . import AppriseConfig
|
from . import AppriseConfig
|
||||||
|
|
||||||
from .utils import parse_list
|
from .utils import parse_list
|
||||||
from .common import NOTIFY_TYPES
|
from .common import NOTIFY_TYPES
|
||||||
from .common import NOTIFY_FORMATS
|
from .common import NOTIFY_FORMATS
|
||||||
|
@ -60,23 +61,42 @@ DEFAULT_RECURSION_DEPTH = 1
|
||||||
CONTEXT_SETTINGS = dict(help_option_names=['-h', '--help'])
|
CONTEXT_SETTINGS = dict(help_option_names=['-h', '--help'])
|
||||||
|
|
||||||
# Define our default configuration we use if nothing is otherwise specified
|
# Define our default configuration we use if nothing is otherwise specified
|
||||||
DEFAULT_SEARCH_PATHS = (
|
DEFAULT_CONFIG_PATHS = (
|
||||||
|
# Legacy Path Support
|
||||||
'~/.apprise',
|
'~/.apprise',
|
||||||
'~/.apprise.yml',
|
'~/.apprise.yml',
|
||||||
'~/.config/apprise',
|
'~/.config/apprise',
|
||||||
'~/.config/apprise.yml',
|
'~/.config/apprise.yml',
|
||||||
|
|
||||||
|
# Plugin Support Extended Directory Search Paths
|
||||||
|
'~/.apprise/apprise',
|
||||||
|
'~/.apprise/apprise.yml',
|
||||||
|
'~/.config/apprise/apprise',
|
||||||
|
'~/.config/apprise/apprise.yml',
|
||||||
|
)
|
||||||
|
|
||||||
|
# Define our paths to search for plugins
|
||||||
|
DEFAULT_PLUGIN_PATHS = (
|
||||||
|
'~/.apprise/plugins',
|
||||||
|
'~/.config/apprise/plugins',
|
||||||
)
|
)
|
||||||
|
|
||||||
# Detect Windows
|
# Detect Windows
|
||||||
if platform.system() == 'Windows':
|
if platform.system() == 'Windows':
|
||||||
# Default Search Path for Windows Users
|
# Default Config Search Path for Windows Users
|
||||||
DEFAULT_SEARCH_PATHS = (
|
DEFAULT_CONFIG_PATHS = (
|
||||||
expandvars('%APPDATA%/Apprise/apprise'),
|
expandvars('%APPDATA%/Apprise/apprise'),
|
||||||
expandvars('%APPDATA%/Apprise/apprise.yml'),
|
expandvars('%APPDATA%/Apprise/apprise.yml'),
|
||||||
expandvars('%LOCALAPPDATA%/Apprise/apprise'),
|
expandvars('%LOCALAPPDATA%/Apprise/apprise'),
|
||||||
expandvars('%LOCALAPPDATA%/Apprise/apprise.yml'),
|
expandvars('%LOCALAPPDATA%/Apprise/apprise.yml'),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Default Plugin Search Path for Windows Users
|
||||||
|
DEFAULT_PLUGIN_PATHS = (
|
||||||
|
expandvars('%APPDATA%/Apprise/plugins'),
|
||||||
|
expandvars('%LOCALAPPDATA%/Apprise/plugins'),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def print_help_msg(command):
|
def print_help_msg(command):
|
||||||
"""
|
"""
|
||||||
|
@ -107,6 +127,9 @@ def print_version_msg():
|
||||||
@click.option('--title', '-t', default=None, type=str,
|
@click.option('--title', '-t', default=None, type=str,
|
||||||
help='Specify the message title. This field is complete '
|
help='Specify the message title. This field is complete '
|
||||||
'optional.')
|
'optional.')
|
||||||
|
@click.option('--plugin-path', '-P', default=None, type=str, multiple=True,
|
||||||
|
metavar='PLUGIN_PATH',
|
||||||
|
help='Specify one or more plugin paths to scan.')
|
||||||
@click.option('--config', '-c', default=None, type=str, multiple=True,
|
@click.option('--config', '-c', default=None, type=str, multiple=True,
|
||||||
metavar='CONFIG_URL',
|
metavar='CONFIG_URL',
|
||||||
help='Specify one or more configuration locations.')
|
help='Specify one or more configuration locations.')
|
||||||
|
@ -158,7 +181,7 @@ def print_version_msg():
|
||||||
metavar='SERVER_URL [SERVER_URL2 [SERVER_URL3]]',)
|
metavar='SERVER_URL [SERVER_URL2 [SERVER_URL3]]',)
|
||||||
def main(body, title, config, attach, urls, notification_type, theme, tag,
|
def main(body, title, config, attach, urls, notification_type, theme, tag,
|
||||||
input_format, dry_run, recursion_depth, verbose, disable_async,
|
input_format, dry_run, recursion_depth, verbose, disable_async,
|
||||||
details, interpret_escapes, debug, version):
|
details, interpret_escapes, plugin_path, debug, version):
|
||||||
"""
|
"""
|
||||||
Send a notification to all of the specified servers identified by their
|
Send a notification to all of the specified servers identified by their
|
||||||
URLs the content provided within the title, body and notification-type.
|
URLs the content provided within the title, body and notification-type.
|
||||||
|
@ -232,6 +255,12 @@ def main(body, title, config, attach, urls, notification_type, theme, tag,
|
||||||
# issue. For consistency, we also return a 2
|
# issue. For consistency, we also return a 2
|
||||||
sys.exit(2)
|
sys.exit(2)
|
||||||
|
|
||||||
|
if not plugin_path:
|
||||||
|
# Prepare a default set of plugin path
|
||||||
|
plugin_path = \
|
||||||
|
next((path for path in DEFAULT_PLUGIN_PATHS
|
||||||
|
if exists(expanduser(path))), None)
|
||||||
|
|
||||||
# Prepare our asset
|
# Prepare our asset
|
||||||
asset = AppriseAsset(
|
asset = AppriseAsset(
|
||||||
# Our body format
|
# Our body format
|
||||||
|
@ -243,11 +272,14 @@ def main(body, title, config, attach, urls, notification_type, theme, tag,
|
||||||
# Set the theme
|
# Set the theme
|
||||||
theme=theme,
|
theme=theme,
|
||||||
|
|
||||||
# Async mode is only used for Python v3+ and allows a user to send
|
# Async mode allows a user to send all of their notifications
|
||||||
# all of their notifications asyncronously. This was made an option
|
# asynchronously. This was made an option incase there are problems
|
||||||
# incase there are problems in the future where it's better that
|
# in the future where it is better that everything runs sequentially/
|
||||||
# everything run sequentially/syncronously instead.
|
# synchronously instead.
|
||||||
async_mode=disable_async is not True,
|
async_mode=disable_async is not True,
|
||||||
|
|
||||||
|
# Load our plugins
|
||||||
|
plugin_paths=plugin_path,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Create our Apprise object
|
# Create our Apprise object
|
||||||
|
@ -263,11 +295,11 @@ def main(body, title, config, attach, urls, notification_type, theme, tag,
|
||||||
for entry in plugins:
|
for entry in plugins:
|
||||||
protocols = [] if not entry['protocols'] else \
|
protocols = [] if not entry['protocols'] else \
|
||||||
[p for p in entry['protocols']
|
[p for p in entry['protocols']
|
||||||
if isinstance(p, six.string_types)]
|
if isinstance(p, str)]
|
||||||
protocols.extend(
|
protocols.extend(
|
||||||
[] if not entry['secure_protocols'] else
|
[] if not entry['secure_protocols'] else
|
||||||
[p for p in entry['secure_protocols']
|
[p for p in entry['secure_protocols']
|
||||||
if isinstance(p, six.string_types)])
|
if isinstance(p, str)])
|
||||||
|
|
||||||
if len(protocols) == 1:
|
if len(protocols) == 1:
|
||||||
# Simplify view by swapping {schema} with the single
|
# Simplify view by swapping {schema} with the single
|
||||||
|
@ -284,11 +316,18 @@ def main(body, title, config, attach, urls, notification_type, theme, tag,
|
||||||
'{}://'.format(protocols[0]),
|
'{}://'.format(protocols[0]),
|
||||||
entry['details']['templates'][x])
|
entry['details']['templates'][x])
|
||||||
|
|
||||||
|
fg = "green" if entry['enabled'] else "red"
|
||||||
|
if entry['category'] == 'custom':
|
||||||
|
# Identify these differently
|
||||||
|
fg = "cyan"
|
||||||
|
# Flip the enable switch so it forces the requirements
|
||||||
|
# to be displayed
|
||||||
|
entry['enabled'] = False
|
||||||
|
|
||||||
click.echo(click.style(
|
click.echo(click.style(
|
||||||
'{} {:<30} '.format(
|
'{} {:<30} '.format(
|
||||||
'+' if entry['enabled'] else '-',
|
'+' if entry['enabled'] else '-',
|
||||||
str(entry['service_name'])),
|
str(entry['service_name'])), fg=fg, bold=True),
|
||||||
fg="green" if entry['enabled'] else "red", bold=True),
|
|
||||||
nl=(not entry['enabled'] or len(protocols) == 1))
|
nl=(not entry['enabled'] or len(protocols) == 1))
|
||||||
|
|
||||||
if not entry['enabled']:
|
if not entry['enabled']:
|
||||||
|
@ -307,8 +346,9 @@ def main(body, title, config, attach, urls, notification_type, theme, tag,
|
||||||
click.echo(' - ' + req)
|
click.echo(' - ' + req)
|
||||||
|
|
||||||
# new line padding between entries
|
# new line padding between entries
|
||||||
click.echo()
|
if entry['category'] == 'native':
|
||||||
continue
|
click.echo()
|
||||||
|
continue
|
||||||
|
|
||||||
if len(protocols) > 1:
|
if len(protocols) > 1:
|
||||||
click.echo('| Schema(s): {}'.format(
|
click.echo('| Schema(s): {}'.format(
|
||||||
|
@ -324,6 +364,7 @@ def main(body, title, config, attach, urls, notification_type, theme, tag,
|
||||||
click.echo()
|
click.echo()
|
||||||
|
|
||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
|
# end if details()
|
||||||
|
|
||||||
# The priorities of what is accepted are parsed in order below:
|
# The priorities of what is accepted are parsed in order below:
|
||||||
# 1. URLs by command line
|
# 1. URLs by command line
|
||||||
|
@ -372,13 +413,14 @@ def main(body, title, config, attach, urls, notification_type, theme, tag,
|
||||||
a.add(AppriseConfig(
|
a.add(AppriseConfig(
|
||||||
paths=os.environ['APPRISE_CONFIG'].strip(),
|
paths=os.environ['APPRISE_CONFIG'].strip(),
|
||||||
asset=asset, recursion=recursion_depth))
|
asset=asset, recursion=recursion_depth))
|
||||||
|
|
||||||
else:
|
else:
|
||||||
# Load default configuration
|
# Load default configuration
|
||||||
a.add(AppriseConfig(
|
a.add(AppriseConfig(
|
||||||
paths=[f for f in DEFAULT_SEARCH_PATHS if isfile(expanduser(f))],
|
paths=[f for f in DEFAULT_CONFIG_PATHS if isfile(expanduser(f))],
|
||||||
asset=asset, recursion=recursion_depth))
|
asset=asset, recursion=recursion_depth))
|
||||||
|
|
||||||
if len(a) == 0:
|
if len(a) == 0 and not urls:
|
||||||
logger.error(
|
logger.error(
|
||||||
'You must specify at least one server URL or populated '
|
'You must specify at least one server URL or populated '
|
||||||
'configuration file.')
|
'configuration file.')
|
||||||
|
|
|
@ -24,7 +24,52 @@
|
||||||
# THE SOFTWARE.
|
# THE SOFTWARE.
|
||||||
|
|
||||||
|
|
||||||
class NotifyType(object):
|
# we mirror our base purely for the ability to reset everything; this
|
||||||
|
# is generally only used in testing and should not be used by developers
|
||||||
|
# It is also used as a means of preventing a module from being reloaded
|
||||||
|
# in the event it already exists
|
||||||
|
NOTIFY_MODULE_MAP = {}
|
||||||
|
|
||||||
|
# Maintains a mapping of all of the Notification services
|
||||||
|
NOTIFY_SCHEMA_MAP = {}
|
||||||
|
|
||||||
|
# This contains a mapping of all plugins dynamicaly loaded at runtime from
|
||||||
|
# external modules such as the @notify decorator
|
||||||
|
#
|
||||||
|
# The elements here will be additionally added to the NOTIFY_SCHEMA_MAP if
|
||||||
|
# there is no conflict otherwise.
|
||||||
|
# The structure looks like the following:
|
||||||
|
# Module path, e.g. /usr/share/apprise/plugins/my_notify_hook.py
|
||||||
|
# {
|
||||||
|
# 'path': path,
|
||||||
|
#
|
||||||
|
# 'notify': {
|
||||||
|
# 'schema': {
|
||||||
|
# 'name': 'Custom schema name',
|
||||||
|
# 'fn_name': 'name_of_function_decorator_was_found_on',
|
||||||
|
# 'url': 'schema://any/additional/info/found/on/url'
|
||||||
|
# 'plugin': <CustomNotifyWrapperPlugin>
|
||||||
|
# },
|
||||||
|
# 'schema2': {
|
||||||
|
# 'name': 'Custom schema name',
|
||||||
|
# 'fn_name': 'name_of_function_decorator_was_found_on',
|
||||||
|
# 'url': 'schema://any/additional/info/found/on/url'
|
||||||
|
# 'plugin': <CustomNotifyWrapperPlugin>
|
||||||
|
# }
|
||||||
|
# }
|
||||||
|
#
|
||||||
|
# Note: that the <CustomNotifyWrapperPlugin> inherits from
|
||||||
|
# NotifyBase
|
||||||
|
NOTIFY_CUSTOM_MODULE_MAP = {}
|
||||||
|
|
||||||
|
# Maintains a mapping of all configuration schema's supported
|
||||||
|
CONFIG_SCHEMA_MAP = {}
|
||||||
|
|
||||||
|
# Maintains a mapping of all attachment schema's supported
|
||||||
|
ATTACHMENT_SCHEMA_MAP = {}
|
||||||
|
|
||||||
|
|
||||||
|
class NotifyType:
|
||||||
"""
|
"""
|
||||||
A simple mapping of notification types most commonly used with
|
A simple mapping of notification types most commonly used with
|
||||||
all types of logging and notification services.
|
all types of logging and notification services.
|
||||||
|
@ -43,7 +88,7 @@ NOTIFY_TYPES = (
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class NotifyImageSize(object):
|
class NotifyImageSize:
|
||||||
"""
|
"""
|
||||||
A list of pre-defined image sizes to make it easier to work with defined
|
A list of pre-defined image sizes to make it easier to work with defined
|
||||||
plugins.
|
plugins.
|
||||||
|
@ -62,7 +107,7 @@ NOTIFY_IMAGE_SIZES = (
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class NotifyFormat(object):
|
class NotifyFormat:
|
||||||
"""
|
"""
|
||||||
A list of pre-defined text message formats that can be passed via the
|
A list of pre-defined text message formats that can be passed via the
|
||||||
apprise library.
|
apprise library.
|
||||||
|
@ -79,7 +124,7 @@ NOTIFY_FORMATS = (
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class OverflowMode(object):
|
class OverflowMode:
|
||||||
"""
|
"""
|
||||||
A list of pre-defined modes of how to handle the text when it exceeds the
|
A list of pre-defined modes of how to handle the text when it exceeds the
|
||||||
defined maximum message size.
|
defined maximum message size.
|
||||||
|
@ -107,7 +152,7 @@ OVERFLOW_MODES = (
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class ConfigFormat(object):
|
class ConfigFormat:
|
||||||
"""
|
"""
|
||||||
A list of pre-defined config formats that can be passed via the
|
A list of pre-defined config formats that can be passed via the
|
||||||
apprise library.
|
apprise library.
|
||||||
|
@ -130,7 +175,7 @@ CONFIG_FORMATS = (
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class ContentIncludeMode(object):
|
class ContentIncludeMode:
|
||||||
"""
|
"""
|
||||||
The different Content inclusion modes. All content based plugins will
|
The different Content inclusion modes. All content based plugins will
|
||||||
have one of these associated with it.
|
have one of these associated with it.
|
||||||
|
@ -155,7 +200,7 @@ CONTENT_INCLUDE_MODES = (
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class ContentLocation(object):
|
class ContentLocation:
|
||||||
"""
|
"""
|
||||||
This is primarily used for handling file attachments. The idea is
|
This is primarily used for handling file attachments. The idea is
|
||||||
to track the source of the attachment itself. We don't want
|
to track the source of the attachment itself. We don't want
|
||||||
|
|
|
@ -25,22 +25,18 @@
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
import six
|
|
||||||
import yaml
|
import yaml
|
||||||
import time
|
import time
|
||||||
|
|
||||||
from .. import plugins
|
from .. import plugins
|
||||||
|
from .. import common
|
||||||
from ..AppriseAsset import AppriseAsset
|
from ..AppriseAsset import AppriseAsset
|
||||||
from ..URLBase import URLBase
|
from ..URLBase import URLBase
|
||||||
from ..common import ConfigFormat
|
|
||||||
from ..common import CONFIG_FORMATS
|
|
||||||
from ..common import ContentIncludeMode
|
|
||||||
from ..utils import GET_SCHEMA_RE
|
from ..utils import GET_SCHEMA_RE
|
||||||
from ..utils import parse_list
|
from ..utils import parse_list
|
||||||
from ..utils import parse_bool
|
from ..utils import parse_bool
|
||||||
from ..utils import parse_urls
|
from ..utils import parse_urls
|
||||||
from ..utils import cwe312_url
|
from ..utils import cwe312_url
|
||||||
from . import SCHEMA_MAP
|
|
||||||
|
|
||||||
# Test whether token is valid or not
|
# Test whether token is valid or not
|
||||||
VALID_TOKEN = re.compile(
|
VALID_TOKEN = re.compile(
|
||||||
|
@ -57,7 +53,7 @@ class ConfigBase(URLBase):
|
||||||
|
|
||||||
# The default expected configuration format unless otherwise
|
# The default expected configuration format unless otherwise
|
||||||
# detected by the sub-modules
|
# detected by the sub-modules
|
||||||
default_config_format = ConfigFormat.TEXT
|
default_config_format = common.ConfigFormat.TEXT
|
||||||
|
|
||||||
# This is only set if the user overrides the config format on the URL
|
# This is only set if the user overrides the config format on the URL
|
||||||
# this should always initialize itself as None
|
# this should always initialize itself as None
|
||||||
|
@ -70,7 +66,7 @@ class ConfigBase(URLBase):
|
||||||
|
|
||||||
# By default all configuration is not includable using the 'include'
|
# By default all configuration is not includable using the 'include'
|
||||||
# line found in configuration files.
|
# line found in configuration files.
|
||||||
allow_cross_includes = ContentIncludeMode.NEVER
|
allow_cross_includes = common.ContentIncludeMode.NEVER
|
||||||
|
|
||||||
# the config path manages the handling of relative include
|
# the config path manages the handling of relative include
|
||||||
config_path = os.getcwd()
|
config_path = os.getcwd()
|
||||||
|
@ -138,11 +134,11 @@ class ConfigBase(URLBase):
|
||||||
self.encoding = kwargs.get('encoding')
|
self.encoding = kwargs.get('encoding')
|
||||||
|
|
||||||
if 'format' in kwargs \
|
if 'format' in kwargs \
|
||||||
and isinstance(kwargs['format'], six.string_types):
|
and isinstance(kwargs['format'], str):
|
||||||
# Store the enforced config format
|
# Store the enforced config format
|
||||||
self.config_format = kwargs.get('format').lower()
|
self.config_format = kwargs.get('format').lower()
|
||||||
|
|
||||||
if self.config_format not in CONFIG_FORMATS:
|
if self.config_format not in common.CONFIG_FORMATS:
|
||||||
# Simple error checking
|
# Simple error checking
|
||||||
err = 'An invalid config format ({}) was specified.'.format(
|
err = 'An invalid config format ({}) was specified.'.format(
|
||||||
self.config_format)
|
self.config_format)
|
||||||
|
@ -183,7 +179,7 @@ class ConfigBase(URLBase):
|
||||||
# config plugin to load the data source and return unparsed content
|
# config plugin to load the data source and return unparsed content
|
||||||
# None is returned if there was an error or simply no data
|
# None is returned if there was an error or simply no data
|
||||||
content = self.read(**kwargs)
|
content = self.read(**kwargs)
|
||||||
if not isinstance(content, six.string_types):
|
if not isinstance(content, str):
|
||||||
# Set the time our content was cached at
|
# Set the time our content was cached at
|
||||||
self._cached_time = time.time()
|
self._cached_time = time.time()
|
||||||
|
|
||||||
|
@ -230,7 +226,7 @@ class ConfigBase(URLBase):
|
||||||
schema = schema.group('schema').lower()
|
schema = schema.group('schema').lower()
|
||||||
|
|
||||||
# Some basic validation
|
# Some basic validation
|
||||||
if schema not in SCHEMA_MAP:
|
if schema not in common.CONFIG_SCHEMA_MAP:
|
||||||
ConfigBase.logger.warning(
|
ConfigBase.logger.warning(
|
||||||
'Unsupported include schema {}.'.format(schema))
|
'Unsupported include schema {}.'.format(schema))
|
||||||
continue
|
continue
|
||||||
|
@ -241,7 +237,7 @@ class ConfigBase(URLBase):
|
||||||
|
|
||||||
# Parse our url details of the server object as dictionary
|
# Parse our url details of the server object as dictionary
|
||||||
# containing all of the information parsed from our URL
|
# containing all of the information parsed from our URL
|
||||||
results = SCHEMA_MAP[schema].parse_url(url)
|
results = common.CONFIG_SCHEMA_MAP[schema].parse_url(url)
|
||||||
if not results:
|
if not results:
|
||||||
# Failed to parse the server URL
|
# Failed to parse the server URL
|
||||||
self.logger.warning(
|
self.logger.warning(
|
||||||
|
@ -249,12 +245,13 @@ class ConfigBase(URLBase):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Handle cross inclusion based on allow_cross_includes rules
|
# Handle cross inclusion based on allow_cross_includes rules
|
||||||
if (SCHEMA_MAP[schema].allow_cross_includes ==
|
if (common.CONFIG_SCHEMA_MAP[schema].allow_cross_includes ==
|
||||||
ContentIncludeMode.STRICT
|
common.ContentIncludeMode.STRICT
|
||||||
and schema not in self.schemas()
|
and schema not in self.schemas()
|
||||||
and not self.insecure_includes) or \
|
and not self.insecure_includes) or \
|
||||||
SCHEMA_MAP[schema].allow_cross_includes == \
|
common.CONFIG_SCHEMA_MAP[schema] \
|
||||||
ContentIncludeMode.NEVER:
|
.allow_cross_includes == \
|
||||||
|
common.ContentIncludeMode.NEVER:
|
||||||
|
|
||||||
# Prevent the loading if insecure base protocols
|
# Prevent the loading if insecure base protocols
|
||||||
ConfigBase.logger.warning(
|
ConfigBase.logger.warning(
|
||||||
|
@ -280,7 +277,8 @@ class ConfigBase(URLBase):
|
||||||
try:
|
try:
|
||||||
# Attempt to create an instance of our plugin using the
|
# Attempt to create an instance of our plugin using the
|
||||||
# parsed URL information
|
# parsed URL information
|
||||||
cfg_plugin = SCHEMA_MAP[results['schema']](**results)
|
cfg_plugin = \
|
||||||
|
common.CONFIG_SCHEMA_MAP[results['schema']](**results)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
# the arguments are invalid or can not be used.
|
# the arguments are invalid or can not be used.
|
||||||
|
@ -379,7 +377,7 @@ class ConfigBase(URLBase):
|
||||||
# Allow overriding the default config format
|
# Allow overriding the default config format
|
||||||
if 'format' in results['qsd']:
|
if 'format' in results['qsd']:
|
||||||
results['format'] = results['qsd'].get('format')
|
results['format'] = results['qsd'].get('format')
|
||||||
if results['format'] not in CONFIG_FORMATS:
|
if results['format'] not in common.CONFIG_FORMATS:
|
||||||
URLBase.logger.warning(
|
URLBase.logger.warning(
|
||||||
'Unsupported format specified {}'.format(
|
'Unsupported format specified {}'.format(
|
||||||
results['format']))
|
results['format']))
|
||||||
|
@ -457,14 +455,14 @@ class ConfigBase(URLBase):
|
||||||
|
|
||||||
# Attempt to detect configuration
|
# Attempt to detect configuration
|
||||||
if result.group('yaml'):
|
if result.group('yaml'):
|
||||||
config_format = ConfigFormat.YAML
|
config_format = common.ConfigFormat.YAML
|
||||||
ConfigBase.logger.debug(
|
ConfigBase.logger.debug(
|
||||||
'Detected YAML configuration '
|
'Detected YAML configuration '
|
||||||
'based on line {}.'.format(line))
|
'based on line {}.'.format(line))
|
||||||
break
|
break
|
||||||
|
|
||||||
elif result.group('text'):
|
elif result.group('text'):
|
||||||
config_format = ConfigFormat.TEXT
|
config_format = common.ConfigFormat.TEXT
|
||||||
ConfigBase.logger.debug(
|
ConfigBase.logger.debug(
|
||||||
'Detected TEXT configuration '
|
'Detected TEXT configuration '
|
||||||
'based on line {}.'.format(line))
|
'based on line {}.'.format(line))
|
||||||
|
@ -472,7 +470,7 @@ class ConfigBase(URLBase):
|
||||||
|
|
||||||
# If we reach here, we have a comment entry
|
# If we reach here, we have a comment entry
|
||||||
# Adjust default format to TEXT
|
# Adjust default format to TEXT
|
||||||
config_format = ConfigFormat.TEXT
|
config_format = common.ConfigFormat.TEXT
|
||||||
|
|
||||||
return config_format
|
return config_format
|
||||||
|
|
||||||
|
@ -493,7 +491,7 @@ class ConfigBase(URLBase):
|
||||||
ConfigBase.logger.error('Could not detect configuration')
|
ConfigBase.logger.error('Could not detect configuration')
|
||||||
return (list(), list())
|
return (list(), list())
|
||||||
|
|
||||||
if config_format not in CONFIG_FORMATS:
|
if config_format not in common.CONFIG_FORMATS:
|
||||||
# Invalid configuration type specified
|
# Invalid configuration type specified
|
||||||
ConfigBase.logger.error(
|
ConfigBase.logger.error(
|
||||||
'An invalid configuration format ({}) was specified'.format(
|
'An invalid configuration format ({}) was specified'.format(
|
||||||
|
@ -618,7 +616,7 @@ class ConfigBase(URLBase):
|
||||||
try:
|
try:
|
||||||
# Attempt to create an instance of our plugin using the
|
# Attempt to create an instance of our plugin using the
|
||||||
# parsed URL information
|
# parsed URL information
|
||||||
plugin = plugins.SCHEMA_MAP[results['schema']](**results)
|
plugin = common.NOTIFY_SCHEMA_MAP[results['schema']](**results)
|
||||||
|
|
||||||
# Create log entry of loaded URL
|
# Create log entry of loaded URL
|
||||||
ConfigBase.logger.debug(
|
ConfigBase.logger.debug(
|
||||||
|
@ -705,7 +703,7 @@ class ConfigBase(URLBase):
|
||||||
|
|
||||||
if not (hasattr(asset, k) and
|
if not (hasattr(asset, k) and
|
||||||
isinstance(getattr(asset, k),
|
isinstance(getattr(asset, k),
|
||||||
(bool, six.string_types))):
|
(bool, str))):
|
||||||
|
|
||||||
# We can't set a function or non-string set value
|
# We can't set a function or non-string set value
|
||||||
ConfigBase.logger.warning(
|
ConfigBase.logger.warning(
|
||||||
|
@ -716,7 +714,7 @@ class ConfigBase(URLBase):
|
||||||
# Convert to an empty string
|
# Convert to an empty string
|
||||||
v = ''
|
v = ''
|
||||||
|
|
||||||
if (isinstance(v, (bool, six.string_types))
|
if (isinstance(v, (bool, str))
|
||||||
and isinstance(getattr(asset, k), bool)):
|
and isinstance(getattr(asset, k), bool)):
|
||||||
|
|
||||||
# If the object in the Asset is a boolean, then
|
# If the object in the Asset is a boolean, then
|
||||||
|
@ -724,7 +722,7 @@ class ConfigBase(URLBase):
|
||||||
# match that.
|
# match that.
|
||||||
setattr(asset, k, parse_bool(v))
|
setattr(asset, k, parse_bool(v))
|
||||||
|
|
||||||
elif isinstance(v, six.string_types):
|
elif isinstance(v, str):
|
||||||
# Set our asset object with the new value
|
# Set our asset object with the new value
|
||||||
setattr(asset, k, v.strip())
|
setattr(asset, k, v.strip())
|
||||||
|
|
||||||
|
@ -739,7 +737,7 @@ class ConfigBase(URLBase):
|
||||||
global_tags = set()
|
global_tags = set()
|
||||||
|
|
||||||
tags = result.get('tag', None)
|
tags = result.get('tag', None)
|
||||||
if tags and isinstance(tags, (list, tuple, six.string_types)):
|
if tags and isinstance(tags, (list, tuple, str)):
|
||||||
# Store any preset tags
|
# Store any preset tags
|
||||||
global_tags = set(parse_list(tags))
|
global_tags = set(parse_list(tags))
|
||||||
|
|
||||||
|
@ -747,7 +745,7 @@ class ConfigBase(URLBase):
|
||||||
# include root directive
|
# include root directive
|
||||||
#
|
#
|
||||||
includes = result.get('include', None)
|
includes = result.get('include', None)
|
||||||
if isinstance(includes, six.string_types):
|
if isinstance(includes, str):
|
||||||
# Support a single inline string or multiple ones separated by a
|
# Support a single inline string or multiple ones separated by a
|
||||||
# comma and/or space
|
# comma and/or space
|
||||||
includes = parse_urls(includes)
|
includes = parse_urls(includes)
|
||||||
|
@ -759,7 +757,7 @@ class ConfigBase(URLBase):
|
||||||
# Iterate over each config URL
|
# Iterate over each config URL
|
||||||
for no, url in enumerate(includes):
|
for no, url in enumerate(includes):
|
||||||
|
|
||||||
if isinstance(url, six.string_types):
|
if isinstance(url, str):
|
||||||
# Support a single inline string or multiple ones separated by
|
# Support a single inline string or multiple ones separated by
|
||||||
# a comma and/or space
|
# a comma and/or space
|
||||||
configs.extend(parse_urls(url))
|
configs.extend(parse_urls(url))
|
||||||
|
@ -787,7 +785,7 @@ class ConfigBase(URLBase):
|
||||||
loggable_url = url if not asset.secure_logging \
|
loggable_url = url if not asset.secure_logging \
|
||||||
else cwe312_url(url)
|
else cwe312_url(url)
|
||||||
|
|
||||||
if isinstance(url, six.string_types):
|
if isinstance(url, str):
|
||||||
# We're just a simple URL string...
|
# We're just a simple URL string...
|
||||||
schema = GET_SCHEMA_RE.match(url)
|
schema = GET_SCHEMA_RE.match(url)
|
||||||
if schema is None:
|
if schema is None:
|
||||||
|
@ -818,10 +816,7 @@ class ConfigBase(URLBase):
|
||||||
# can at least tell the end user what entries were ignored
|
# can at least tell the end user what entries were ignored
|
||||||
# due to errors
|
# due to errors
|
||||||
|
|
||||||
if six.PY2:
|
it = iter(url.items())
|
||||||
it = url.iteritems()
|
|
||||||
else: # six.PY3
|
|
||||||
it = iter(url.items())
|
|
||||||
|
|
||||||
# Track the URL to-load
|
# Track the URL to-load
|
||||||
_url = None
|
_url = None
|
||||||
|
@ -871,17 +866,14 @@ class ConfigBase(URLBase):
|
||||||
|
|
||||||
# We are a url string with additional unescaped options
|
# We are a url string with additional unescaped options
|
||||||
if isinstance(entries, dict):
|
if isinstance(entries, dict):
|
||||||
if six.PY2:
|
_url, tokens = next(iter(url.items()))
|
||||||
_url, tokens = next(url.iteritems())
|
|
||||||
else: # six.PY3
|
|
||||||
_url, tokens = next(iter(url.items()))
|
|
||||||
|
|
||||||
# Tags you just can't over-ride
|
# Tags you just can't over-ride
|
||||||
if 'schema' in entries:
|
if 'schema' in entries:
|
||||||
del entries['schema']
|
del entries['schema']
|
||||||
|
|
||||||
# support our special tokens (if they're present)
|
# support our special tokens (if they're present)
|
||||||
if schema in plugins.SCHEMA_MAP:
|
if schema in common.NOTIFY_SCHEMA_MAP:
|
||||||
entries = ConfigBase._special_token_handler(
|
entries = ConfigBase._special_token_handler(
|
||||||
schema, entries)
|
schema, entries)
|
||||||
|
|
||||||
|
@ -893,7 +885,7 @@ class ConfigBase(URLBase):
|
||||||
|
|
||||||
elif isinstance(tokens, dict):
|
elif isinstance(tokens, dict):
|
||||||
# support our special tokens (if they're present)
|
# support our special tokens (if they're present)
|
||||||
if schema in plugins.SCHEMA_MAP:
|
if schema in common.NOTIFY_SCHEMA_MAP:
|
||||||
tokens = ConfigBase._special_token_handler(
|
tokens = ConfigBase._special_token_handler(
|
||||||
schema, tokens)
|
schema, tokens)
|
||||||
|
|
||||||
|
@ -927,6 +919,14 @@ class ConfigBase(URLBase):
|
||||||
# Grab our first item
|
# Grab our first item
|
||||||
_results = results.pop(0)
|
_results = results.pop(0)
|
||||||
|
|
||||||
|
if _results['schema'] not in common.NOTIFY_SCHEMA_MAP:
|
||||||
|
# the arguments are invalid or can not be used.
|
||||||
|
ConfigBase.logger.warning(
|
||||||
|
'An invalid Apprise schema ({}) in YAML configuration '
|
||||||
|
'entry #{}, item #{}'
|
||||||
|
.format(_results['schema'], no + 1, entry))
|
||||||
|
continue
|
||||||
|
|
||||||
# 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
|
# The below ensures our tags are set correctly
|
||||||
if 'tag' in _results:
|
if 'tag' in _results:
|
||||||
|
@ -958,10 +958,12 @@ class ConfigBase(URLBase):
|
||||||
# Prepare our Asset Object
|
# Prepare our Asset Object
|
||||||
_results['asset'] = asset
|
_results['asset'] = asset
|
||||||
|
|
||||||
|
# Now we generate our plugin
|
||||||
try:
|
try:
|
||||||
# Attempt to create an instance of our plugin using the
|
# Attempt to create an instance of our plugin using the
|
||||||
# parsed URL information
|
# parsed URL information
|
||||||
plugin = plugins.SCHEMA_MAP[_results['schema']](**_results)
|
plugin = common.\
|
||||||
|
NOTIFY_SCHEMA_MAP[_results['schema']](**_results)
|
||||||
|
|
||||||
# Create log entry of loaded URL
|
# Create log entry of loaded URL
|
||||||
ConfigBase.logger.debug(
|
ConfigBase.logger.debug(
|
||||||
|
@ -1014,7 +1016,7 @@ class ConfigBase(URLBase):
|
||||||
# Create a copy of our dictionary
|
# Create a copy of our dictionary
|
||||||
tokens = tokens.copy()
|
tokens = tokens.copy()
|
||||||
|
|
||||||
for kw, meta in plugins.SCHEMA_MAP[schema]\
|
for kw, meta in common.NOTIFY_SCHEMA_MAP[schema]\
|
||||||
.template_kwargs.items():
|
.template_kwargs.items():
|
||||||
|
|
||||||
# Determine our prefix:
|
# Determine our prefix:
|
||||||
|
@ -1059,7 +1061,7 @@ class ConfigBase(URLBase):
|
||||||
# This function here allows these mappings to take place within the
|
# This function here allows these mappings to take place within the
|
||||||
# YAML file as independant arguments.
|
# YAML file as independant arguments.
|
||||||
class_templates = \
|
class_templates = \
|
||||||
plugins.details(plugins.SCHEMA_MAP[schema])
|
plugins.details(common.NOTIFY_SCHEMA_MAP[schema])
|
||||||
|
|
||||||
for key in list(tokens.keys()):
|
for key in list(tokens.keys()):
|
||||||
|
|
||||||
|
@ -1088,7 +1090,7 @@ class ConfigBase(URLBase):
|
||||||
|
|
||||||
# Detect if we're dealign with a list or not
|
# Detect if we're dealign with a list or not
|
||||||
is_list = re.search(
|
is_list = re.search(
|
||||||
r'^(list|choice):.*',
|
r'^list:.*',
|
||||||
meta.get('type'),
|
meta.get('type'),
|
||||||
re.IGNORECASE)
|
re.IGNORECASE)
|
||||||
|
|
||||||
|
@ -1105,7 +1107,7 @@ class ConfigBase(URLBase):
|
||||||
r'^(choice:)?string',
|
r'^(choice:)?string',
|
||||||
meta.get('type'),
|
meta.get('type'),
|
||||||
re.IGNORECASE) \
|
re.IGNORECASE) \
|
||||||
and not isinstance(value, six.string_types):
|
and not isinstance(value, str):
|
||||||
|
|
||||||
# Ensure our format is as expected
|
# Ensure our format is as expected
|
||||||
value = str(value)
|
value = str(value)
|
||||||
|
@ -1158,19 +1160,8 @@ class ConfigBase(URLBase):
|
||||||
|
|
||||||
def __bool__(self):
|
def __bool__(self):
|
||||||
"""
|
"""
|
||||||
Allows the Apprise object to be wrapped in an Python 3.x based 'if
|
Allows the Apprise object to be wrapped in an 'if statement'.
|
||||||
statement'. True is returned if our content was downloaded correctly.
|
True is returned if our content was downloaded correctly.
|
||||||
"""
|
|
||||||
if not isinstance(self._cached_servers, list):
|
|
||||||
# Generate ourselves a list of content we can pull from
|
|
||||||
self.servers()
|
|
||||||
|
|
||||||
return True if self._cached_servers else False
|
|
||||||
|
|
||||||
def __nonzero__(self):
|
|
||||||
"""
|
|
||||||
Allows the Apprise object to be wrapped in an Python 2.x based 'if
|
|
||||||
statement'. True is returned if our content was downloaded correctly.
|
|
||||||
"""
|
"""
|
||||||
if not isinstance(self._cached_servers, list):
|
if not isinstance(self._cached_servers, list):
|
||||||
# Generate ourselves a list of content we can pull from
|
# Generate ourselves a list of content we can pull from
|
||||||
|
|
|
@ -24,7 +24,6 @@
|
||||||
# THE SOFTWARE.
|
# THE SOFTWARE.
|
||||||
|
|
||||||
import re
|
import re
|
||||||
import io
|
|
||||||
import os
|
import os
|
||||||
from .ConfigBase import ConfigBase
|
from .ConfigBase import ConfigBase
|
||||||
from ..common import ConfigFormat
|
from ..common import ConfigFormat
|
||||||
|
@ -119,9 +118,7 @@ class ConfigFile(ConfigBase):
|
||||||
self.throttle()
|
self.throttle()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Python 3 just supports open(), however to remain compatible with
|
with open(self.path, "rt", encoding=self.encoding) as f:
|
||||||
# Python 2, we use the io module
|
|
||||||
with io.open(self.path, "rt", encoding=self.encoding) as f:
|
|
||||||
# Store our content for parsing
|
# Store our content for parsing
|
||||||
response = f.read()
|
response = f.read()
|
||||||
|
|
||||||
|
|
|
@ -24,7 +24,6 @@
|
||||||
# THE SOFTWARE.
|
# THE SOFTWARE.
|
||||||
|
|
||||||
import re
|
import re
|
||||||
import six
|
|
||||||
import requests
|
import requests
|
||||||
from .ConfigBase import ConfigBase
|
from .ConfigBase import ConfigBase
|
||||||
from ..common import ConfigFormat
|
from ..common import ConfigFormat
|
||||||
|
@ -81,7 +80,7 @@ class ConfigHTTP(ConfigBase):
|
||||||
self.schema = 'https' if self.secure else 'http'
|
self.schema = 'https' if self.secure else 'http'
|
||||||
|
|
||||||
self.fullpath = kwargs.get('fullpath')
|
self.fullpath = kwargs.get('fullpath')
|
||||||
if not isinstance(self.fullpath, six.string_types):
|
if not isinstance(self.fullpath, str):
|
||||||
self.fullpath = '/'
|
self.fullpath = '/'
|
||||||
|
|
||||||
self.headers = {}
|
self.headers = {}
|
||||||
|
|
|
@ -24,14 +24,11 @@
|
||||||
# THE SOFTWARE.
|
# THE SOFTWARE.
|
||||||
|
|
||||||
import re
|
import re
|
||||||
import six
|
|
||||||
from os import listdir
|
from os import listdir
|
||||||
from os.path import dirname
|
from os.path import dirname
|
||||||
from os.path import abspath
|
from os.path import abspath
|
||||||
from ..logger import logger
|
from ..logger import logger
|
||||||
|
from ..common import CONFIG_SCHEMA_MAP
|
||||||
# Maintains a mapping of all of the configuration services
|
|
||||||
SCHEMA_MAP = {}
|
|
||||||
|
|
||||||
__all__ = []
|
__all__ = []
|
||||||
|
|
||||||
|
@ -89,40 +86,20 @@ def __load_matrix(path=abspath(dirname(__file__)), name='apprise.config'):
|
||||||
globals()[plugin_name] = plugin
|
globals()[plugin_name] = plugin
|
||||||
|
|
||||||
fn = getattr(plugin, 'schemas', None)
|
fn = getattr(plugin, 'schemas', None)
|
||||||
try:
|
schemas = set([]) if not callable(fn) else fn(plugin)
|
||||||
schemas = set([]) if not callable(fn) else fn(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)
|
|
||||||
|
|
||||||
elif isinstance(schema, (set, list, tuple)):
|
|
||||||
# Support iterables list types
|
|
||||||
for s in schema:
|
|
||||||
if isinstance(s, six.string_types):
|
|
||||||
schemas.add(s)
|
|
||||||
|
|
||||||
# map our schema to our plugin
|
# map our schema to our plugin
|
||||||
for schema in schemas:
|
for schema in schemas:
|
||||||
if schema in SCHEMA_MAP:
|
if schema in CONFIG_SCHEMA_MAP:
|
||||||
logger.error(
|
logger.error(
|
||||||
"Config schema ({}) mismatch detected - {} to {}"
|
"Config schema ({}) mismatch detected - {} to {}"
|
||||||
.format(schema, SCHEMA_MAP[schema], plugin))
|
.format(schema, CONFIG_SCHEMA_MAP[schema], plugin))
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Assign plugin
|
# Assign plugin
|
||||||
SCHEMA_MAP[schema] = plugin
|
CONFIG_SCHEMA_MAP[schema] = plugin
|
||||||
|
|
||||||
return SCHEMA_MAP
|
return CONFIG_SCHEMA_MAP
|
||||||
|
|
||||||
|
|
||||||
# Dynamically build our schema base
|
# Dynamically build our schema base
|
||||||
|
|
|
@ -23,18 +23,12 @@
|
||||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
# THE SOFTWARE.
|
# THE SOFTWARE.
|
||||||
|
|
||||||
|
|
||||||
import re
|
import re
|
||||||
import six
|
|
||||||
from markdown import markdown
|
from markdown import markdown
|
||||||
from .common import NotifyFormat
|
from .common import NotifyFormat
|
||||||
from .URLBase import URLBase
|
from .URLBase import URLBase
|
||||||
|
|
||||||
if six.PY2:
|
from html.parser import HTMLParser
|
||||||
from HTMLParser import HTMLParser
|
|
||||||
|
|
||||||
else:
|
|
||||||
from html.parser import HTMLParser
|
|
||||||
|
|
||||||
|
|
||||||
def convert_between(from_format, to_format, content):
|
def convert_between(from_format, to_format, content):
|
||||||
|
@ -70,7 +64,8 @@ def text_to_html(content):
|
||||||
Converts specified content from plain text to HTML.
|
Converts specified content from plain text to HTML.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
return URLBase.escape_html(content)
|
# First eliminate any carriage returns
|
||||||
|
return URLBase.escape_html(content, convert_new_lines=True)
|
||||||
|
|
||||||
|
|
||||||
def html_to_text(content):
|
def html_to_text(content):
|
||||||
|
@ -79,10 +74,6 @@ def html_to_text(content):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
parser = HTMLConverter()
|
parser = HTMLConverter()
|
||||||
if six.PY2:
|
|
||||||
# Python 2.7 requires an additional parsing to un-escape characters
|
|
||||||
content = parser.unescape(content)
|
|
||||||
|
|
||||||
parser.feed(content)
|
parser.feed(content)
|
||||||
parser.close()
|
parser.close()
|
||||||
return parser.converted
|
return parser.converted
|
||||||
|
@ -96,7 +87,9 @@ class HTMLConverter(HTMLParser, object):
|
||||||
'div', 'td', 'th', 'code', 'pre', 'label', 'li',)
|
'div', 'td', 'th', 'code', 'pre', 'label', 'li',)
|
||||||
|
|
||||||
# the folowing tags ignore any internal text
|
# the folowing tags ignore any internal text
|
||||||
IGNORE_TAGS = ('style', 'link', 'meta', 'title', 'html', 'head', 'script')
|
IGNORE_TAGS = (
|
||||||
|
'form', 'input', 'textarea', 'select', 'ul', 'ol', 'style', 'link',
|
||||||
|
'meta', 'title', 'html', 'head', 'script')
|
||||||
|
|
||||||
# Condense Whitespace
|
# Condense Whitespace
|
||||||
WS_TRIM = re.compile(r'[\s]+', re.DOTALL | re.MULTILINE)
|
WS_TRIM = re.compile(r'[\s]+', re.DOTALL | re.MULTILINE)
|
||||||
|
@ -122,14 +115,6 @@ class HTMLConverter(HTMLParser, object):
|
||||||
string = ''.join(self._finalize(self._result))
|
string = ''.join(self._finalize(self._result))
|
||||||
self.converted = string.strip()
|
self.converted = string.strip()
|
||||||
|
|
||||||
if six.PY2:
|
|
||||||
# See https://stackoverflow.com/questions/10993612/\
|
|
||||||
# how-to-remove-xa0-from-string-in-python
|
|
||||||
#
|
|
||||||
# This is required since the unescape() nbsp; with \xa0 when
|
|
||||||
# using Python 2.7
|
|
||||||
self.converted = self.converted.replace(u'\xa0', u' ')
|
|
||||||
|
|
||||||
def _finalize(self, result):
|
def _finalize(self, result):
|
||||||
"""
|
"""
|
||||||
Combines and strips consecutive strings, then converts consecutive
|
Combines and strips consecutive strings, then converts consecutive
|
||||||
|
|
226
libs/apprise/decorators/CustomNotifyPlugin.py
Normal file
226
libs/apprise/decorators/CustomNotifyPlugin.py
Normal file
|
@ -0,0 +1,226 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
#
|
||||||
|
# Copyright (C) 2022 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 ..plugins.NotifyBase import NotifyBase
|
||||||
|
from ..utils import URL_DETAILS_RE
|
||||||
|
from ..utils import parse_url
|
||||||
|
from ..utils import url_assembly
|
||||||
|
from ..utils import dict_full_update
|
||||||
|
from .. import common
|
||||||
|
from ..logger import logger
|
||||||
|
import inspect
|
||||||
|
|
||||||
|
|
||||||
|
class CustomNotifyPlugin(NotifyBase):
|
||||||
|
"""
|
||||||
|
Apprise Custom Plugin Hook
|
||||||
|
|
||||||
|
This gets initialized based on @notify decorator definitions
|
||||||
|
|
||||||
|
"""
|
||||||
|
# Our Custom notification
|
||||||
|
service_url = 'https://github.com/caronc/apprise/wiki/Custom_Notification'
|
||||||
|
|
||||||
|
# Over-ride our category since this inheritance of the NotifyBase class
|
||||||
|
# should be treated differently.
|
||||||
|
category = 'custom'
|
||||||
|
|
||||||
|
# Define object templates
|
||||||
|
templates = (
|
||||||
|
'{schema}://',
|
||||||
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def parse_url(url):
|
||||||
|
"""
|
||||||
|
Parses the URL and returns arguments retrieved
|
||||||
|
|
||||||
|
"""
|
||||||
|
return parse_url(url, verify_host=False, simple=True)
|
||||||
|
|
||||||
|
def url(self, privacy=False, *args, **kwargs):
|
||||||
|
"""
|
||||||
|
General URL assembly
|
||||||
|
"""
|
||||||
|
return '{schema}://'.format(schema=self.secure_protocol)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def instantiate_plugin(url, send_func, name=None):
|
||||||
|
"""
|
||||||
|
The function used to add a new notification plugin based on the schema
|
||||||
|
parsed from the provided URL into our supported matrix structure.
|
||||||
|
"""
|
||||||
|
|
||||||
|
if not isinstance(url, str):
|
||||||
|
msg = 'An invalid custom notify url/schema ({}) provided in ' \
|
||||||
|
'function {}.'.format(url, send_func.__name__)
|
||||||
|
logger.warning(msg)
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Validate that our schema is okay
|
||||||
|
re_match = URL_DETAILS_RE.match(url)
|
||||||
|
if not re_match:
|
||||||
|
msg = 'An invalid custom notify url/schema ({}) provided in ' \
|
||||||
|
'function {}.'.format(url, send_func.__name__)
|
||||||
|
logger.warning(msg)
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Acquire our plugin name
|
||||||
|
plugin_name = re_match.group('schema').lower()
|
||||||
|
|
||||||
|
if not re_match.group('base'):
|
||||||
|
url = '{}://'.format(plugin_name)
|
||||||
|
|
||||||
|
# Keep a default set of arguments to apply to all called references
|
||||||
|
base_args = parse_url(
|
||||||
|
url, default_schema=plugin_name, verify_host=False, simple=True)
|
||||||
|
|
||||||
|
if plugin_name in common.NOTIFY_SCHEMA_MAP:
|
||||||
|
# we're already handling this object
|
||||||
|
msg = 'The schema ({}) is already defined and could not be ' \
|
||||||
|
'loaded from custom notify function {}.' \
|
||||||
|
.format(url, send_func.__name__)
|
||||||
|
logger.warning(msg)
|
||||||
|
return None
|
||||||
|
|
||||||
|
# We define our own custom wrapper class so that we can initialize
|
||||||
|
# some key default configuration values allowing calls to our
|
||||||
|
# `Apprise.details()` to correctly differentiate one custom plugin
|
||||||
|
# that was loaded from another
|
||||||
|
class CustomNotifyPluginWrapper(CustomNotifyPlugin):
|
||||||
|
|
||||||
|
# Our Service Name
|
||||||
|
service_name = name if isinstance(name, str) \
|
||||||
|
and name else 'Custom - {}'.format(plugin_name)
|
||||||
|
|
||||||
|
# Store our matched schema
|
||||||
|
secure_protocol = plugin_name
|
||||||
|
|
||||||
|
requirements = {
|
||||||
|
# Define our required packaging in order to work
|
||||||
|
'details': "Source: {}".format(inspect.getfile(send_func))
|
||||||
|
}
|
||||||
|
|
||||||
|
# Assign our send() function
|
||||||
|
__send = staticmethod(send_func)
|
||||||
|
|
||||||
|
# Update our default arguments
|
||||||
|
_base_args = base_args
|
||||||
|
|
||||||
|
def __init__(self, **kwargs):
|
||||||
|
"""
|
||||||
|
Our initialization
|
||||||
|
|
||||||
|
"""
|
||||||
|
# init parent
|
||||||
|
super(CustomNotifyPluginWrapper, self).__init__(**kwargs)
|
||||||
|
|
||||||
|
self._default_args = {}
|
||||||
|
|
||||||
|
# Apply our updates based on what was parsed
|
||||||
|
dict_full_update(self._default_args, self._base_args)
|
||||||
|
dict_full_update(self._default_args, kwargs)
|
||||||
|
|
||||||
|
# Update our arguments (applying them to what we originally)
|
||||||
|
# initialized as
|
||||||
|
self._default_args['url'] = url_assembly(**self._default_args)
|
||||||
|
|
||||||
|
def send(self, body, title='', notify_type=common.NotifyType.INFO,
|
||||||
|
*args, **kwargs):
|
||||||
|
"""
|
||||||
|
Our send() call which triggers our hook
|
||||||
|
"""
|
||||||
|
|
||||||
|
response = False
|
||||||
|
try:
|
||||||
|
# Enforce a boolean response
|
||||||
|
result = self.__send(
|
||||||
|
body, title, notify_type, *args,
|
||||||
|
meta=self._default_args, **kwargs)
|
||||||
|
|
||||||
|
if result is None:
|
||||||
|
# The wrapper did not define a return (or returned
|
||||||
|
# None)
|
||||||
|
# this is treated as a successful return as it is
|
||||||
|
# assumed the developer did not care about the result
|
||||||
|
# of the call.
|
||||||
|
response = True
|
||||||
|
|
||||||
|
else:
|
||||||
|
# Perform boolean check (allowing obects to also be
|
||||||
|
# returned and check against the __bool__ call
|
||||||
|
response = True if result else False
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
# Unhandled Exception
|
||||||
|
self.logger.warning(
|
||||||
|
'An exception occured sending a %s notification.',
|
||||||
|
common.
|
||||||
|
NOTIFY_SCHEMA_MAP[self.secure_protocol].service_name)
|
||||||
|
self.logger.debug(
|
||||||
|
'%s Exception: %s',
|
||||||
|
common.NOTIFY_SCHEMA_MAP[self.secure_protocol], str(e))
|
||||||
|
return False
|
||||||
|
|
||||||
|
if response:
|
||||||
|
self.logger.info(
|
||||||
|
'Sent %s notification.',
|
||||||
|
common.
|
||||||
|
NOTIFY_SCHEMA_MAP[self.secure_protocol].service_name)
|
||||||
|
else:
|
||||||
|
self.logger.warning(
|
||||||
|
'Failed to send %s notification.',
|
||||||
|
common.
|
||||||
|
NOTIFY_SCHEMA_MAP[self.secure_protocol].service_name)
|
||||||
|
return response
|
||||||
|
|
||||||
|
# Store our plugin into our core map file
|
||||||
|
common.NOTIFY_SCHEMA_MAP[plugin_name] = CustomNotifyPluginWrapper
|
||||||
|
|
||||||
|
# Update our custom plugin map
|
||||||
|
module_pyname = str(send_func.__module__)
|
||||||
|
if module_pyname not in common.NOTIFY_CUSTOM_MODULE_MAP:
|
||||||
|
# Support non-dynamic includes as well...
|
||||||
|
common.NOTIFY_CUSTOM_MODULE_MAP[module_pyname] = {
|
||||||
|
'path': inspect.getfile(send_func),
|
||||||
|
|
||||||
|
# Initialize our template
|
||||||
|
'notify': {},
|
||||||
|
}
|
||||||
|
|
||||||
|
common.\
|
||||||
|
NOTIFY_CUSTOM_MODULE_MAP[module_pyname]['notify'][plugin_name] = {
|
||||||
|
# Our Serivice Description (for API and CLI --details view)
|
||||||
|
'name': CustomNotifyPluginWrapper.service_name,
|
||||||
|
# The name of the send function the @notify decorator wrapped
|
||||||
|
'fn_name': send_func.__name__,
|
||||||
|
# The URL that was provided in the @notify decorator call
|
||||||
|
# associated with the 'on='
|
||||||
|
'url': url,
|
||||||
|
# The Initialized Plugin that was generated based on the above
|
||||||
|
# parameters
|
||||||
|
'plugin': CustomNotifyPluginWrapper}
|
||||||
|
|
||||||
|
# return our plugin
|
||||||
|
return common.NOTIFY_SCHEMA_MAP[plugin_name]
|
30
libs/apprise/decorators/__init__.py
Normal file
30
libs/apprise/decorators/__init__.py
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
#
|
||||||
|
# Copyright (C) 2022 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 .notify import notify
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
'notify'
|
||||||
|
]
|
123
libs/apprise/decorators/notify.py
Normal file
123
libs/apprise/decorators/notify.py
Normal file
|
@ -0,0 +1,123 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
#
|
||||||
|
# Copyright (C) 2022 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 .CustomNotifyPlugin import CustomNotifyPlugin
|
||||||
|
|
||||||
|
|
||||||
|
def notify(on, name=None):
|
||||||
|
"""
|
||||||
|
@notify decorator allows you to map functions you've defined to be loaded
|
||||||
|
as a regular notify by Apprise. You must identify a protocol that
|
||||||
|
users will trigger your call by.
|
||||||
|
|
||||||
|
@notify(on="foobar")
|
||||||
|
def your_declaration(body, title, notify_type, meta, *args, **kwargs):
|
||||||
|
...
|
||||||
|
|
||||||
|
You can optionally provide the name to associate with the plugin which
|
||||||
|
is what calling functions via the API will receive.
|
||||||
|
|
||||||
|
@notify(on="foobar", name="My Foobar Process")
|
||||||
|
def your_action(body, title, notify_type, meta, *args, **kwargs):
|
||||||
|
...
|
||||||
|
|
||||||
|
The meta variable is actually the processed URL contents found in
|
||||||
|
configuration files that landed you in this function you wrote in
|
||||||
|
the first place. It's very easily tokenized already for you so
|
||||||
|
that you can bend the notification logic to your hearts content.
|
||||||
|
|
||||||
|
@notify(on="foobar", name="My Foobar Process")
|
||||||
|
def your_action(body, title, notify_type, body_format, meta, attach,
|
||||||
|
*args, **kwargs):
|
||||||
|
...
|
||||||
|
|
||||||
|
Arguments break down as follows:
|
||||||
|
body: The message body associated with the notification
|
||||||
|
title: The message title associated with the notification
|
||||||
|
notify_type: The message type (info, success, warning, and failure)
|
||||||
|
body_format: The format of the incoming notification body. This is
|
||||||
|
either text, html, or markdown.
|
||||||
|
meta: Combines the URL arguments specified on the `on` call
|
||||||
|
with the ones loaded from a users configuration. This
|
||||||
|
is a dictionary that presents itself like this:
|
||||||
|
{
|
||||||
|
'schema': 'http',
|
||||||
|
'url': 'http://hostname',
|
||||||
|
'host': 'hostname',
|
||||||
|
|
||||||
|
'user': 'john',
|
||||||
|
'password': 'doe',
|
||||||
|
'port': 80,
|
||||||
|
'path': '/',
|
||||||
|
'fullpath': '/test.php',
|
||||||
|
'query': 'test.php',
|
||||||
|
|
||||||
|
'qsd': {'key': 'value', 'key2': 'value2'},
|
||||||
|
|
||||||
|
'asset': <AppriseAsset>,
|
||||||
|
'tag': set(),
|
||||||
|
}
|
||||||
|
|
||||||
|
Meta entries are ONLY present if found. A simple URL
|
||||||
|
such as foobar:// would only produce the following:
|
||||||
|
{
|
||||||
|
'schema': 'foobar',
|
||||||
|
'url': 'foobar://',
|
||||||
|
|
||||||
|
'asset': <AppriseAsset>,
|
||||||
|
'tag': set(),
|
||||||
|
}
|
||||||
|
|
||||||
|
attach: An array AppriseAttachment objects (if any were provided)
|
||||||
|
|
||||||
|
body_format: Defaults to the expected format output; By default this
|
||||||
|
will be TEXT unless over-ridden in the Apprise URL
|
||||||
|
|
||||||
|
|
||||||
|
If you don't intend on using all of the parameters, your @notify() call
|
||||||
|
# can be greatly simplified to just:
|
||||||
|
|
||||||
|
@notify(on="foobar", name="My Foobar Process")
|
||||||
|
def your_action(body, title, *args, **kwargs)
|
||||||
|
|
||||||
|
Always end your wrappers declaration with *args and **kwargs to be future
|
||||||
|
proof with newer versions of Apprise.
|
||||||
|
|
||||||
|
Your wrapper should return True if processed the send() function as you
|
||||||
|
expected and return False if not. If nothing is returned, then this is
|
||||||
|
treated as as success (True).
|
||||||
|
|
||||||
|
"""
|
||||||
|
def wrapper(func):
|
||||||
|
"""
|
||||||
|
Instantiate our custom (notification) plugin
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Generate
|
||||||
|
CustomNotifyPlugin.instantiate_plugin(
|
||||||
|
url=on, send_func=func, name=name)
|
||||||
|
|
||||||
|
return func
|
||||||
|
|
||||||
|
return wrapper
|
|
@ -66,7 +66,7 @@ logging.Logger.deprecate = deprecate
|
||||||
logger = logging.getLogger(LOGGER_NAME)
|
logger = logging.getLogger(LOGGER_NAME)
|
||||||
|
|
||||||
|
|
||||||
class LogCapture(object):
|
class LogCapture:
|
||||||
"""
|
"""
|
||||||
A class used to allow one to instantiate loggers that write to
|
A class used to allow one to instantiate loggers that write to
|
||||||
memory for temporary purposes. e.g.:
|
memory for temporary purposes. e.g.:
|
||||||
|
|
|
@ -24,7 +24,6 @@
|
||||||
# THE SOFTWARE.
|
# THE SOFTWARE.
|
||||||
|
|
||||||
import re
|
import re
|
||||||
import six
|
|
||||||
import requests
|
import requests
|
||||||
from json import dumps
|
from json import dumps
|
||||||
|
|
||||||
|
@ -137,7 +136,7 @@ class NotifyAppriseAPI(NotifyBase):
|
||||||
super(NotifyAppriseAPI, self).__init__(**kwargs)
|
super(NotifyAppriseAPI, self).__init__(**kwargs)
|
||||||
|
|
||||||
self.fullpath = kwargs.get('fullpath')
|
self.fullpath = kwargs.get('fullpath')
|
||||||
if not isinstance(self.fullpath, six.string_types):
|
if not isinstance(self.fullpath, str):
|
||||||
self.fullpath = '/'
|
self.fullpath = '/'
|
||||||
|
|
||||||
self.token = validate_regex(
|
self.token = validate_regex(
|
||||||
|
@ -339,18 +338,10 @@ class NotifyAppriseAPI(NotifyBase):
|
||||||
return results
|
return results
|
||||||
|
|
||||||
# Add our headers that the user can potentially over-ride if they wish
|
# Add our headers that the user can potentially over-ride if they wish
|
||||||
# to to our returned result set
|
# to to our returned result set and tidy entries by unquoting them
|
||||||
results['headers'] = results['qsd+']
|
|
||||||
if results['qsd-']:
|
|
||||||
results['headers'].update(results['qsd-'])
|
|
||||||
NotifyBase.logger.deprecate(
|
|
||||||
"minus (-) based Apprise API header tokens are being "
|
|
||||||
" removed; use the plus (+) symbol instead.")
|
|
||||||
|
|
||||||
# Tidy our header entries by unquoting them
|
|
||||||
results['headers'] = \
|
results['headers'] = \
|
||||||
{NotifyAppriseAPI.unquote(x): NotifyAppriseAPI.unquote(y)
|
{NotifyAppriseAPI.unquote(x): NotifyAppriseAPI.unquote(y)
|
||||||
for x, y in results['headers'].items()}
|
for x, y in results['qsd+'].items()}
|
||||||
|
|
||||||
# Support the passing of tags in the URL
|
# Support the passing of tags in the URL
|
||||||
if 'tags' in results['qsd'] and len(results['qsd']['tags']):
|
if 'tags' in results['qsd'] and len(results['qsd']['tags']):
|
||||||
|
|
506
libs/apprise/plugins/NotifyBark.py
Normal file
506
libs/apprise/plugins/NotifyBark.py
Normal file
|
@ -0,0 +1,506 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
#
|
||||||
|
# Copyright (C) 2022 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.
|
||||||
|
#
|
||||||
|
# API: https://github.com/Finb/bark-server/blob/master/docs/API_V2.md#python
|
||||||
|
#
|
||||||
|
import requests
|
||||||
|
import json
|
||||||
|
|
||||||
|
from .NotifyBase import NotifyBase
|
||||||
|
from ..URLBase import PrivacyMode
|
||||||
|
from ..common import NotifyImageSize
|
||||||
|
from ..common import NotifyType
|
||||||
|
from ..utils import parse_list
|
||||||
|
from ..utils import parse_bool
|
||||||
|
from ..AppriseLocale import gettext_lazy as _
|
||||||
|
|
||||||
|
|
||||||
|
# Sounds generated off of: https://github.com/Finb/Bark/tree/master/Sounds
|
||||||
|
BARK_SOUNDS = (
|
||||||
|
"alarm.caf",
|
||||||
|
"anticipate.caf",
|
||||||
|
"bell.caf",
|
||||||
|
"birdsong.caf",
|
||||||
|
"bloom.caf",
|
||||||
|
"calypso.caf",
|
||||||
|
"chime.caf",
|
||||||
|
"choo.caf",
|
||||||
|
"descent.caf",
|
||||||
|
"electronic.caf",
|
||||||
|
"fanfare.caf",
|
||||||
|
"glass.caf",
|
||||||
|
"gotosleep.caf",
|
||||||
|
"healthnotification.caf",
|
||||||
|
"horn.caf",
|
||||||
|
"ladder.caf",
|
||||||
|
"mailsent.caf",
|
||||||
|
"minuet.caf",
|
||||||
|
"multiwayinvitation.caf",
|
||||||
|
"newmail.caf",
|
||||||
|
"newsflash.caf",
|
||||||
|
"noir.caf",
|
||||||
|
"paymentsuccess.caf",
|
||||||
|
"shake.caf",
|
||||||
|
"sherwoodforest.caf",
|
||||||
|
"silence.caf",
|
||||||
|
"spell.caf",
|
||||||
|
"suspense.caf",
|
||||||
|
"telegraph.caf",
|
||||||
|
"tiptoes.caf",
|
||||||
|
"typewriters.caf",
|
||||||
|
"update.caf",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# Supported Level Entries
|
||||||
|
class NotifyBarkLevel:
|
||||||
|
"""
|
||||||
|
Defines the Bark Level options
|
||||||
|
"""
|
||||||
|
ACTIVE = 'active'
|
||||||
|
|
||||||
|
TIME_SENSITIVE = 'timeSensitive'
|
||||||
|
|
||||||
|
PASSIVE = 'passive'
|
||||||
|
|
||||||
|
|
||||||
|
BARK_LEVELS = (
|
||||||
|
NotifyBarkLevel.ACTIVE,
|
||||||
|
NotifyBarkLevel.TIME_SENSITIVE,
|
||||||
|
NotifyBarkLevel.PASSIVE,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class NotifyBark(NotifyBase):
|
||||||
|
"""
|
||||||
|
A wrapper for Notify Bark Notifications
|
||||||
|
"""
|
||||||
|
|
||||||
|
# The default descriptive name associated with the Notification
|
||||||
|
service_name = 'Bark'
|
||||||
|
|
||||||
|
# The services URL
|
||||||
|
service_url = 'https://github.com/Finb/Bark'
|
||||||
|
|
||||||
|
# The default protocol
|
||||||
|
protocol = 'bark'
|
||||||
|
|
||||||
|
# The default secure protocol
|
||||||
|
secure_protocol = 'barks'
|
||||||
|
|
||||||
|
# A URL that takes you to the setup/help of the specific protocol
|
||||||
|
setup_url = 'https://github.com/caronc/apprise/wiki/Notify_bark'
|
||||||
|
|
||||||
|
# Allows the user to specify the NotifyImageSize object; this is supported
|
||||||
|
# through the webhook
|
||||||
|
image_size = NotifyImageSize.XY_128
|
||||||
|
|
||||||
|
# Define object templates
|
||||||
|
templates = (
|
||||||
|
'{schema}://{host}:{port}/{targets}',
|
||||||
|
'{schema}://{user}:{password}@{host}/{targets}',
|
||||||
|
'{schema}://{user}:{password}@{host}:{port}/{targets}',
|
||||||
|
'{schema}://{user}:{password}@{host}/{targets}',
|
||||||
|
)
|
||||||
|
|
||||||
|
# Define our template arguments
|
||||||
|
template_tokens = dict(NotifyBase.template_tokens, **{
|
||||||
|
'host': {
|
||||||
|
'name': _('Hostname'),
|
||||||
|
'type': 'string',
|
||||||
|
'required': True,
|
||||||
|
},
|
||||||
|
'port': {
|
||||||
|
'name': _('Port'),
|
||||||
|
'type': 'int',
|
||||||
|
'min': 1,
|
||||||
|
'max': 65535,
|
||||||
|
},
|
||||||
|
'user': {
|
||||||
|
'name': _('Username'),
|
||||||
|
'type': 'string',
|
||||||
|
},
|
||||||
|
'password': {
|
||||||
|
'name': _('Password'),
|
||||||
|
'type': 'string',
|
||||||
|
'private': True,
|
||||||
|
},
|
||||||
|
'target_device': {
|
||||||
|
'name': _('Target Device'),
|
||||||
|
'type': 'string',
|
||||||
|
'map_to': 'targets',
|
||||||
|
},
|
||||||
|
'targets': {
|
||||||
|
'name': _('Targets'),
|
||||||
|
'type': 'list:string',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
# Define our template arguments
|
||||||
|
template_args = dict(NotifyBase.template_args, **{
|
||||||
|
'to': {
|
||||||
|
'alias_of': 'targets',
|
||||||
|
},
|
||||||
|
'sound': {
|
||||||
|
'name': _('Sound'),
|
||||||
|
'type': 'choice:string',
|
||||||
|
'values': BARK_SOUNDS,
|
||||||
|
},
|
||||||
|
'level': {
|
||||||
|
'name': _('Level'),
|
||||||
|
'type': 'choice:string',
|
||||||
|
'values': BARK_LEVELS,
|
||||||
|
},
|
||||||
|
'click': {
|
||||||
|
'name': _('Click'),
|
||||||
|
'type': 'string',
|
||||||
|
},
|
||||||
|
'badge': {
|
||||||
|
'name': _('Badge'),
|
||||||
|
'type': 'int',
|
||||||
|
'min': 0,
|
||||||
|
},
|
||||||
|
'category': {
|
||||||
|
'name': _('Category'),
|
||||||
|
'type': 'string',
|
||||||
|
},
|
||||||
|
'group': {
|
||||||
|
'name': _('Group'),
|
||||||
|
'type': 'string',
|
||||||
|
},
|
||||||
|
'image': {
|
||||||
|
'name': _('Include Image'),
|
||||||
|
'type': 'bool',
|
||||||
|
'default': True,
|
||||||
|
'map_to': 'include_image',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
def __init__(self, targets=None, include_image=True, sound=None,
|
||||||
|
category=None, group=None, level=None, click=None,
|
||||||
|
badge=None, **kwargs):
|
||||||
|
"""
|
||||||
|
Initialize Notify Bark Object
|
||||||
|
"""
|
||||||
|
super(NotifyBark, self).__init__(**kwargs)
|
||||||
|
|
||||||
|
# Prepare our URL
|
||||||
|
self.notify_url = '%s://%s%s/push' % (
|
||||||
|
'https' if self.secure else 'http',
|
||||||
|
self.host,
|
||||||
|
':{}'.format(self.port)
|
||||||
|
if (self.port and isinstance(self.port, int)) else '',
|
||||||
|
)
|
||||||
|
|
||||||
|
# Assign our category
|
||||||
|
self.category = \
|
||||||
|
category if isinstance(category, str) else None
|
||||||
|
|
||||||
|
# Assign our group
|
||||||
|
self.group = group if isinstance(group, str) else None
|
||||||
|
|
||||||
|
# Initialize device list
|
||||||
|
self.targets = parse_list(targets)
|
||||||
|
|
||||||
|
# Place an image inline with the message body
|
||||||
|
self.include_image = include_image
|
||||||
|
|
||||||
|
# A clickthrough option for notifications
|
||||||
|
self.click = click
|
||||||
|
|
||||||
|
# Badge
|
||||||
|
try:
|
||||||
|
# Acquire our badge count if we can:
|
||||||
|
# - We accept both the integer form as well as a string
|
||||||
|
# representation
|
||||||
|
self.badge = int(badge)
|
||||||
|
if self.badge < 0:
|
||||||
|
raise ValueError()
|
||||||
|
|
||||||
|
except TypeError:
|
||||||
|
# NoneType means use Default; this is an okay exception
|
||||||
|
self.badge = None
|
||||||
|
|
||||||
|
except ValueError:
|
||||||
|
self.badge = None
|
||||||
|
self.logger.warning(
|
||||||
|
'The specified Bark badge ({}) is not valid ', badge)
|
||||||
|
|
||||||
|
# Sound (easy-lookup)
|
||||||
|
self.sound = None if not sound else next(
|
||||||
|
(f for f in BARK_SOUNDS if f.startswith(sound.lower())), None)
|
||||||
|
if sound and not self.sound:
|
||||||
|
self.logger.warning(
|
||||||
|
'The specified Bark sound ({}) was not found ', sound)
|
||||||
|
|
||||||
|
# Level
|
||||||
|
self.level = None if not level else next(
|
||||||
|
(f for f in BARK_LEVELS if f[0] == level[0]), None)
|
||||||
|
if level and not self.level:
|
||||||
|
self.logger.warning(
|
||||||
|
'The specified Bark level ({}) is not valid ', level)
|
||||||
|
|
||||||
|
return
|
||||||
|
|
||||||
|
def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs):
|
||||||
|
"""
|
||||||
|
Perform Bark Notification
|
||||||
|
"""
|
||||||
|
|
||||||
|
# error tracking (used for function return)
|
||||||
|
has_error = False
|
||||||
|
|
||||||
|
if not len(self.targets):
|
||||||
|
# We have nothing to notify; we're done
|
||||||
|
self.logger.warning('There are no Bark devices to notify')
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Prepare our headers
|
||||||
|
headers = {
|
||||||
|
'User-Agent': self.app_id,
|
||||||
|
'Content-Type': 'application/json; charset=utf-8',
|
||||||
|
}
|
||||||
|
|
||||||
|
# Prepare our payload (sample below)
|
||||||
|
# {
|
||||||
|
# "body": "Test Bark Server",
|
||||||
|
# "device_key": "nysrshcqielvoxsa",
|
||||||
|
# "title": "bleem",
|
||||||
|
# "category": "category",
|
||||||
|
# "sound": "minuet.caf",
|
||||||
|
# "badge": 1,
|
||||||
|
# "icon": "https://day.app/assets/images/avatar.jpg",
|
||||||
|
# "group": "test",
|
||||||
|
# "url": "https://mritd.com"
|
||||||
|
# }
|
||||||
|
payload = {
|
||||||
|
'title': title if title else self.app_desc,
|
||||||
|
'body': body,
|
||||||
|
}
|
||||||
|
|
||||||
|
# Acquire our image url if configured to do so
|
||||||
|
image_url = None if not self.include_image else \
|
||||||
|
self.image_url(notify_type)
|
||||||
|
|
||||||
|
if image_url:
|
||||||
|
payload['icon'] = image_url
|
||||||
|
|
||||||
|
if self.sound:
|
||||||
|
payload['sound'] = self.sound
|
||||||
|
|
||||||
|
if self.click:
|
||||||
|
payload['url'] = self.click
|
||||||
|
|
||||||
|
if self.badge:
|
||||||
|
payload['badge'] = self.badge
|
||||||
|
|
||||||
|
if self.level:
|
||||||
|
payload['level'] = self.level
|
||||||
|
|
||||||
|
if self.category:
|
||||||
|
payload['category'] = self.category
|
||||||
|
|
||||||
|
if self.group:
|
||||||
|
payload['group'] = self.group
|
||||||
|
|
||||||
|
auth = None
|
||||||
|
if self.user:
|
||||||
|
auth = (self.user, self.password)
|
||||||
|
|
||||||
|
# Create a copy of the targets
|
||||||
|
targets = list(self.targets)
|
||||||
|
|
||||||
|
while len(targets) > 0:
|
||||||
|
# Retrieve our device key
|
||||||
|
target = targets.pop()
|
||||||
|
|
||||||
|
payload['device_key'] = target
|
||||||
|
self.logger.debug('Bark POST URL: %s (cert_verify=%r)' % (
|
||||||
|
self.notify_url, self.verify_certificate,
|
||||||
|
))
|
||||||
|
self.logger.debug('Bark Payload: %s' % str(payload))
|
||||||
|
|
||||||
|
# Always call throttle before any remote server i/o is made
|
||||||
|
self.throttle()
|
||||||
|
try:
|
||||||
|
r = requests.post(
|
||||||
|
self.notify_url,
|
||||||
|
data=json.dumps(payload),
|
||||||
|
headers=headers,
|
||||||
|
auth=auth,
|
||||||
|
verify=self.verify_certificate,
|
||||||
|
timeout=self.request_timeout,
|
||||||
|
)
|
||||||
|
if r.status_code != requests.codes.ok:
|
||||||
|
# We had a problem
|
||||||
|
status_str = \
|
||||||
|
NotifyBark.http_response_code_lookup(
|
||||||
|
r.status_code)
|
||||||
|
|
||||||
|
self.logger.warning(
|
||||||
|
'Failed to send Bark notification to {}: '
|
||||||
|
'{}{}error={}.'.format(
|
||||||
|
target,
|
||||||
|
status_str,
|
||||||
|
', ' if status_str else '',
|
||||||
|
r.status_code))
|
||||||
|
|
||||||
|
self.logger.debug(
|
||||||
|
'Response Details:\r\n{}'.format(r.content))
|
||||||
|
|
||||||
|
# Mark our failure
|
||||||
|
has_error = True
|
||||||
|
continue
|
||||||
|
|
||||||
|
else:
|
||||||
|
self.logger.info(
|
||||||
|
'Sent Bark notification to {}.'.format(target))
|
||||||
|
|
||||||
|
except requests.RequestException as e:
|
||||||
|
self.logger.warning(
|
||||||
|
'A Connection error occurred sending Bark '
|
||||||
|
'notification to {}.'.format(target))
|
||||||
|
self.logger.debug('Socket Exception: %s' % str(e))
|
||||||
|
|
||||||
|
# Mark our failure
|
||||||
|
has_error = True
|
||||||
|
continue
|
||||||
|
|
||||||
|
return not has_error
|
||||||
|
|
||||||
|
def url(self, privacy=False, *args, **kwargs):
|
||||||
|
"""
|
||||||
|
Returns the URL built dynamically based on specified arguments.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Define any URL parameters
|
||||||
|
params = {
|
||||||
|
'image': 'yes' if self.include_image else 'no',
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.sound:
|
||||||
|
params['sound'] = self.sound
|
||||||
|
|
||||||
|
if self.click:
|
||||||
|
params['click'] = self.click
|
||||||
|
|
||||||
|
if self.badge:
|
||||||
|
params['badge'] = str(self.badge)
|
||||||
|
|
||||||
|
if self.level:
|
||||||
|
params['level'] = self.level
|
||||||
|
|
||||||
|
if self.category:
|
||||||
|
params['category'] = self.category
|
||||||
|
|
||||||
|
if self.group:
|
||||||
|
params['group'] = self.group
|
||||||
|
|
||||||
|
# Extend our parameters
|
||||||
|
params.update(self.url_parameters(privacy=privacy, *args, **kwargs))
|
||||||
|
|
||||||
|
# Determine Authentication
|
||||||
|
auth = ''
|
||||||
|
if self.user and self.password:
|
||||||
|
auth = '{user}:{password}@'.format(
|
||||||
|
user=NotifyBark.quote(self.user, safe=''),
|
||||||
|
password=self.pprint(
|
||||||
|
self.password, privacy, mode=PrivacyMode.Secret, safe=''),
|
||||||
|
)
|
||||||
|
elif self.user:
|
||||||
|
auth = '{user}@'.format(
|
||||||
|
user=NotifyBark.quote(self.user, safe=''),
|
||||||
|
)
|
||||||
|
|
||||||
|
default_port = 443 if self.secure else 80
|
||||||
|
|
||||||
|
return '{schema}://{auth}{hostname}{port}/{targets}?{params}'.format(
|
||||||
|
schema=self.secure_protocol if self.secure else self.protocol,
|
||||||
|
auth=auth,
|
||||||
|
# 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(
|
||||||
|
[NotifyBark.quote('{}'.format(x)) for x in self.targets]),
|
||||||
|
params=NotifyBark.urlencode(params),
|
||||||
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def parse_url(url):
|
||||||
|
"""
|
||||||
|
Parses the URL and returns enough arguments that can allow
|
||||||
|
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
|
||||||
|
|
||||||
|
# Apply our targets
|
||||||
|
results['targets'] = NotifyBark.split_path(results['fullpath'])
|
||||||
|
|
||||||
|
# Category
|
||||||
|
if 'category' in results['qsd'] and results['qsd']['category']:
|
||||||
|
results['category'] = NotifyBark.unquote(
|
||||||
|
results['qsd']['category'].strip())
|
||||||
|
|
||||||
|
# Group
|
||||||
|
if 'group' in results['qsd'] and results['qsd']['group']:
|
||||||
|
results['group'] = NotifyBark.unquote(
|
||||||
|
results['qsd']['group'].strip())
|
||||||
|
|
||||||
|
# Badge
|
||||||
|
if 'badge' in results['qsd'] and results['qsd']['badge']:
|
||||||
|
results['badge'] = NotifyBark.unquote(
|
||||||
|
results['qsd']['badge'].strip())
|
||||||
|
|
||||||
|
# Level
|
||||||
|
if 'level' in results['qsd'] and results['qsd']['level']:
|
||||||
|
results['level'] = NotifyBark.unquote(
|
||||||
|
results['qsd']['level'].strip())
|
||||||
|
|
||||||
|
# Click (URL)
|
||||||
|
if 'click' in results['qsd'] and results['qsd']['click']:
|
||||||
|
results['click'] = NotifyBark.unquote(
|
||||||
|
results['qsd']['click'].strip())
|
||||||
|
|
||||||
|
# Sound
|
||||||
|
if 'sound' in results['qsd'] and results['qsd']['sound']:
|
||||||
|
results['sound'] = NotifyBark.unquote(
|
||||||
|
results['qsd']['sound'].strip())
|
||||||
|
|
||||||
|
# The 'to' makes it easier to use yaml configuration
|
||||||
|
if 'to' in results['qsd'] and len(results['qsd']['to']):
|
||||||
|
results['targets'] += \
|
||||||
|
NotifyBark.parse_list(results['qsd']['to'])
|
||||||
|
|
||||||
|
# use image= for consistency with the other plugins
|
||||||
|
results['include_image'] = \
|
||||||
|
parse_bool(results['qsd'].get('image', True))
|
||||||
|
|
||||||
|
return results
|
|
@ -24,7 +24,6 @@
|
||||||
# THE SOFTWARE.
|
# THE SOFTWARE.
|
||||||
|
|
||||||
import re
|
import re
|
||||||
import six
|
|
||||||
|
|
||||||
from ..URLBase import URLBase
|
from ..URLBase import URLBase
|
||||||
from ..common import NotifyType
|
from ..common import NotifyType
|
||||||
|
@ -37,14 +36,9 @@ from ..AppriseLocale import gettext_lazy as _
|
||||||
from ..AppriseAttachment import AppriseAttachment
|
from ..AppriseAttachment import AppriseAttachment
|
||||||
|
|
||||||
|
|
||||||
if six.PY3:
|
# Wrap our base with the asyncio wrapper
|
||||||
# Wrap our base with the asyncio wrapper
|
from ..py3compat.asyncio import AsyncNotifyBase
|
||||||
from ..py3compat.asyncio import AsyncNotifyBase
|
BASE_OBJECT = AsyncNotifyBase
|
||||||
BASE_OBJECT = AsyncNotifyBase
|
|
||||||
|
|
||||||
else:
|
|
||||||
# Python v2.7 (backwards compatibility)
|
|
||||||
BASE_OBJECT = URLBase
|
|
||||||
|
|
||||||
|
|
||||||
class NotifyBase(BASE_OBJECT):
|
class NotifyBase(BASE_OBJECT):
|
||||||
|
@ -59,6 +53,15 @@ class NotifyBase(BASE_OBJECT):
|
||||||
# enabled.
|
# enabled.
|
||||||
enabled = True
|
enabled = True
|
||||||
|
|
||||||
|
# The category allows for parent inheritance of this object to alter
|
||||||
|
# this when it's function/use is intended to behave differently. The
|
||||||
|
# following category types exist:
|
||||||
|
#
|
||||||
|
# native: Is a native plugin written/stored in `apprise/plugins/Notify*`
|
||||||
|
# custom: Is a custom plugin written/stored in a users plugin directory
|
||||||
|
# that they loaded at execution time.
|
||||||
|
category = 'native'
|
||||||
|
|
||||||
# Some plugins may require additional packages above what is provided
|
# Some plugins may require additional packages above what is provided
|
||||||
# already by Apprise.
|
# already by Apprise.
|
||||||
#
|
#
|
||||||
|
|
|
@ -24,7 +24,6 @@
|
||||||
# THE SOFTWARE.
|
# THE SOFTWARE.
|
||||||
|
|
||||||
import re
|
import re
|
||||||
import six
|
|
||||||
import requests
|
import requests
|
||||||
import hmac
|
import hmac
|
||||||
from json import dumps
|
from json import dumps
|
||||||
|
@ -181,7 +180,7 @@ class NotifyBoxcar(NotifyBase):
|
||||||
self.tags.append(DEFAULT_TAG)
|
self.tags.append(DEFAULT_TAG)
|
||||||
targets = []
|
targets = []
|
||||||
|
|
||||||
elif isinstance(targets, six.string_types):
|
elif isinstance(targets, str):
|
||||||
targets = [x for x in filter(bool, TAGS_LIST_DELIM.split(
|
targets = [x for x in filter(bool, TAGS_LIST_DELIM.split(
|
||||||
targets,
|
targets,
|
||||||
))]
|
))]
|
||||||
|
@ -357,13 +356,8 @@ class NotifyBoxcar(NotifyBase):
|
||||||
# by default
|
# by default
|
||||||
entries = NotifyBoxcar.split_path(results['fullpath'])
|
entries = NotifyBoxcar.split_path(results['fullpath'])
|
||||||
|
|
||||||
try:
|
# Now fetch the remaining tokens
|
||||||
# Now fetch the remaining tokens
|
results['secret'] = entries.pop(0) if entries else None
|
||||||
results['secret'] = entries.pop(0)
|
|
||||||
|
|
||||||
except IndexError:
|
|
||||||
# secret wasn't specified
|
|
||||||
results['secret'] = None
|
|
||||||
|
|
||||||
# Our recipients make up the remaining entries of our array
|
# Our recipients make up the remaining entries of our array
|
||||||
results['targets'] = entries
|
results['targets'] = entries
|
||||||
|
|
457
libs/apprise/plugins/NotifyBulkSMS.py
Normal file
457
libs/apprise/plugins/NotifyBulkSMS.py
Normal file
|
@ -0,0 +1,457 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
#
|
||||||
|
# Copyright (C) 2022 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.
|
||||||
|
|
||||||
|
# To use this service you will need a BulkSMS account
|
||||||
|
# You will need credits (new accounts start with a few)
|
||||||
|
# https://www.bulksms.com/account/
|
||||||
|
#
|
||||||
|
# API is documented here:
|
||||||
|
# - https://www.bulksms.com/developer/json/v1/#tag/Message
|
||||||
|
import re
|
||||||
|
import six
|
||||||
|
import requests
|
||||||
|
import json
|
||||||
|
from itertools import chain
|
||||||
|
from .NotifyBase import NotifyBase
|
||||||
|
from ..URLBase import PrivacyMode
|
||||||
|
from ..common import NotifyType
|
||||||
|
from ..utils import is_phone_no
|
||||||
|
from ..utils import parse_phone_no
|
||||||
|
from ..utils import parse_bool
|
||||||
|
from ..AppriseLocale import gettext_lazy as _
|
||||||
|
|
||||||
|
|
||||||
|
IS_GROUP_RE = re.compile(
|
||||||
|
r'^(@?(?P<group>[A-Z0-9_-]+))$',
|
||||||
|
re.IGNORECASE,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class BulkSMSRoutingGroup(object):
|
||||||
|
"""
|
||||||
|
The different categories of routing
|
||||||
|
"""
|
||||||
|
ECONOMY = "ECONOMY"
|
||||||
|
STANDARD = "STANDARD"
|
||||||
|
PREMIUM = "PREMIUM"
|
||||||
|
|
||||||
|
|
||||||
|
# Used for verification purposes
|
||||||
|
BULKSMS_ROUTING_GROUPS = (
|
||||||
|
BulkSMSRoutingGroup.ECONOMY,
|
||||||
|
BulkSMSRoutingGroup.STANDARD,
|
||||||
|
BulkSMSRoutingGroup.PREMIUM,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class BulkSMSEncoding(object):
|
||||||
|
"""
|
||||||
|
The different categories of routing
|
||||||
|
"""
|
||||||
|
TEXT = "TEXT"
|
||||||
|
UNICODE = "UNICODE"
|
||||||
|
BINARY = "BINARY"
|
||||||
|
|
||||||
|
|
||||||
|
class NotifyBulkSMS(NotifyBase):
|
||||||
|
"""
|
||||||
|
A wrapper for BulkSMS Notifications
|
||||||
|
"""
|
||||||
|
|
||||||
|
# The default descriptive name associated with the Notification
|
||||||
|
service_name = 'BulkSMS'
|
||||||
|
|
||||||
|
# The services URL
|
||||||
|
service_url = 'https://bulksms.com/'
|
||||||
|
|
||||||
|
# All notification requests are secure
|
||||||
|
secure_protocol = 'bulksms'
|
||||||
|
|
||||||
|
# A URL that takes you to the setup/help of the specific protocol
|
||||||
|
setup_url = 'https://github.com/caronc/apprise/wiki/Notify_bulksms'
|
||||||
|
|
||||||
|
# BulkSMS uses the http protocol with JSON requests
|
||||||
|
notify_url = 'https://api.bulksms.com/v1/messages'
|
||||||
|
|
||||||
|
# The maximum length of the body
|
||||||
|
body_maxlen = 160
|
||||||
|
|
||||||
|
# The maximum amount of texts that can go out in one batch
|
||||||
|
default_batch_size = 4000
|
||||||
|
|
||||||
|
# A title can not be used for SMS Messages. Setting this to zero will
|
||||||
|
# cause any title (if defined) to get placed into the message body.
|
||||||
|
title_maxlen = 0
|
||||||
|
|
||||||
|
# Define object templates
|
||||||
|
templates = (
|
||||||
|
'{schema}://{user}:{password}@{targets}',
|
||||||
|
)
|
||||||
|
|
||||||
|
# Define our template tokens
|
||||||
|
template_tokens = dict(NotifyBase.template_tokens, **{
|
||||||
|
'user': {
|
||||||
|
'name': _('User Name'),
|
||||||
|
'type': 'string',
|
||||||
|
},
|
||||||
|
'password': {
|
||||||
|
'name': _('Password'),
|
||||||
|
'type': 'string',
|
||||||
|
'private': True,
|
||||||
|
},
|
||||||
|
'target_phone': {
|
||||||
|
'name': _('Target Phone No'),
|
||||||
|
'type': 'string',
|
||||||
|
'prefix': '+',
|
||||||
|
'regex': (r'^[0-9\s)(+-]+$', 'i'),
|
||||||
|
'map_to': 'targets',
|
||||||
|
},
|
||||||
|
'target_group': {
|
||||||
|
'name': _('Target Group'),
|
||||||
|
'type': 'string',
|
||||||
|
'prefix': '+',
|
||||||
|
'regex': (r'^[A-Z0-9 _-]+$', 'i'),
|
||||||
|
'map_to': 'targets',
|
||||||
|
},
|
||||||
|
'targets': {
|
||||||
|
'name': _('Targets'),
|
||||||
|
'type': 'list:string',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
# Define our template arguments
|
||||||
|
template_args = dict(NotifyBase.template_args, **{
|
||||||
|
'to': {
|
||||||
|
'alias_of': 'targets',
|
||||||
|
},
|
||||||
|
'from': {
|
||||||
|
'name': _('From Phone No'),
|
||||||
|
'type': 'string',
|
||||||
|
'regex': (r'^\+?[0-9\s)(+-]+$', 'i'),
|
||||||
|
'map_to': 'source',
|
||||||
|
},
|
||||||
|
'route': {
|
||||||
|
'name': _('Route Group'),
|
||||||
|
'type': 'choice:string',
|
||||||
|
'values': BULKSMS_ROUTING_GROUPS,
|
||||||
|
'default': BulkSMSRoutingGroup.STANDARD,
|
||||||
|
},
|
||||||
|
'unicode': {
|
||||||
|
# Unicode characters
|
||||||
|
'name': _('Unicode Characters'),
|
||||||
|
'type': 'bool',
|
||||||
|
'default': True,
|
||||||
|
},
|
||||||
|
'batch': {
|
||||||
|
'name': _('Batch Mode'),
|
||||||
|
'type': 'bool',
|
||||||
|
'default': False,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
def __init__(self, source=None, targets=None, unicode=None, batch=None,
|
||||||
|
route=None, **kwargs):
|
||||||
|
"""
|
||||||
|
Initialize BulkSMS Object
|
||||||
|
"""
|
||||||
|
super(NotifyBulkSMS, self).__init__(**kwargs)
|
||||||
|
|
||||||
|
self.source = None
|
||||||
|
if source:
|
||||||
|
result = is_phone_no(source)
|
||||||
|
if not result:
|
||||||
|
msg = 'The Account (From) Phone # specified ' \
|
||||||
|
'({}) is invalid.'.format(source)
|
||||||
|
self.logger.warning(msg)
|
||||||
|
raise TypeError(msg)
|
||||||
|
|
||||||
|
# Tidy source
|
||||||
|
self.source = '+{}'.format(result['full'])
|
||||||
|
|
||||||
|
# Setup our route
|
||||||
|
self.route = self.template_args['route']['default'] \
|
||||||
|
if not isinstance(route, six.string_types) else route.upper()
|
||||||
|
if self.route not in BULKSMS_ROUTING_GROUPS:
|
||||||
|
msg = 'The route specified ({}) is invalid.'.format(route)
|
||||||
|
self.logger.warning(msg)
|
||||||
|
raise TypeError(msg)
|
||||||
|
|
||||||
|
# Define whether or not we should set the unicode flag
|
||||||
|
self.unicode = self.template_args['unicode']['default'] \
|
||||||
|
if unicode is None else bool(unicode)
|
||||||
|
|
||||||
|
# Define whether or not we should operate in a batch mode
|
||||||
|
self.batch = self.template_args['batch']['default'] \
|
||||||
|
if batch is None else bool(batch)
|
||||||
|
|
||||||
|
# Parse our targets
|
||||||
|
self.targets = list()
|
||||||
|
self.groups = list()
|
||||||
|
|
||||||
|
for target in parse_phone_no(targets):
|
||||||
|
# Parse each phone number we found
|
||||||
|
result = is_phone_no(target)
|
||||||
|
if result:
|
||||||
|
self.targets.append('+{}'.format(result['full']))
|
||||||
|
continue
|
||||||
|
|
||||||
|
group_re = IS_GROUP_RE.match(target)
|
||||||
|
if group_re and not target.isdigit():
|
||||||
|
# If the target specified is all digits, it MUST have a @
|
||||||
|
# in front of it to eliminate any ambiguity
|
||||||
|
self.groups.append(group_re.group('group'))
|
||||||
|
continue
|
||||||
|
|
||||||
|
self.logger.warning(
|
||||||
|
'Dropped invalid phone # and/or Group '
|
||||||
|
'({}) specified.'.format(target),
|
||||||
|
)
|
||||||
|
|
||||||
|
return
|
||||||
|
|
||||||
|
def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs):
|
||||||
|
"""
|
||||||
|
Perform BulkSMS Notification
|
||||||
|
"""
|
||||||
|
|
||||||
|
if not (self.password and self.user):
|
||||||
|
self.logger.warning(
|
||||||
|
'There were no valid login credentials provided')
|
||||||
|
return False
|
||||||
|
|
||||||
|
if not (self.targets or self.groups):
|
||||||
|
# We have nothing to notify
|
||||||
|
self.logger.warning('There are no Twist targets to notify')
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Send in batches if identified to do so
|
||||||
|
batch_size = 1 if not self.batch else self.default_batch_size
|
||||||
|
|
||||||
|
# error tracking (used for function return)
|
||||||
|
has_error = False
|
||||||
|
|
||||||
|
# Prepare our headers
|
||||||
|
headers = {
|
||||||
|
'User-Agent': self.app_id,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
}
|
||||||
|
|
||||||
|
# Prepare our payload
|
||||||
|
payload = {
|
||||||
|
# The To gets populated in the loop below
|
||||||
|
'to': None,
|
||||||
|
'body': body,
|
||||||
|
'routingGroup': self.route,
|
||||||
|
'encoding': BulkSMSEncoding.UNICODE \
|
||||||
|
if self.unicode else BulkSMSEncoding.TEXT,
|
||||||
|
# Options are NONE, ALL and ERRORS
|
||||||
|
'deliveryReports': "ERRORS"
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.source:
|
||||||
|
payload.update({
|
||||||
|
'from': self.source,
|
||||||
|
})
|
||||||
|
|
||||||
|
# Authentication
|
||||||
|
auth = (self.user, self.password)
|
||||||
|
|
||||||
|
# Prepare our targets
|
||||||
|
targets = list(self.targets) if batch_size == 1 else \
|
||||||
|
[self.targets[index:index + batch_size]
|
||||||
|
for index in range(0, len(self.targets), batch_size)]
|
||||||
|
targets += [{"type": "GROUP", "name": g} for g in self.groups]
|
||||||
|
|
||||||
|
while len(targets):
|
||||||
|
# Get our target to notify
|
||||||
|
target = targets.pop(0)
|
||||||
|
|
||||||
|
# Prepare our user
|
||||||
|
payload['to'] = target
|
||||||
|
|
||||||
|
# Printable reference
|
||||||
|
if isinstance(target, dict):
|
||||||
|
p_target = target['name']
|
||||||
|
|
||||||
|
elif isinstance(target, list):
|
||||||
|
p_target = '{} targets'.format(len(target))
|
||||||
|
|
||||||
|
else:
|
||||||
|
p_target = target
|
||||||
|
|
||||||
|
# Some Debug Logging
|
||||||
|
self.logger.debug('BulkSMS POST URL: {} (cert_verify={})'.format(
|
||||||
|
self.notify_url, self.verify_certificate))
|
||||||
|
self.logger.debug('BulkSMS Payload: {}' .format(payload))
|
||||||
|
|
||||||
|
# Always call throttle before any remote server i/o is made
|
||||||
|
self.throttle()
|
||||||
|
try:
|
||||||
|
r = requests.post(
|
||||||
|
self.notify_url,
|
||||||
|
data=json.dumps(payload),
|
||||||
|
headers=headers,
|
||||||
|
auth=auth,
|
||||||
|
verify=self.verify_certificate,
|
||||||
|
timeout=self.request_timeout,
|
||||||
|
)
|
||||||
|
|
||||||
|
# The responsne might look like:
|
||||||
|
# [
|
||||||
|
# {
|
||||||
|
# "id": "string",
|
||||||
|
# "type": "SENT",
|
||||||
|
# "from": "string",
|
||||||
|
# "to": "string",
|
||||||
|
# "body": null,
|
||||||
|
# "encoding": "TEXT",
|
||||||
|
# "protocolId": 0,
|
||||||
|
# "messageClass": 0,
|
||||||
|
# "numberOfParts": 0,
|
||||||
|
# "creditCost": 0,
|
||||||
|
# "submission": {...},
|
||||||
|
# "status": {...},
|
||||||
|
# "relatedSentMessageId": "string",
|
||||||
|
# "userSuppliedId": "string"
|
||||||
|
# }
|
||||||
|
# ]
|
||||||
|
|
||||||
|
if r.status_code not in (
|
||||||
|
requests.codes.created, requests.codes.ok):
|
||||||
|
# We had a problem
|
||||||
|
status_str = \
|
||||||
|
NotifyBase.http_response_code_lookup(r.status_code)
|
||||||
|
|
||||||
|
# set up our status code to use
|
||||||
|
status_code = r.status_code
|
||||||
|
|
||||||
|
self.logger.warning(
|
||||||
|
'Failed to send BulkSMS notification to {}: '
|
||||||
|
'{}{}error={}.'.format(
|
||||||
|
p_target,
|
||||||
|
status_str,
|
||||||
|
', ' if status_str else '',
|
||||||
|
status_code))
|
||||||
|
|
||||||
|
self.logger.debug(
|
||||||
|
'Response Details:\r\n{}'.format(r.content))
|
||||||
|
|
||||||
|
# Mark our failure
|
||||||
|
has_error = True
|
||||||
|
continue
|
||||||
|
|
||||||
|
else:
|
||||||
|
self.logger.info(
|
||||||
|
'Sent BulkSMS notification to {}.'.format(p_target))
|
||||||
|
|
||||||
|
except requests.RequestException as e:
|
||||||
|
self.logger.warning(
|
||||||
|
'A Connection error occurred sending BulkSMS: to %s ',
|
||||||
|
p_target)
|
||||||
|
self.logger.debug('Socket Exception: %s' % str(e))
|
||||||
|
|
||||||
|
# Mark our failure
|
||||||
|
has_error = True
|
||||||
|
continue
|
||||||
|
|
||||||
|
return not has_error
|
||||||
|
|
||||||
|
def url(self, privacy=False, *args, **kwargs):
|
||||||
|
"""
|
||||||
|
Returns the URL built dynamically based on specified arguments.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Define any URL parameters
|
||||||
|
params = {
|
||||||
|
'unicode': 'yes' if self.unicode else 'no',
|
||||||
|
'batch': 'yes' if self.batch else 'no',
|
||||||
|
'route': self.route,
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.source:
|
||||||
|
params['from'] = self.source
|
||||||
|
|
||||||
|
# Extend our parameters
|
||||||
|
params.update(self.url_parameters(privacy=privacy, *args, **kwargs))
|
||||||
|
|
||||||
|
return '{schema}://{user}:{password}@{targets}/?{params}'.format(
|
||||||
|
schema=self.secure_protocol,
|
||||||
|
user=self.pprint(self.user, privacy, safe=''),
|
||||||
|
password=self.pprint(
|
||||||
|
self.password, privacy, mode=PrivacyMode.Secret, safe=''),
|
||||||
|
targets='/'.join(chain(
|
||||||
|
[NotifyBulkSMS.quote('{}'.format(x), safe='+')
|
||||||
|
for x in self.targets],
|
||||||
|
[NotifyBulkSMS.quote('@{}'.format(x), safe='@')
|
||||||
|
for x in self.groups])),
|
||||||
|
params=NotifyBulkSMS.urlencode(params))
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def parse_url(url):
|
||||||
|
"""
|
||||||
|
Parses the URL and returns enough arguments that can allow
|
||||||
|
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
|
||||||
|
|
||||||
|
# Get our entries; split_path() looks after unquoting content for us
|
||||||
|
# by default
|
||||||
|
results['targets'] = [
|
||||||
|
NotifyBulkSMS.unquote(results['host']),
|
||||||
|
*NotifyBulkSMS.split_path(results['fullpath'])]
|
||||||
|
|
||||||
|
# Support the 'from' and 'source' variable so that we can support
|
||||||
|
# targets this way too.
|
||||||
|
# The 'from' makes it easier to use yaml configuration
|
||||||
|
if 'from' in results['qsd'] and len(results['qsd']['from']):
|
||||||
|
results['source'] = \
|
||||||
|
NotifyBulkSMS.unquote(results['qsd']['from'])
|
||||||
|
|
||||||
|
# Support the 'to' variable so that we can support targets this way too
|
||||||
|
# The 'to' makes it easier to use yaml configuration
|
||||||
|
if 'to' in results['qsd'] and len(results['qsd']['to']):
|
||||||
|
results['targets'] += \
|
||||||
|
NotifyBulkSMS.parse_phone_no(results['qsd']['to'])
|
||||||
|
|
||||||
|
# Unicode Characters
|
||||||
|
results['unicode'] = \
|
||||||
|
parse_bool(results['qsd'].get(
|
||||||
|
'unicode', NotifyBulkSMS.template_args['unicode']['default']))
|
||||||
|
|
||||||
|
# Get Batch Mode Flag
|
||||||
|
results['batch'] = \
|
||||||
|
parse_bool(results['qsd'].get(
|
||||||
|
'batch', NotifyBulkSMS.template_args['batch']['default']))
|
||||||
|
|
||||||
|
# Allow one to define a route group
|
||||||
|
if 'route' in results['qsd'] and len(results['qsd']['route']):
|
||||||
|
results['route'] = \
|
||||||
|
NotifyBulkSMS.unquote(results['qsd']['route'])
|
||||||
|
|
||||||
|
return results
|
|
@ -30,7 +30,6 @@
|
||||||
# (both user and password) from the API Details section from within your
|
# (both user and password) from the API Details section from within your
|
||||||
# account profile area: https://d7networks.com/accounts/profile/
|
# account profile area: https://d7networks.com/accounts/profile/
|
||||||
|
|
||||||
import six
|
|
||||||
import requests
|
import requests
|
||||||
import base64
|
import base64
|
||||||
from json import dumps
|
from json import dumps
|
||||||
|
@ -54,7 +53,7 @@ D7NETWORKS_HTTP_ERROR_MAP = {
|
||||||
|
|
||||||
|
|
||||||
# Priorities
|
# Priorities
|
||||||
class D7SMSPriority(object):
|
class D7SMSPriority:
|
||||||
"""
|
"""
|
||||||
D7 Networks SMS Message Priority
|
D7 Networks SMS Message Priority
|
||||||
"""
|
"""
|
||||||
|
@ -192,7 +191,7 @@ class NotifyD7Networks(NotifyBase):
|
||||||
|
|
||||||
# Setup our source address (if defined)
|
# Setup our source address (if defined)
|
||||||
self.source = None \
|
self.source = None \
|
||||||
if not isinstance(source, six.string_types) else source.strip()
|
if not isinstance(source, str) else source.strip()
|
||||||
|
|
||||||
if not (self.user and self.password):
|
if not (self.user and self.password):
|
||||||
msg = 'A D7 Networks user/pass was not provided.'
|
msg = 'A D7 Networks user/pass was not provided.'
|
||||||
|
@ -232,10 +231,10 @@ class NotifyD7Networks(NotifyBase):
|
||||||
|
|
||||||
auth = '{user}:{password}'.format(
|
auth = '{user}:{password}'.format(
|
||||||
user=self.user, password=self.password)
|
user=self.user, password=self.password)
|
||||||
if six.PY3:
|
|
||||||
# Python 3's versio of b64encode() expects a byte array and not
|
# Python 3's versio of b64encode() expects a byte array and not
|
||||||
# a string. To accomodate this, we encode the content here
|
# a string. To accommodate this, we encode the content here
|
||||||
auth = auth.encode('utf-8')
|
auth = auth.encode('utf-8')
|
||||||
|
|
||||||
# Prepare our headers
|
# Prepare our headers
|
||||||
headers = {
|
headers = {
|
||||||
|
|
|
@ -60,7 +60,7 @@ try:
|
||||||
from dbus.mainloop.glib import DBusGMainLoop
|
from dbus.mainloop.glib import DBusGMainLoop
|
||||||
LOOP_GLIB = DBusGMainLoop()
|
LOOP_GLIB = DBusGMainLoop()
|
||||||
|
|
||||||
except ImportError:
|
except ImportError: # pragma: no cover
|
||||||
# No problem
|
# No problem
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
@ -109,18 +109,36 @@ MAINLOOP_MAP = {
|
||||||
|
|
||||||
|
|
||||||
# Urgencies
|
# Urgencies
|
||||||
class DBusUrgency(object):
|
class DBusUrgency:
|
||||||
LOW = 0
|
LOW = 0
|
||||||
NORMAL = 1
|
NORMAL = 1
|
||||||
HIGH = 2
|
HIGH = 2
|
||||||
|
|
||||||
|
|
||||||
# Define our urgency levels
|
DBUS_URGENCIES = {
|
||||||
DBUS_URGENCIES = (
|
# Note: This also acts as a reverse lookup mapping
|
||||||
DBusUrgency.LOW,
|
DBusUrgency.LOW: 'low',
|
||||||
DBusUrgency.NORMAL,
|
DBusUrgency.NORMAL: 'normal',
|
||||||
DBusUrgency.HIGH,
|
DBusUrgency.HIGH: 'high',
|
||||||
)
|
}
|
||||||
|
|
||||||
|
DBUS_URGENCY_MAP = {
|
||||||
|
# Maps against string 'low'
|
||||||
|
'l': DBusUrgency.LOW,
|
||||||
|
# Maps against string 'moderate'
|
||||||
|
'm': DBusUrgency.LOW,
|
||||||
|
# Maps against string 'normal'
|
||||||
|
'n': DBusUrgency.NORMAL,
|
||||||
|
# Maps against string 'high'
|
||||||
|
'h': DBusUrgency.HIGH,
|
||||||
|
# Maps against string 'emergency'
|
||||||
|
'e': DBusUrgency.HIGH,
|
||||||
|
|
||||||
|
# Entries to additionally support (so more like DBus's API)
|
||||||
|
'0': DBusUrgency.LOW,
|
||||||
|
'1': DBusUrgency.NORMAL,
|
||||||
|
'2': DBusUrgency.HIGH,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class NotifyDBus(NotifyBase):
|
class NotifyDBus(NotifyBase):
|
||||||
|
@ -143,10 +161,11 @@ class NotifyDBus(NotifyBase):
|
||||||
service_url = 'http://www.freedesktop.org/Software/dbus/'
|
service_url = 'http://www.freedesktop.org/Software/dbus/'
|
||||||
|
|
||||||
# The default protocols
|
# The default protocols
|
||||||
# Python 3 keys() does not return a list object, it's it's own dict_keys()
|
# Python 3 keys() does not return a list object, it is its own dict_keys()
|
||||||
# object if we were to reference, we wouldn't be backwards compatible with
|
# 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
|
# Python v2. So converting the result set back into a list makes us
|
||||||
# compatible
|
# compatible
|
||||||
|
# TODO: Review after dropping support for Python 2.
|
||||||
protocol = list(MAINLOOP_MAP.keys())
|
protocol = list(MAINLOOP_MAP.keys())
|
||||||
|
|
||||||
# A URL that takes you to the setup/help of the specific protocol
|
# A URL that takes you to the setup/help of the specific protocol
|
||||||
|
@ -182,6 +201,12 @@ class NotifyDBus(NotifyBase):
|
||||||
'values': DBUS_URGENCIES,
|
'values': DBUS_URGENCIES,
|
||||||
'default': DBusUrgency.NORMAL,
|
'default': DBusUrgency.NORMAL,
|
||||||
},
|
},
|
||||||
|
'priority': {
|
||||||
|
# Apprise uses 'priority' everywhere; it's just a nice consistent
|
||||||
|
# feel to be able to use it here as well. Just map the
|
||||||
|
# value back to 'priority'
|
||||||
|
'alias_of': 'urgency',
|
||||||
|
},
|
||||||
'x': {
|
'x': {
|
||||||
'name': _('X-Axis'),
|
'name': _('X-Axis'),
|
||||||
'type': 'int',
|
'type': 'int',
|
||||||
|
@ -223,15 +248,29 @@ class NotifyDBus(NotifyBase):
|
||||||
raise TypeError(msg)
|
raise TypeError(msg)
|
||||||
|
|
||||||
# The urgency of the message
|
# The urgency of the message
|
||||||
if urgency not in DBUS_URGENCIES:
|
self.urgency = int(
|
||||||
self.urgency = DBusUrgency.NORMAL
|
NotifyDBus.template_args['urgency']['default']
|
||||||
|
if urgency is None else
|
||||||
else:
|
next((
|
||||||
self.urgency = urgency
|
v for k, v in DBUS_URGENCY_MAP.items()
|
||||||
|
if str(urgency).lower().startswith(k)),
|
||||||
|
NotifyDBus.template_args['urgency']['default']))
|
||||||
|
|
||||||
# Our x/y axis settings
|
# Our x/y axis settings
|
||||||
self.x_axis = x_axis if isinstance(x_axis, int) else None
|
if x_axis or y_axis:
|
||||||
self.y_axis = y_axis if isinstance(y_axis, int) else None
|
try:
|
||||||
|
self.x_axis = int(x_axis)
|
||||||
|
self.y_axis = int(y_axis)
|
||||||
|
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
# Invalid x/y values specified
|
||||||
|
msg = 'The x,y coordinates specified ({},{}) are invalid.'\
|
||||||
|
.format(x_axis, y_axis)
|
||||||
|
self.logger.warning(msg)
|
||||||
|
raise TypeError(msg)
|
||||||
|
else:
|
||||||
|
self.x_axis = None
|
||||||
|
self.y_axis = None
|
||||||
|
|
||||||
# Track whether or not we want to send an image with our notification
|
# Track whether or not we want to send an image with our notification
|
||||||
# or not.
|
# or not.
|
||||||
|
@ -343,17 +382,13 @@ class NotifyDBus(NotifyBase):
|
||||||
Returns the URL built dynamically based on specified arguments.
|
Returns the URL built dynamically based on specified arguments.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
_map = {
|
|
||||||
DBusUrgency.LOW: 'low',
|
|
||||||
DBusUrgency.NORMAL: 'normal',
|
|
||||||
DBusUrgency.HIGH: 'high',
|
|
||||||
}
|
|
||||||
|
|
||||||
# Define any URL parameters
|
# Define any URL parameters
|
||||||
params = {
|
params = {
|
||||||
'image': 'yes' if self.include_image else 'no',
|
'image': 'yes' if self.include_image else 'no',
|
||||||
'urgency': 'normal' if self.urgency not in _map
|
'urgency':
|
||||||
else _map[self.urgency],
|
DBUS_URGENCIES[self.template_args['urgency']['default']]
|
||||||
|
if self.urgency not in DBUS_URGENCIES
|
||||||
|
else DBUS_URGENCIES[self.urgency],
|
||||||
}
|
}
|
||||||
|
|
||||||
# Extend our parameters
|
# Extend our parameters
|
||||||
|
@ -389,38 +424,20 @@ class NotifyDBus(NotifyBase):
|
||||||
|
|
||||||
# DBus supports urgency, but we we also support the keyword priority
|
# DBus supports urgency, but we we also support the keyword priority
|
||||||
# so that it is consistent with some of the other plugins
|
# so that it is consistent with some of the other plugins
|
||||||
urgency = results['qsd'].get('urgency', results['qsd'].get('priority'))
|
if 'priority' in results['qsd'] and len(results['qsd']['priority']):
|
||||||
if urgency and len(urgency):
|
# We intentionally store the priority in the urgency section
|
||||||
_map = {
|
results['urgency'] = \
|
||||||
'0': DBusUrgency.LOW,
|
NotifyDBus.unquote(results['qsd']['priority'])
|
||||||
'l': DBusUrgency.LOW,
|
|
||||||
'n': DBusUrgency.NORMAL,
|
|
||||||
'1': DBusUrgency.NORMAL,
|
|
||||||
'h': DBusUrgency.HIGH,
|
|
||||||
'2': DBusUrgency.HIGH,
|
|
||||||
}
|
|
||||||
|
|
||||||
try:
|
if 'urgency' in results['qsd'] and len(results['qsd']['urgency']):
|
||||||
# Attempt to index/retrieve our urgency
|
results['urgency'] = \
|
||||||
results['urgency'] = _map[urgency[0].lower()]
|
NotifyDBus.unquote(results['qsd']['urgency'])
|
||||||
|
|
||||||
except KeyError:
|
|
||||||
# No priority was set
|
|
||||||
pass
|
|
||||||
|
|
||||||
# handle x,y coordinates
|
# handle x,y coordinates
|
||||||
try:
|
if 'x' in results['qsd'] and len(results['qsd']['x']):
|
||||||
results['x_axis'] = int(results['qsd'].get('x'))
|
results['x_axis'] = NotifyDBus.unquote(results['qsd'].get('x'))
|
||||||
|
|
||||||
except (TypeError, ValueError):
|
if 'y' in results['qsd'] and len(results['qsd']['y']):
|
||||||
# No x was set
|
results['y_axis'] = NotifyDBus.unquote(results['qsd'].get('y'))
|
||||||
pass
|
|
||||||
|
|
||||||
try:
|
|
||||||
results['y_axis'] = int(results['qsd'].get('y'))
|
|
||||||
|
|
||||||
except (TypeError, ValueError):
|
|
||||||
# No y was set
|
|
||||||
pass
|
|
||||||
|
|
||||||
return results
|
return results
|
||||||
|
|
|
@ -58,15 +58,27 @@ from ..utils import parse_list
|
||||||
from ..utils import parse_bool
|
from ..utils import parse_bool
|
||||||
|
|
||||||
|
|
||||||
class DapnetPriority(object):
|
class DapnetPriority:
|
||||||
NORMAL = 0
|
NORMAL = 0
|
||||||
EMERGENCY = 1
|
EMERGENCY = 1
|
||||||
|
|
||||||
|
|
||||||
DAPNET_PRIORITIES = (
|
DAPNET_PRIORITIES = {
|
||||||
DapnetPriority.NORMAL,
|
DapnetPriority.NORMAL: 'normal',
|
||||||
DapnetPriority.EMERGENCY,
|
DapnetPriority.EMERGENCY: 'emergency',
|
||||||
)
|
}
|
||||||
|
|
||||||
|
|
||||||
|
DAPNET_PRIORITY_MAP = {
|
||||||
|
# Maps against string 'normal'
|
||||||
|
'n': DapnetPriority.NORMAL,
|
||||||
|
# Maps against string 'emergency'
|
||||||
|
'e': DapnetPriority.EMERGENCY,
|
||||||
|
|
||||||
|
# Entries to additionally support (so more like Dapnet's API)
|
||||||
|
'0': DapnetPriority.NORMAL,
|
||||||
|
'1': DapnetPriority.EMERGENCY,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class NotifyDapnet(NotifyBase):
|
class NotifyDapnet(NotifyBase):
|
||||||
|
@ -172,11 +184,14 @@ class NotifyDapnet(NotifyBase):
|
||||||
# Parse our targets
|
# Parse our targets
|
||||||
self.targets = list()
|
self.targets = list()
|
||||||
|
|
||||||
# get the emergency prio setting
|
# The Priority of the message
|
||||||
if priority not in DAPNET_PRIORITIES:
|
self.priority = int(
|
||||||
self.priority = self.template_args['priority']['default']
|
NotifyDapnet.template_args['priority']['default']
|
||||||
else:
|
if priority is None else
|
||||||
self.priority = priority
|
next((
|
||||||
|
v for k, v in DAPNET_PRIORITY_MAP.items()
|
||||||
|
if str(priority).lower().startswith(k)),
|
||||||
|
NotifyDapnet.template_args['priority']['default']))
|
||||||
|
|
||||||
if not (self.user and self.password):
|
if not (self.user and self.password):
|
||||||
msg = 'A Dapnet user/pass was not provided.'
|
msg = 'A Dapnet user/pass was not provided.'
|
||||||
|
@ -201,8 +216,7 @@ class NotifyDapnet(NotifyBase):
|
||||||
)
|
)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Store callsign without SSID and
|
# Store callsign without SSID and ignore duplicates
|
||||||
# ignore duplicates
|
|
||||||
if result['callsign'] not in self.targets:
|
if result['callsign'] not in self.targets:
|
||||||
self.targets.append(result['callsign'])
|
self.targets.append(result['callsign'])
|
||||||
|
|
||||||
|
@ -230,10 +244,6 @@ class NotifyDapnet(NotifyBase):
|
||||||
# error tracking (used for function return)
|
# error tracking (used for function return)
|
||||||
has_error = False
|
has_error = False
|
||||||
|
|
||||||
# prepare the emergency mode
|
|
||||||
emergency_mode = True \
|
|
||||||
if self.priority == DapnetPriority.EMERGENCY else False
|
|
||||||
|
|
||||||
# Create a copy of the targets list
|
# Create a copy of the targets list
|
||||||
targets = list(self.targets)
|
targets = list(self.targets)
|
||||||
|
|
||||||
|
@ -244,7 +254,7 @@ class NotifyDapnet(NotifyBase):
|
||||||
'text': body,
|
'text': body,
|
||||||
'callSignNames': targets[index:index + batch_size],
|
'callSignNames': targets[index:index + batch_size],
|
||||||
'transmitterGroupNames': self.txgroups,
|
'transmitterGroupNames': self.txgroups,
|
||||||
'emergency': emergency_mode,
|
'emergency': (self.priority == DapnetPriority.EMERGENCY),
|
||||||
}
|
}
|
||||||
|
|
||||||
self.logger.debug('DAPNET POST URL: %s' % self.notify_url)
|
self.logger.debug('DAPNET POST URL: %s' % self.notify_url)
|
||||||
|
@ -304,16 +314,12 @@ class NotifyDapnet(NotifyBase):
|
||||||
Returns the URL built dynamically based on specified arguments.
|
Returns the URL built dynamically based on specified arguments.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# Define any URL parameters
|
|
||||||
_map = {
|
|
||||||
DapnetPriority.NORMAL: 'normal',
|
|
||||||
DapnetPriority.EMERGENCY: 'emergency',
|
|
||||||
}
|
|
||||||
|
|
||||||
# Define any URL parameters
|
# Define any URL parameters
|
||||||
params = {
|
params = {
|
||||||
'priority': 'normal' if self.priority not in _map
|
'priority':
|
||||||
else _map[self.priority],
|
DAPNET_PRIORITIES[self.template_args['priority']['default']]
|
||||||
|
if self.priority not in DAPNET_PRIORITIES
|
||||||
|
else DAPNET_PRIORITIES[self.priority],
|
||||||
'batch': 'yes' if self.batch else 'no',
|
'batch': 'yes' if self.batch else 'no',
|
||||||
'txgroups': ','.join(self.txgroups),
|
'txgroups': ','.join(self.txgroups),
|
||||||
}
|
}
|
||||||
|
@ -361,25 +367,10 @@ class NotifyDapnet(NotifyBase):
|
||||||
results['targets'] += \
|
results['targets'] += \
|
||||||
NotifyDapnet.parse_list(results['qsd']['to'])
|
NotifyDapnet.parse_list(results['qsd']['to'])
|
||||||
|
|
||||||
# Check for priority
|
# Set our priority
|
||||||
if 'priority' in results['qsd'] and len(results['qsd']['priority']):
|
if 'priority' in results['qsd'] and len(results['qsd']['priority']):
|
||||||
_map = {
|
results['priority'] = \
|
||||||
# Letter Assignments
|
NotifyDapnet.unquote(results['qsd']['priority'])
|
||||||
'n': DapnetPriority.NORMAL,
|
|
||||||
'e': DapnetPriority.EMERGENCY,
|
|
||||||
'no': DapnetPriority.NORMAL,
|
|
||||||
'em': DapnetPriority.EMERGENCY,
|
|
||||||
# Numeric assignments
|
|
||||||
'0': DapnetPriority.NORMAL,
|
|
||||||
'1': DapnetPriority.EMERGENCY,
|
|
||||||
}
|
|
||||||
try:
|
|
||||||
results['priority'] = \
|
|
||||||
_map[results['qsd']['priority'][0:2].lower()]
|
|
||||||
|
|
||||||
except KeyError:
|
|
||||||
# No priority was set
|
|
||||||
pass
|
|
||||||
|
|
||||||
# Check for one or multiple transmitter groups (comma separated)
|
# Check for one or multiple transmitter groups (comma separated)
|
||||||
# and split them up, when necessary
|
# and split them up, when necessary
|
||||||
|
|
|
@ -128,6 +128,12 @@ class NotifyDiscord(NotifyBase):
|
||||||
'name': _('Avatar URL'),
|
'name': _('Avatar URL'),
|
||||||
'type': 'string',
|
'type': 'string',
|
||||||
},
|
},
|
||||||
|
# Send a message to the specified thread within a webhook's channel.
|
||||||
|
# The thread will automatically be unarchived.
|
||||||
|
'thread': {
|
||||||
|
'name': _('Thread ID'),
|
||||||
|
'type': 'string',
|
||||||
|
},
|
||||||
'footer': {
|
'footer': {
|
||||||
'name': _('Display Footer'),
|
'name': _('Display Footer'),
|
||||||
'type': 'bool',
|
'type': 'bool',
|
||||||
|
@ -153,7 +159,7 @@ class NotifyDiscord(NotifyBase):
|
||||||
|
|
||||||
def __init__(self, webhook_id, webhook_token, tts=False, avatar=True,
|
def __init__(self, webhook_id, webhook_token, tts=False, avatar=True,
|
||||||
footer=False, footer_logo=True, include_image=False,
|
footer=False, footer_logo=True, include_image=False,
|
||||||
fields=True, avatar_url=None, **kwargs):
|
fields=True, avatar_url=None, thread=None, **kwargs):
|
||||||
"""
|
"""
|
||||||
Initialize Discord Object
|
Initialize Discord Object
|
||||||
|
|
||||||
|
@ -194,6 +200,9 @@ class NotifyDiscord(NotifyBase):
|
||||||
# Use Fields
|
# Use Fields
|
||||||
self.fields = fields
|
self.fields = fields
|
||||||
|
|
||||||
|
# Specified Thread ID
|
||||||
|
self.thread_id = thread
|
||||||
|
|
||||||
# Avatar URL
|
# Avatar URL
|
||||||
# This allows a user to provide an over-ride to the otherwise
|
# This allows a user to provide an over-ride to the otherwise
|
||||||
# dynamically generated avatar url images
|
# dynamically generated avatar url images
|
||||||
|
@ -274,6 +283,9 @@ class NotifyDiscord(NotifyBase):
|
||||||
payload['content'] = \
|
payload['content'] = \
|
||||||
body if not title else "{}\r\n{}".format(title, body)
|
body if not title else "{}\r\n{}".format(title, body)
|
||||||
|
|
||||||
|
if self.thread_id:
|
||||||
|
payload['thread_id'] = self.thread_id
|
||||||
|
|
||||||
if self.avatar and (image_url or self.avatar_url):
|
if self.avatar and (image_url or self.avatar_url):
|
||||||
payload['avatar_url'] = \
|
payload['avatar_url'] = \
|
||||||
self.avatar_url if self.avatar_url else image_url
|
self.avatar_url if self.avatar_url else image_url
|
||||||
|
@ -447,6 +459,9 @@ class NotifyDiscord(NotifyBase):
|
||||||
if self.avatar_url:
|
if self.avatar_url:
|
||||||
params['avatar_url'] = self.avatar_url
|
params['avatar_url'] = self.avatar_url
|
||||||
|
|
||||||
|
if self.thread_id:
|
||||||
|
params['thread'] = self.thread_id
|
||||||
|
|
||||||
# Extend our parameters
|
# Extend our parameters
|
||||||
params.update(self.url_parameters(privacy=privacy, *args, **kwargs))
|
params.update(self.url_parameters(privacy=privacy, *args, **kwargs))
|
||||||
|
|
||||||
|
@ -515,6 +530,11 @@ class NotifyDiscord(NotifyBase):
|
||||||
results['avatar_url'] = \
|
results['avatar_url'] = \
|
||||||
NotifyDiscord.unquote(results['qsd']['avatar_url'])
|
NotifyDiscord.unquote(results['qsd']['avatar_url'])
|
||||||
|
|
||||||
|
# Extract thread id if it was specified
|
||||||
|
if 'thread' in results['qsd']:
|
||||||
|
results['thread'] = \
|
||||||
|
NotifyDiscord.unquote(results['qsd']['thread'])
|
||||||
|
|
||||||
return results
|
return results
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
|
|
|
@ -24,12 +24,11 @@
|
||||||
# THE SOFTWARE.
|
# THE SOFTWARE.
|
||||||
|
|
||||||
import re
|
import re
|
||||||
import six
|
|
||||||
import smtplib
|
import smtplib
|
||||||
from email.mime.text import MIMEText
|
from email.mime.text import MIMEText
|
||||||
from email.mime.application import MIMEApplication
|
from email.mime.application import MIMEApplication
|
||||||
from email.mime.multipart import MIMEMultipart
|
from email.mime.multipart import MIMEMultipart
|
||||||
from email.utils import formataddr
|
from email.utils import formataddr, make_msgid
|
||||||
from email.header import Header
|
from email.header import Header
|
||||||
from email import charset
|
from email import charset
|
||||||
|
|
||||||
|
@ -38,17 +37,16 @@ from datetime import datetime
|
||||||
|
|
||||||
from .NotifyBase import NotifyBase
|
from .NotifyBase import NotifyBase
|
||||||
from ..URLBase import PrivacyMode
|
from ..URLBase import PrivacyMode
|
||||||
from ..common import NotifyFormat
|
from ..common import NotifyFormat, NotifyType
|
||||||
from ..common import NotifyType
|
from ..conversion import convert_between
|
||||||
from ..utils import is_email
|
from ..utils import is_email, parse_emails
|
||||||
from ..utils import parse_emails
|
|
||||||
from ..AppriseLocale import gettext_lazy as _
|
from ..AppriseLocale import gettext_lazy as _
|
||||||
|
|
||||||
# Globally Default encoding mode set to Quoted Printable.
|
# Globally Default encoding mode set to Quoted Printable.
|
||||||
charset.add_charset('utf-8', charset.QP, charset.QP, 'utf-8')
|
charset.add_charset('utf-8', charset.QP, charset.QP, 'utf-8')
|
||||||
|
|
||||||
|
|
||||||
class WebBaseLogin(object):
|
class WebBaseLogin:
|
||||||
"""
|
"""
|
||||||
This class is just used in conjunction of the default emailers
|
This class is just used in conjunction of the default emailers
|
||||||
to best formulate a login to it using the data detected
|
to best formulate a login to it using the data detected
|
||||||
|
@ -61,7 +59,7 @@ class WebBaseLogin(object):
|
||||||
|
|
||||||
|
|
||||||
# Secure Email Modes
|
# Secure Email Modes
|
||||||
class SecureMailMode(object):
|
class SecureMailMode:
|
||||||
SSL = "ssl"
|
SSL = "ssl"
|
||||||
STARTTLS = "starttls"
|
STARTTLS = "starttls"
|
||||||
|
|
||||||
|
@ -91,21 +89,6 @@ EMAIL_TEMPLATES = (
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|
||||||
# Pronto Mail
|
|
||||||
(
|
|
||||||
'Pronto Mail',
|
|
||||||
re.compile(
|
|
||||||
r'^((?P<label>[^+]+)\+)?(?P<id>[^@]+)@'
|
|
||||||
r'(?P<domain>prontomail\.com)$', re.I),
|
|
||||||
{
|
|
||||||
'port': 465,
|
|
||||||
'smtp_host': 'secure.emailsrvr.com',
|
|
||||||
'secure': True,
|
|
||||||
'secure_mode': SecureMailMode.STARTTLS,
|
|
||||||
'login_type': (WebBaseLogin.EMAIL, )
|
|
||||||
},
|
|
||||||
),
|
|
||||||
|
|
||||||
# Yandex
|
# Yandex
|
||||||
(
|
(
|
||||||
'Yandex',
|
'Yandex',
|
||||||
|
@ -126,7 +109,7 @@ EMAIL_TEMPLATES = (
|
||||||
'Microsoft Hotmail',
|
'Microsoft Hotmail',
|
||||||
re.compile(
|
re.compile(
|
||||||
r'^((?P<label>[^+]+)\+)?(?P<id>[^@]+)@'
|
r'^((?P<label>[^+]+)\+)?(?P<id>[^@]+)@'
|
||||||
r'(?P<domain>(hotmail|live)\.com)$', re.I),
|
r'(?P<domain>(outlook|hotmail|live)\.com(\.au)?)$', re.I),
|
||||||
{
|
{
|
||||||
'port': 587,
|
'port': 587,
|
||||||
'smtp_host': 'smtp-mail.outlook.com',
|
'smtp_host': 'smtp-mail.outlook.com',
|
||||||
|
@ -412,6 +395,11 @@ class NotifyEmail(NotifyBase):
|
||||||
'default': SecureMailMode.STARTTLS,
|
'default': SecureMailMode.STARTTLS,
|
||||||
'map_to': 'secure_mode',
|
'map_to': 'secure_mode',
|
||||||
},
|
},
|
||||||
|
'reply': {
|
||||||
|
'name': _('Reply To'),
|
||||||
|
'type': 'list:string',
|
||||||
|
'map_to': 'reply_to',
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
# Define any kwargs we're using
|
# Define any kwargs we're using
|
||||||
|
@ -424,7 +412,7 @@ class NotifyEmail(NotifyBase):
|
||||||
|
|
||||||
def __init__(self, smtp_host=None, from_name=None,
|
def __init__(self, smtp_host=None, from_name=None,
|
||||||
from_addr=None, secure_mode=None, targets=None, cc=None,
|
from_addr=None, secure_mode=None, targets=None, cc=None,
|
||||||
bcc=None, headers=None, **kwargs):
|
bcc=None, reply_to=None, headers=None, **kwargs):
|
||||||
"""
|
"""
|
||||||
Initialize Email Object
|
Initialize Email Object
|
||||||
|
|
||||||
|
@ -450,6 +438,9 @@ class NotifyEmail(NotifyBase):
|
||||||
# Acquire Blind Carbon Copies
|
# Acquire Blind Carbon Copies
|
||||||
self.bcc = set()
|
self.bcc = set()
|
||||||
|
|
||||||
|
# Acquire Reply To
|
||||||
|
self.reply_to = set()
|
||||||
|
|
||||||
# For tracking our email -> name lookups
|
# For tracking our email -> name lookups
|
||||||
self.names = {}
|
self.names = {}
|
||||||
|
|
||||||
|
@ -482,13 +473,17 @@ class NotifyEmail(NotifyBase):
|
||||||
# Set our from name
|
# Set our from name
|
||||||
self.from_name = from_name if from_name else result['name']
|
self.from_name = from_name if from_name else result['name']
|
||||||
|
|
||||||
|
# Store our lookup
|
||||||
|
self.names[self.from_addr] = \
|
||||||
|
self.from_name if self.from_name else False
|
||||||
|
|
||||||
# Now detect the SMTP Server
|
# Now detect the SMTP Server
|
||||||
self.smtp_host = \
|
self.smtp_host = \
|
||||||
smtp_host if isinstance(smtp_host, six.string_types) else ''
|
smtp_host if isinstance(smtp_host, str) else ''
|
||||||
|
|
||||||
# Now detect secure mode
|
# Now detect secure mode
|
||||||
self.secure_mode = self.default_secure_mode \
|
self.secure_mode = self.default_secure_mode \
|
||||||
if not isinstance(secure_mode, six.string_types) \
|
if not isinstance(secure_mode, str) \
|
||||||
else secure_mode.lower()
|
else secure_mode.lower()
|
||||||
if self.secure_mode not in SECURE_MODES:
|
if self.secure_mode not in SECURE_MODES:
|
||||||
msg = 'The secure mode specified ({}) is invalid.'\
|
msg = 'The secure mode specified ({}) is invalid.'\
|
||||||
|
@ -548,8 +543,24 @@ class NotifyEmail(NotifyBase):
|
||||||
'({}) specified.'.format(recipient),
|
'({}) specified.'.format(recipient),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Validate recipients (reply-to:) and drop bad ones:
|
||||||
|
for recipient in parse_emails(reply_to):
|
||||||
|
email = is_email(recipient)
|
||||||
|
if email:
|
||||||
|
self.reply_to.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(
|
||||||
|
'Dropped invalid Reply To email '
|
||||||
|
'({}) specified.'.format(recipient),
|
||||||
|
)
|
||||||
|
|
||||||
# Apply any defaults based on certain known configurations
|
# Apply any defaults based on certain known configurations
|
||||||
self.NotifyEmailDefaults()
|
self.NotifyEmailDefaults(secure_mode=secure_mode, **kwargs)
|
||||||
|
|
||||||
# if there is still no smtp_host then we fall back to the hostname
|
# if there is still no smtp_host then we fall back to the hostname
|
||||||
if not self.smtp_host:
|
if not self.smtp_host:
|
||||||
|
@ -557,7 +568,7 @@ class NotifyEmail(NotifyBase):
|
||||||
|
|
||||||
return
|
return
|
||||||
|
|
||||||
def NotifyEmailDefaults(self):
|
def NotifyEmailDefaults(self, secure_mode=None, port=None, **kwargs):
|
||||||
"""
|
"""
|
||||||
A function that prefills defaults based on the email
|
A function that prefills defaults based on the email
|
||||||
it was provided.
|
it was provided.
|
||||||
|
@ -586,18 +597,23 @@ class NotifyEmail(NotifyBase):
|
||||||
'Applying %s Defaults' %
|
'Applying %s Defaults' %
|
||||||
EMAIL_TEMPLATES[i][0],
|
EMAIL_TEMPLATES[i][0],
|
||||||
)
|
)
|
||||||
self.port = EMAIL_TEMPLATES[i][2]\
|
# the secure flag can not be altered if defined in the template
|
||||||
.get('port', self.port)
|
|
||||||
self.secure = EMAIL_TEMPLATES[i][2]\
|
self.secure = EMAIL_TEMPLATES[i][2]\
|
||||||
.get('secure', self.secure)
|
.get('secure', self.secure)
|
||||||
self.secure_mode = EMAIL_TEMPLATES[i][2]\
|
|
||||||
.get('secure_mode', self.secure_mode)
|
# The SMTP Host check is already done above; if it was
|
||||||
|
# specified we wouldn't even reach this part of the code.
|
||||||
self.smtp_host = EMAIL_TEMPLATES[i][2]\
|
self.smtp_host = EMAIL_TEMPLATES[i][2]\
|
||||||
.get('smtp_host', self.smtp_host)
|
.get('smtp_host', self.smtp_host)
|
||||||
|
|
||||||
if self.smtp_host is None:
|
# The following can be over-ridden if defined manually in the
|
||||||
# default to our host
|
# Apprise URL. Otherwise they take on the template value
|
||||||
self.smtp_host = self.host
|
if not port:
|
||||||
|
self.port = EMAIL_TEMPLATES[i][2]\
|
||||||
|
.get('port', self.port)
|
||||||
|
if not secure_mode:
|
||||||
|
self.secure_mode = EMAIL_TEMPLATES[i][2]\
|
||||||
|
.get('secure_mode', self.secure_mode)
|
||||||
|
|
||||||
# Adjust email login based on the defined usertype. If no entry
|
# Adjust email login based on the defined usertype. If no entry
|
||||||
# was specified, then we default to having them all set (which
|
# was specified, then we default to having them all set (which
|
||||||
|
@ -622,6 +638,18 @@ class NotifyEmail(NotifyBase):
|
||||||
|
|
||||||
break
|
break
|
||||||
|
|
||||||
|
def _get_charset(self, input_string):
|
||||||
|
"""
|
||||||
|
Get utf-8 charset if non ascii string only
|
||||||
|
|
||||||
|
Encode an ascii string to utf-8 is bad for email deliverability
|
||||||
|
because some anti-spam gives a bad score for that
|
||||||
|
like SUBJ_EXCESS_QP flag on Rspamd
|
||||||
|
"""
|
||||||
|
if not input_string:
|
||||||
|
return None
|
||||||
|
return 'utf-8' if not all(ord(c) < 128 for c in input_string) else None
|
||||||
|
|
||||||
def send(self, body, title='', notify_type=NotifyType.INFO, attach=None,
|
def send(self, body, title='', notify_type=NotifyType.INFO, attach=None,
|
||||||
**kwargs):
|
**kwargs):
|
||||||
"""
|
"""
|
||||||
|
@ -652,26 +680,24 @@ class NotifyEmail(NotifyBase):
|
||||||
# Strip target out of bcc list if in To
|
# Strip target out of bcc list if in To
|
||||||
bcc = (self.bcc - set([to_addr]))
|
bcc = (self.bcc - set([to_addr]))
|
||||||
|
|
||||||
try:
|
# Strip target out of reply_to list if in To
|
||||||
# Format our cc addresses to support the Name field
|
reply_to = (self.reply_to - set([to_addr]))
|
||||||
cc = [formataddr(
|
|
||||||
|
# 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]
|
||||||
|
|
||||||
|
if reply_to:
|
||||||
|
# Format our reply-to addresses to support the Name field
|
||||||
|
reply_to = [formataddr(
|
||||||
(self.names.get(addr, False), addr), charset='utf-8')
|
(self.names.get(addr, False), addr), charset='utf-8')
|
||||||
for addr in cc]
|
for addr in reply_to]
|
||||||
|
|
||||||
# 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( # pragma: no branch
|
|
||||||
(self.names.get(addr, False), addr)) for addr in cc]
|
|
||||||
|
|
||||||
# Format our bcc addresses to support the Name field
|
|
||||||
bcc = [formataddr( # pragma: no branch
|
|
||||||
(self.names.get(addr, False), addr)) for addr in bcc]
|
|
||||||
|
|
||||||
self.logger.debug(
|
self.logger.debug(
|
||||||
'Email From: {} <{}>'.format(from_name, self.from_addr))
|
'Email From: {} <{}>'.format(from_name, self.from_addr))
|
||||||
|
@ -680,45 +706,29 @@ class NotifyEmail(NotifyBase):
|
||||||
self.logger.debug('Email Cc: {}'.format(', '.join(cc)))
|
self.logger.debug('Email Cc: {}'.format(', '.join(cc)))
|
||||||
if bcc:
|
if bcc:
|
||||||
self.logger.debug('Email Bcc: {}'.format(', '.join(bcc)))
|
self.logger.debug('Email Bcc: {}'.format(', '.join(bcc)))
|
||||||
|
if reply_to:
|
||||||
|
self.logger.debug(
|
||||||
|
'Email Reply-To: {}'.format(', '.join(reply_to))
|
||||||
|
)
|
||||||
self.logger.debug('Login ID: {}'.format(self.user))
|
self.logger.debug('Login ID: {}'.format(self.user))
|
||||||
self.logger.debug(
|
self.logger.debug(
|
||||||
'Delivery: {}:{}'.format(self.smtp_host, self.port))
|
'Delivery: {}:{}'.format(self.smtp_host, self.port))
|
||||||
|
|
||||||
# Prepare Email Message
|
# Prepare Email Message
|
||||||
if self.notify_format == NotifyFormat.HTML:
|
if self.notify_format == NotifyFormat.HTML:
|
||||||
content = MIMEText(body, 'html', 'utf-8')
|
base = MIMEMultipart("alternative")
|
||||||
|
base.attach(MIMEText(
|
||||||
|
convert_between(
|
||||||
|
NotifyFormat.HTML, NotifyFormat.TEXT, body),
|
||||||
|
'plain', 'utf-8')
|
||||||
|
)
|
||||||
|
base.attach(MIMEText(body, 'html', 'utf-8'))
|
||||||
else:
|
else:
|
||||||
content = MIMEText(body, 'plain', 'utf-8')
|
base = MIMEText(body, 'plain', 'utf-8')
|
||||||
|
|
||||||
base = MIMEMultipart() if attach else content
|
|
||||||
|
|
||||||
# Apply any provided custom headers
|
|
||||||
for k, v in self.headers.items():
|
|
||||||
base[k] = Header(v, 'utf-8')
|
|
||||||
|
|
||||||
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")
|
|
||||||
base['X-Application'] = self.app_id
|
|
||||||
|
|
||||||
if attach:
|
if attach:
|
||||||
# First attach our body to our content as the first element
|
mixed = MIMEMultipart("mixed")
|
||||||
base.attach(content)
|
mixed.attach(base)
|
||||||
|
|
||||||
# Now store our attachments
|
# Now store our attachments
|
||||||
for attachment in attach:
|
for attachment in attach:
|
||||||
if not attachment:
|
if not attachment:
|
||||||
|
@ -745,8 +755,28 @@ class NotifyEmail(NotifyBase):
|
||||||
'attachment; filename="{}"'.format(
|
'attachment; filename="{}"'.format(
|
||||||
Header(attachment.name, 'utf-8')),
|
Header(attachment.name, 'utf-8')),
|
||||||
)
|
)
|
||||||
|
mixed.attach(app)
|
||||||
|
base = mixed
|
||||||
|
|
||||||
base.attach(app)
|
# Apply any provided custom headers
|
||||||
|
for k, v in self.headers.items():
|
||||||
|
base[k] = Header(v, self._get_charset(v))
|
||||||
|
|
||||||
|
base['Subject'] = Header(title, self._get_charset(title))
|
||||||
|
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')
|
||||||
|
base['Message-ID'] = make_msgid(domain=self.smtp_host)
|
||||||
|
base['Date'] = \
|
||||||
|
datetime.utcnow().strftime("%a, %d %b %Y %H:%M:%S +0000")
|
||||||
|
base['X-Application'] = self.app_id
|
||||||
|
|
||||||
|
if cc:
|
||||||
|
base['Cc'] = ','.join(cc)
|
||||||
|
|
||||||
|
if reply_to:
|
||||||
|
base['Reply-To'] = ','.join(reply_to)
|
||||||
|
|
||||||
# bind the socket variable to the current namespace
|
# bind the socket variable to the current namespace
|
||||||
socket = None
|
socket = None
|
||||||
|
@ -839,6 +869,14 @@ class NotifyEmail(NotifyBase):
|
||||||
'' if not e not in self.names
|
'' if not e not in self.names
|
||||||
else '{}:'.format(self.names[e]), e) for e in self.bcc])
|
else '{}:'.format(self.names[e]), e) for e in self.bcc])
|
||||||
|
|
||||||
|
if self.reply_to:
|
||||||
|
# Handle our Reply-To Addresses
|
||||||
|
params['reply'] = ','.join(
|
||||||
|
['{}{}'.format(
|
||||||
|
'' if not e not in self.names
|
||||||
|
else '{}:'.format(self.names[e]), e)
|
||||||
|
for e in self.reply_to])
|
||||||
|
|
||||||
# pull email suffix from username (if present)
|
# pull email suffix from username (if present)
|
||||||
user = None if not self.user else self.user.split('@')[0]
|
user = None if not self.user else self.user.split('@')[0]
|
||||||
|
|
||||||
|
@ -916,13 +954,6 @@ class NotifyEmail(NotifyBase):
|
||||||
# Extract from name to associate with from address
|
# Extract from name to associate with from address
|
||||||
results['from_name'] = NotifyEmail.unquote(results['qsd']['name'])
|
results['from_name'] = NotifyEmail.unquote(results['qsd']['name'])
|
||||||
|
|
||||||
if 'timeout' in results['qsd'] and len(results['qsd']['timeout']):
|
|
||||||
# Deprecated in favor of cto= flag
|
|
||||||
NotifyBase.logger.deprecate(
|
|
||||||
"timeout= argument is deprecated; use cto= instead.")
|
|
||||||
results['qsd']['cto'] = results['qsd']['timeout']
|
|
||||||
del results['qsd']['timeout']
|
|
||||||
|
|
||||||
# Store SMTP Host if specified
|
# Store SMTP Host if specified
|
||||||
if 'smtp' in results['qsd'] and len(results['qsd']['smtp']):
|
if 'smtp' in results['qsd'] and len(results['qsd']['smtp']):
|
||||||
# Extract the smtp server
|
# Extract the smtp server
|
||||||
|
@ -940,6 +971,10 @@ class NotifyEmail(NotifyBase):
|
||||||
if 'bcc' in results['qsd'] and len(results['qsd']['bcc']):
|
if 'bcc' in results['qsd'] and len(results['qsd']['bcc']):
|
||||||
results['bcc'] = results['qsd']['bcc']
|
results['bcc'] = results['qsd']['bcc']
|
||||||
|
|
||||||
|
# Handle Reply To Addresses
|
||||||
|
if 'reply' in results['qsd'] and len(results['qsd']['reply']):
|
||||||
|
results['reply_to'] = results['qsd']['reply']
|
||||||
|
|
||||||
results['from_addr'] = from_addr
|
results['from_addr'] = from_addr
|
||||||
results['smtp_host'] = smtp_host
|
results['smtp_host'] = smtp_host
|
||||||
|
|
||||||
|
|
|
@ -677,7 +677,7 @@ class NotifyEmby(NotifyBase):
|
||||||
|
|
||||||
def __del__(self):
|
def __del__(self):
|
||||||
"""
|
"""
|
||||||
Deconstructor
|
Destructor
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
self.logout()
|
self.logout()
|
||||||
|
@ -694,20 +694,20 @@ class NotifyEmby(NotifyBase):
|
||||||
# - https://bugs.python.org/issue29288
|
# - https://bugs.python.org/issue29288
|
||||||
#
|
#
|
||||||
# A ~similar~ issue can be identified here in the requests
|
# A ~similar~ issue can be identified here in the requests
|
||||||
# ticket system as unresolved and has provided work-arounds
|
# ticket system as unresolved and has provided workarounds
|
||||||
# - https://github.com/kennethreitz/requests/issues/3578
|
# - https://github.com/kennethreitz/requests/issues/3578
|
||||||
pass
|
pass
|
||||||
|
|
||||||
except ImportError: # pragma: no cover
|
except ImportError: # pragma: no cover
|
||||||
# The actual exception is `ModuleNotFoundError` however ImportError
|
# The actual exception is `ModuleNotFoundError` however ImportError
|
||||||
# grants us backwards compatiblity with versions of Python older
|
# grants us backwards compatibility with versions of Python older
|
||||||
# than v3.6
|
# than v3.6
|
||||||
|
|
||||||
# Python code that makes early calls to sys.exit() can cause
|
# Python code that makes early calls to sys.exit() can cause
|
||||||
# the __del__() code to run. However in some newer versions of
|
# the __del__() code to run. However, in some newer versions of
|
||||||
# Python, this causes the `sys` library to no longer be
|
# Python, this causes the `sys` library to no longer be
|
||||||
# available. The stack overflow also goes on to suggest that
|
# available. The stack overflow also goes on to suggest that
|
||||||
# it's not wise to use the __del__() as a deconstructor
|
# it's not wise to use the __del__() as a destructor
|
||||||
# which is the case here.
|
# which is the case here.
|
||||||
|
|
||||||
# https://stackoverflow.com/questions/67218341/\
|
# https://stackoverflow.com/questions/67218341/\
|
||||||
|
@ -719,6 +719,6 @@ class NotifyEmby(NotifyBase):
|
||||||
# /1481488/what-is-the-del-method-and-how-do-i-call-it
|
# /1481488/what-is-the-del-method-and-how-do-i-call-it
|
||||||
|
|
||||||
# At this time it seems clean to try to log out (if we can)
|
# At this time it seems clean to try to log out (if we can)
|
||||||
# but not throw any unessisary exceptions (like this one) to
|
# but not throw any unnecessary exceptions (like this one) to
|
||||||
# the end user if we don't have to.
|
# the end user if we don't have to.
|
||||||
pass
|
pass
|
||||||
|
|
|
@ -31,7 +31,6 @@
|
||||||
# - https://github.com/E2OpenPlugins/e2openplugin-OpenWebif/wiki/\
|
# - https://github.com/E2OpenPlugins/e2openplugin-OpenWebif/wiki/\
|
||||||
# OpenWebif-API-documentation#message
|
# OpenWebif-API-documentation#message
|
||||||
|
|
||||||
import six
|
|
||||||
import requests
|
import requests
|
||||||
from json import loads
|
from json import loads
|
||||||
|
|
||||||
|
@ -41,7 +40,7 @@ from ..common import NotifyType
|
||||||
from ..AppriseLocale import gettext_lazy as _
|
from ..AppriseLocale import gettext_lazy as _
|
||||||
|
|
||||||
|
|
||||||
class Enigma2MessageType(object):
|
class Enigma2MessageType:
|
||||||
# Defines the Enigma2 notification types Apprise can map to
|
# Defines the Enigma2 notification types Apprise can map to
|
||||||
INFO = 1
|
INFO = 1
|
||||||
WARNING = 2
|
WARNING = 2
|
||||||
|
@ -169,7 +168,7 @@ class NotifyEnigma2(NotifyBase):
|
||||||
self.timeout = self.template_args['timeout']['default']
|
self.timeout = self.template_args['timeout']['default']
|
||||||
|
|
||||||
self.fullpath = kwargs.get('fullpath')
|
self.fullpath = kwargs.get('fullpath')
|
||||||
if not isinstance(self.fullpath, six.string_types):
|
if not isinstance(self.fullpath, str):
|
||||||
self.fullpath = '/'
|
self.fullpath = '/'
|
||||||
|
|
||||||
self.headers = {}
|
self.headers = {}
|
||||||
|
@ -337,18 +336,10 @@ class NotifyEnigma2(NotifyBase):
|
||||||
return results
|
return results
|
||||||
|
|
||||||
# Add our headers that the user can potentially over-ride if they wish
|
# Add our headers that the user can potentially over-ride if they wish
|
||||||
# to to our returned result set
|
# to to our returned result set and tidy entries by unquoting them
|
||||||
results['headers'] = results['qsd+']
|
|
||||||
if results['qsd-']:
|
|
||||||
results['headers'].update(results['qsd-'])
|
|
||||||
NotifyBase.logger.deprecate(
|
|
||||||
"minus (-) based Enigma header tokens are being "
|
|
||||||
" removed; use the plus (+) symbol instead.")
|
|
||||||
|
|
||||||
# Tidy our header entries by unquoting them
|
|
||||||
results['headers'] = {
|
results['headers'] = {
|
||||||
NotifyEnigma2.unquote(x): NotifyEnigma2.unquote(y)
|
NotifyEnigma2.unquote(x): NotifyEnigma2.unquote(y)
|
||||||
for x, y in results['headers'].items()}
|
for x, y in results['qsd+'].items()}
|
||||||
|
|
||||||
# Save timeout value (if specified)
|
# Save timeout value (if specified)
|
||||||
if 'timeout' in results['qsd'] and len(results['qsd']['timeout']):
|
if 'timeout' in results['qsd'] and len(results['qsd']['timeout']):
|
||||||
|
|
|
@ -45,7 +45,6 @@
|
||||||
#
|
#
|
||||||
# If you Generate a new private key, it will provide a .json file
|
# If you Generate a new private key, it will provide a .json file
|
||||||
# You will need this in order to send an apprise messag
|
# You will need this in order to send an apprise messag
|
||||||
import six
|
|
||||||
import requests
|
import requests
|
||||||
from json import dumps
|
from json import dumps
|
||||||
from ..NotifyBase import NotifyBase
|
from ..NotifyBase import NotifyBase
|
||||||
|
@ -53,6 +52,7 @@ from ...common import NotifyType
|
||||||
from ...utils import validate_regex
|
from ...utils import validate_regex
|
||||||
from ...utils import parse_list
|
from ...utils import parse_list
|
||||||
from ...utils import parse_bool
|
from ...utils import parse_bool
|
||||||
|
from ...utils import dict_full_update
|
||||||
from ...common import NotifyImageSize
|
from ...common import NotifyImageSize
|
||||||
from ...AppriseAttachment import AppriseAttachment
|
from ...AppriseAttachment import AppriseAttachment
|
||||||
from ...AppriseLocale import gettext_lazy as _
|
from ...AppriseLocale import gettext_lazy as _
|
||||||
|
@ -73,7 +73,7 @@ except ImportError:
|
||||||
# cryptography is the dependency of the .oauth library
|
# cryptography is the dependency of the .oauth library
|
||||||
|
|
||||||
# Create a dummy object for init() call to work
|
# Create a dummy object for init() call to work
|
||||||
class GoogleOAuth(object):
|
class GoogleOAuth:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@ -227,7 +227,7 @@ class NotifyFCM(NotifyBase):
|
||||||
else:
|
else:
|
||||||
# Setup our mode
|
# Setup our mode
|
||||||
self.mode = NotifyFCM.template_tokens['mode']['default'] \
|
self.mode = NotifyFCM.template_tokens['mode']['default'] \
|
||||||
if not isinstance(mode, six.string_types) else mode.lower()
|
if not isinstance(mode, str) else mode.lower()
|
||||||
if self.mode and self.mode not in FCM_MODES:
|
if self.mode and self.mode not in FCM_MODES:
|
||||||
msg = 'The FCM mode specified ({}) is invalid.'.format(mode)
|
msg = 'The FCM mode specified ({}) is invalid.'.format(mode)
|
||||||
self.logger.warning(msg)
|
self.logger.warning(msg)
|
||||||
|
@ -450,17 +450,9 @@ class NotifyFCM(NotifyBase):
|
||||||
"FCM recipient %s parsed as a device token",
|
"FCM recipient %s parsed as a device token",
|
||||||
recipient)
|
recipient)
|
||||||
|
|
||||||
#
|
# A more advanced dict.update() that recursively includes
|
||||||
# Apply our priority configuration (if set)
|
# sub-dictionaries as well
|
||||||
#
|
dict_full_update(payload, self.priority.payload())
|
||||||
def merge(d1, d2):
|
|
||||||
for k in d2:
|
|
||||||
if k in d1 and isinstance(d1[k], dict) \
|
|
||||||
and isinstance(d2[k], dict):
|
|
||||||
merge(d1[k], d2[k])
|
|
||||||
else:
|
|
||||||
d1[k] = d2[k]
|
|
||||||
merge(payload, self.priority.payload())
|
|
||||||
|
|
||||||
self.logger.debug(
|
self.logger.debug(
|
||||||
'FCM %s POST URL: %s (cert_verify=%r)',
|
'FCM %s POST URL: %s (cert_verify=%r)',
|
||||||
|
|
|
@ -31,13 +31,12 @@
|
||||||
# https://firebase.google.com/docs/reference/fcm/rest/v1/\
|
# https://firebase.google.com/docs/reference/fcm/rest/v1/\
|
||||||
# projects.messages#androidnotification
|
# projects.messages#androidnotification
|
||||||
import re
|
import re
|
||||||
import six
|
|
||||||
from ...utils import parse_bool
|
from ...utils import parse_bool
|
||||||
from ...common import NotifyType
|
from ...common import NotifyType
|
||||||
from ...AppriseAsset import AppriseAsset
|
from ...AppriseAsset import AppriseAsset
|
||||||
|
|
||||||
|
|
||||||
class FCMColorManager(object):
|
class FCMColorManager:
|
||||||
"""
|
"""
|
||||||
A Simple object to accept either a boolean value
|
A Simple object to accept either a boolean value
|
||||||
- True: Use colors provided by Apprise
|
- True: Use colors provided by Apprise
|
||||||
|
@ -63,7 +62,7 @@ class FCMColorManager(object):
|
||||||
|
|
||||||
# Prepare our color
|
# Prepare our color
|
||||||
self.color = color
|
self.color = color
|
||||||
if isinstance(color, six.string_types):
|
if isinstance(color, str):
|
||||||
self.color = self.__color_rgb.match(color)
|
self.color = self.__color_rgb.match(color)
|
||||||
if self.color:
|
if self.color:
|
||||||
# Store our RGB value as #rrggbb
|
# Store our RGB value as #rrggbb
|
||||||
|
@ -112,16 +111,8 @@ class FCMColorManager(object):
|
||||||
|
|
||||||
def __bool__(self):
|
def __bool__(self):
|
||||||
"""
|
"""
|
||||||
Allows this object to be wrapped in an Python 3.x based 'if
|
Allows this object to be wrapped in an 'if statement'.
|
||||||
statement'. True is returned if a color was loaded
|
True is returned if a color was loaded
|
||||||
"""
|
"""
|
||||||
return True if self.color is True or \
|
return True if self.color is True or \
|
||||||
isinstance(self.color, six.string_types) else False
|
isinstance(self.color, str) else False
|
||||||
|
|
||||||
def __nonzero__(self):
|
|
||||||
"""
|
|
||||||
Allows this object to be wrapped in an Python 2.x based 'if
|
|
||||||
statement'. True is returned if a color was loaded
|
|
||||||
"""
|
|
||||||
return True if self.color is True or \
|
|
||||||
isinstance(self.color, six.string_types) else False
|
|
||||||
|
|
|
@ -22,7 +22,7 @@
|
||||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
# 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
|
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
# THE SOFTWARE.
|
# THE SOFTWARE.
|
||||||
class FCMMode(object):
|
class FCMMode:
|
||||||
"""
|
"""
|
||||||
Define the Firebase Cloud Messaging Modes
|
Define the Firebase Cloud Messaging Modes
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -29,7 +29,6 @@
|
||||||
# 2. Click Generate New Private Key, then confirm by clicking Generate Key.
|
# 2. Click Generate New Private Key, then confirm by clicking Generate Key.
|
||||||
# 3. Securely store the JSON file containing the key.
|
# 3. Securely store the JSON file containing the key.
|
||||||
|
|
||||||
import io
|
|
||||||
import requests
|
import requests
|
||||||
import base64
|
import base64
|
||||||
import json
|
import json
|
||||||
|
@ -41,26 +40,13 @@ from cryptography.hazmat.primitives import asymmetric
|
||||||
from cryptography.exceptions import UnsupportedAlgorithm
|
from cryptography.exceptions import UnsupportedAlgorithm
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
|
from json.decoder import JSONDecodeError
|
||||||
|
from urllib.parse import urlencode as _urlencode
|
||||||
|
|
||||||
from ...logger import logger
|
from ...logger import logger
|
||||||
|
|
||||||
try:
|
|
||||||
# Python 2.7
|
|
||||||
from urllib import urlencode as _urlencode
|
|
||||||
|
|
||||||
except ImportError:
|
class GoogleOAuth:
|
||||||
# Python 3.x
|
|
||||||
from urllib.parse import urlencode as _urlencode
|
|
||||||
|
|
||||||
try:
|
|
||||||
# Python 3.x
|
|
||||||
from json.decoder import JSONDecodeError
|
|
||||||
|
|
||||||
except ImportError:
|
|
||||||
# Python v2.7 Backwards Compatibility support
|
|
||||||
JSONDecodeError = ValueError
|
|
||||||
|
|
||||||
|
|
||||||
class GoogleOAuth(object):
|
|
||||||
"""
|
"""
|
||||||
A OAuth simplified implimentation to Google's Firebase Cloud Messaging
|
A OAuth simplified implimentation to Google's Firebase Cloud Messaging
|
||||||
|
|
||||||
|
@ -127,7 +113,7 @@ class GoogleOAuth(object):
|
||||||
self.__access_token_expiry = datetime.utcnow()
|
self.__access_token_expiry = datetime.utcnow()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
with io.open(path, mode="r", encoding=self.encoding) as fp:
|
with open(path, mode="r", encoding=self.encoding) as fp:
|
||||||
self.content = json.loads(fp.read())
|
self.content = json.loads(fp.read())
|
||||||
|
|
||||||
except (OSError, IOError):
|
except (OSError, IOError):
|
||||||
|
|
|
@ -33,7 +33,7 @@ from .common import (FCMMode, FCM_MODES)
|
||||||
from ...logger import logger
|
from ...logger import logger
|
||||||
|
|
||||||
|
|
||||||
class NotificationPriority(object):
|
class NotificationPriority:
|
||||||
"""
|
"""
|
||||||
Defines the Notification Priorities as described on:
|
Defines the Notification Priorities as described on:
|
||||||
https://firebase.google.com/docs/reference/fcm/rest/v1/\
|
https://firebase.google.com/docs/reference/fcm/rest/v1/\
|
||||||
|
@ -63,7 +63,7 @@ class NotificationPriority(object):
|
||||||
HIGH = 'HIGH'
|
HIGH = 'HIGH'
|
||||||
|
|
||||||
|
|
||||||
class FCMPriority(object):
|
class FCMPriority:
|
||||||
"""
|
"""
|
||||||
Defines our accepted priorites
|
Defines our accepted priorites
|
||||||
"""
|
"""
|
||||||
|
@ -87,7 +87,7 @@ FCM_PRIORITIES = (
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class FCMPriorityManager(object):
|
class FCMPriorityManager:
|
||||||
"""
|
"""
|
||||||
A Simple object to make it easier to work with FCM set priorities
|
A Simple object to make it easier to work with FCM set priorities
|
||||||
"""
|
"""
|
||||||
|
@ -242,14 +242,7 @@ class FCMPriorityManager(object):
|
||||||
|
|
||||||
def __bool__(self):
|
def __bool__(self):
|
||||||
"""
|
"""
|
||||||
Allows this object to be wrapped in an Python 3.x based 'if
|
Allows this object to be wrapped in an 'if statement'.
|
||||||
statement'. True is returned if a priority was loaded
|
True is returned if a priority was loaded
|
||||||
"""
|
|
||||||
return True if self.priority else False
|
|
||||||
|
|
||||||
def __nonzero__(self):
|
|
||||||
"""
|
|
||||||
Allows this object to be wrapped in an Python 2.x based 'if
|
|
||||||
statement'. True is returned if a priority was loaded
|
|
||||||
"""
|
"""
|
||||||
return True if self.priority else False
|
return True if self.priority else False
|
||||||
|
|
|
@ -23,7 +23,6 @@
|
||||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
# THE SOFTWARE.
|
# THE SOFTWARE.
|
||||||
|
|
||||||
import six
|
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
from .NotifyBase import NotifyBase
|
from .NotifyBase import NotifyBase
|
||||||
|
@ -137,11 +136,11 @@ class NotifyForm(NotifyBase):
|
||||||
super(NotifyForm, self).__init__(**kwargs)
|
super(NotifyForm, self).__init__(**kwargs)
|
||||||
|
|
||||||
self.fullpath = kwargs.get('fullpath')
|
self.fullpath = kwargs.get('fullpath')
|
||||||
if not isinstance(self.fullpath, six.string_types):
|
if not isinstance(self.fullpath, str):
|
||||||
self.fullpath = ''
|
self.fullpath = ''
|
||||||
|
|
||||||
self.method = self.template_args['method']['default'] \
|
self.method = self.template_args['method']['default'] \
|
||||||
if not isinstance(method, six.string_types) else method.upper()
|
if not isinstance(method, str) else method.upper()
|
||||||
|
|
||||||
if self.method not in METHODS:
|
if self.method not in METHODS:
|
||||||
msg = 'The method specified ({}) is invalid.'.format(method)
|
msg = 'The method specified ({}) is invalid.'.format(method)
|
||||||
|
@ -374,17 +373,9 @@ class NotifyForm(NotifyBase):
|
||||||
for x, y in results['qsd:'].items()}
|
for x, y in results['qsd:'].items()}
|
||||||
|
|
||||||
# Add our headers that the user can potentially over-ride if they wish
|
# Add our headers that the user can potentially over-ride if they wish
|
||||||
# to to our returned result set
|
# to to our returned result set and tidy entries by unquoting them
|
||||||
results['headers'] = results['qsd+']
|
|
||||||
if results['qsd-']:
|
|
||||||
results['headers'].update(results['qsd-'])
|
|
||||||
NotifyBase.logger.deprecate(
|
|
||||||
"minus (-) based Form header tokens are being "
|
|
||||||
" removed; use the plus (+) symbol instead.")
|
|
||||||
|
|
||||||
# Tidy our header entries by unquoting them
|
|
||||||
results['headers'] = {NotifyForm.unquote(x): NotifyForm.unquote(y)
|
results['headers'] = {NotifyForm.unquote(x): NotifyForm.unquote(y)
|
||||||
for x, y in results['headers'].items()}
|
for x, y in results['qsd+'].items()}
|
||||||
|
|
||||||
# Set method if not otherwise set
|
# Set method if not otherwise set
|
||||||
if 'method' in results['qsd'] and len(results['qsd']['method']):
|
if 'method' in results['qsd'] and len(results['qsd']['method']):
|
||||||
|
|
|
@ -60,17 +60,36 @@ except (ImportError, ValueError, AttributeError):
|
||||||
|
|
||||||
|
|
||||||
# Urgencies
|
# Urgencies
|
||||||
class GnomeUrgency(object):
|
class GnomeUrgency:
|
||||||
LOW = 0
|
LOW = 0
|
||||||
NORMAL = 1
|
NORMAL = 1
|
||||||
HIGH = 2
|
HIGH = 2
|
||||||
|
|
||||||
|
|
||||||
GNOME_URGENCIES = (
|
GNOME_URGENCIES = {
|
||||||
GnomeUrgency.LOW,
|
GnomeUrgency.LOW: 'low',
|
||||||
GnomeUrgency.NORMAL,
|
GnomeUrgency.NORMAL: 'normal',
|
||||||
GnomeUrgency.HIGH,
|
GnomeUrgency.HIGH: 'high',
|
||||||
)
|
}
|
||||||
|
|
||||||
|
|
||||||
|
GNOME_URGENCY_MAP = {
|
||||||
|
# Maps against string 'low'
|
||||||
|
'l': GnomeUrgency.LOW,
|
||||||
|
# Maps against string 'moderate'
|
||||||
|
'm': GnomeUrgency.LOW,
|
||||||
|
# Maps against string 'normal'
|
||||||
|
'n': GnomeUrgency.NORMAL,
|
||||||
|
# Maps against string 'high'
|
||||||
|
'h': GnomeUrgency.HIGH,
|
||||||
|
# Maps against string 'emergency'
|
||||||
|
'e': GnomeUrgency.HIGH,
|
||||||
|
|
||||||
|
# Entries to additionally support (so more like Gnome's API)
|
||||||
|
'0': GnomeUrgency.LOW,
|
||||||
|
'1': GnomeUrgency.NORMAL,
|
||||||
|
'2': GnomeUrgency.HIGH,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class NotifyGnome(NotifyBase):
|
class NotifyGnome(NotifyBase):
|
||||||
|
@ -126,6 +145,12 @@ class NotifyGnome(NotifyBase):
|
||||||
'values': GNOME_URGENCIES,
|
'values': GNOME_URGENCIES,
|
||||||
'default': GnomeUrgency.NORMAL,
|
'default': GnomeUrgency.NORMAL,
|
||||||
},
|
},
|
||||||
|
'priority': {
|
||||||
|
# Apprise uses 'priority' everywhere; it's just a nice consistent
|
||||||
|
# feel to be able to use it here as well. Just map the
|
||||||
|
# value back to 'priority'
|
||||||
|
'alias_of': 'urgency',
|
||||||
|
},
|
||||||
'image': {
|
'image': {
|
||||||
'name': _('Include Image'),
|
'name': _('Include Image'),
|
||||||
'type': 'bool',
|
'type': 'bool',
|
||||||
|
@ -142,11 +167,13 @@ class NotifyGnome(NotifyBase):
|
||||||
super(NotifyGnome, self).__init__(**kwargs)
|
super(NotifyGnome, self).__init__(**kwargs)
|
||||||
|
|
||||||
# The urgency of the message
|
# The urgency of the message
|
||||||
if urgency not in GNOME_URGENCIES:
|
self.urgency = int(
|
||||||
self.urgency = self.template_args['urgency']['default']
|
NotifyGnome.template_args['urgency']['default']
|
||||||
|
if urgency is None else
|
||||||
else:
|
next((
|
||||||
self.urgency = urgency
|
v for k, v in GNOME_URGENCY_MAP.items()
|
||||||
|
if str(urgency).lower().startswith(k)),
|
||||||
|
NotifyGnome.template_args['urgency']['default']))
|
||||||
|
|
||||||
# Track whether or not we want to send an image with our notification
|
# Track whether or not we want to send an image with our notification
|
||||||
# or not.
|
# or not.
|
||||||
|
@ -205,17 +232,13 @@ class NotifyGnome(NotifyBase):
|
||||||
Returns the URL built dynamically based on specified arguments.
|
Returns the URL built dynamically based on specified arguments.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
_map = {
|
|
||||||
GnomeUrgency.LOW: 'low',
|
|
||||||
GnomeUrgency.NORMAL: 'normal',
|
|
||||||
GnomeUrgency.HIGH: 'high',
|
|
||||||
}
|
|
||||||
|
|
||||||
# Define any URL parameters
|
# Define any URL parameters
|
||||||
params = {
|
params = {
|
||||||
'image': 'yes' if self.include_image else 'no',
|
'image': 'yes' if self.include_image else 'no',
|
||||||
'urgency': 'normal' if self.urgency not in _map
|
'urgency':
|
||||||
else _map[self.urgency],
|
GNOME_URGENCIES[self.template_args['urgency']['default']]
|
||||||
|
if self.urgency not in GNOME_URGENCIES
|
||||||
|
else GNOME_URGENCIES[self.urgency],
|
||||||
}
|
}
|
||||||
|
|
||||||
# Extend our parameters
|
# Extend our parameters
|
||||||
|
@ -243,23 +266,13 @@ class NotifyGnome(NotifyBase):
|
||||||
|
|
||||||
# Gnome supports urgency, but we we also support the keyword priority
|
# Gnome supports urgency, but we we also support the keyword priority
|
||||||
# so that it is consistent with some of the other plugins
|
# so that it is consistent with some of the other plugins
|
||||||
urgency = results['qsd'].get('urgency', results['qsd'].get('priority'))
|
if 'priority' in results['qsd'] and len(results['qsd']['priority']):
|
||||||
if urgency and len(urgency):
|
# We intentionally store the priority in the urgency section
|
||||||
_map = {
|
results['urgency'] = \
|
||||||
'0': GnomeUrgency.LOW,
|
NotifyGnome.unquote(results['qsd']['priority'])
|
||||||
'l': GnomeUrgency.LOW,
|
|
||||||
'n': GnomeUrgency.NORMAL,
|
|
||||||
'1': GnomeUrgency.NORMAL,
|
|
||||||
'h': GnomeUrgency.HIGH,
|
|
||||||
'2': GnomeUrgency.HIGH,
|
|
||||||
}
|
|
||||||
|
|
||||||
try:
|
if 'urgency' in results['qsd'] and len(results['qsd']['urgency']):
|
||||||
# Attempt to index/retrieve our urgency
|
results['urgency'] = \
|
||||||
results['urgency'] = _map[urgency[0].lower()]
|
NotifyGnome.unquote(results['qsd']['urgency'])
|
||||||
|
|
||||||
except KeyError:
|
|
||||||
# No priority was set
|
|
||||||
pass
|
|
||||||
|
|
||||||
return results
|
return results
|
||||||
|
|
|
@ -41,7 +41,7 @@ from ..AppriseLocale import gettext_lazy as _
|
||||||
|
|
||||||
|
|
||||||
# Priorities
|
# Priorities
|
||||||
class GotifyPriority(object):
|
class GotifyPriority:
|
||||||
LOW = 0
|
LOW = 0
|
||||||
MODERATE = 3
|
MODERATE = 3
|
||||||
NORMAL = 5
|
NORMAL = 5
|
||||||
|
@ -49,13 +49,37 @@ class GotifyPriority(object):
|
||||||
EMERGENCY = 10
|
EMERGENCY = 10
|
||||||
|
|
||||||
|
|
||||||
GOTIFY_PRIORITIES = (
|
GOTIFY_PRIORITIES = {
|
||||||
GotifyPriority.LOW,
|
# Note: This also acts as a reverse lookup mapping
|
||||||
GotifyPriority.MODERATE,
|
GotifyPriority.LOW: 'low',
|
||||||
GotifyPriority.NORMAL,
|
GotifyPriority.MODERATE: 'moderate',
|
||||||
GotifyPriority.HIGH,
|
GotifyPriority.NORMAL: 'normal',
|
||||||
GotifyPriority.EMERGENCY,
|
GotifyPriority.HIGH: 'high',
|
||||||
)
|
GotifyPriority.EMERGENCY: 'emergency',
|
||||||
|
}
|
||||||
|
|
||||||
|
GOTIFY_PRIORITY_MAP = {
|
||||||
|
# Maps against string 'low'
|
||||||
|
'l': GotifyPriority.LOW,
|
||||||
|
# Maps against string 'moderate'
|
||||||
|
'm': GotifyPriority.MODERATE,
|
||||||
|
# Maps against string 'normal'
|
||||||
|
'n': GotifyPriority.NORMAL,
|
||||||
|
# Maps against string 'high'
|
||||||
|
'h': GotifyPriority.HIGH,
|
||||||
|
# Maps against string 'emergency'
|
||||||
|
'e': GotifyPriority.EMERGENCY,
|
||||||
|
|
||||||
|
# Entries to additionally support (so more like Gotify's API)
|
||||||
|
'10': GotifyPriority.EMERGENCY,
|
||||||
|
# ^ 10 needs to be checked before '1' below or it will match the wrong
|
||||||
|
# priority
|
||||||
|
'0': GotifyPriority.LOW, '1': GotifyPriority.LOW, '2': GotifyPriority.LOW,
|
||||||
|
'3': GotifyPriority.MODERATE, '4': GotifyPriority.MODERATE,
|
||||||
|
'5': GotifyPriority.NORMAL, '6': GotifyPriority.NORMAL,
|
||||||
|
'7': GotifyPriority.NORMAL,
|
||||||
|
'8': GotifyPriority.HIGH, '9': GotifyPriority.HIGH,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class NotifyGotify(NotifyBase):
|
class NotifyGotify(NotifyBase):
|
||||||
|
@ -144,11 +168,14 @@ class NotifyGotify(NotifyBase):
|
||||||
# prepare our fullpath
|
# prepare our fullpath
|
||||||
self.fullpath = kwargs.get('fullpath', '/')
|
self.fullpath = kwargs.get('fullpath', '/')
|
||||||
|
|
||||||
if priority not in GOTIFY_PRIORITIES:
|
# The Priority of the message
|
||||||
self.priority = GotifyPriority.NORMAL
|
self.priority = int(
|
||||||
|
NotifyGotify.template_args['priority']['default']
|
||||||
else:
|
if priority is None else
|
||||||
self.priority = priority
|
next((
|
||||||
|
v for k, v in GOTIFY_PRIORITY_MAP.items()
|
||||||
|
if str(priority).lower().startswith(k)),
|
||||||
|
NotifyGotify.template_args['priority']['default']))
|
||||||
|
|
||||||
if self.secure:
|
if self.secure:
|
||||||
self.schema = 'https'
|
self.schema = 'https'
|
||||||
|
@ -246,7 +273,10 @@ class NotifyGotify(NotifyBase):
|
||||||
|
|
||||||
# Define any URL parameters
|
# Define any URL parameters
|
||||||
params = {
|
params = {
|
||||||
'priority': self.priority,
|
'priority':
|
||||||
|
GOTIFY_PRIORITIES[self.template_args['priority']['default']]
|
||||||
|
if self.priority not in GOTIFY_PRIORITIES
|
||||||
|
else GOTIFY_PRIORITIES[self.priority],
|
||||||
}
|
}
|
||||||
|
|
||||||
# Extend our parameters
|
# Extend our parameters
|
||||||
|
@ -294,20 +324,9 @@ class NotifyGotify(NotifyBase):
|
||||||
results['fullpath'] = \
|
results['fullpath'] = \
|
||||||
'/' if not entries else '/{}/'.format('/'.join(entries))
|
'/' if not entries else '/{}/'.format('/'.join(entries))
|
||||||
|
|
||||||
|
# Set our priority
|
||||||
if 'priority' in results['qsd'] and len(results['qsd']['priority']):
|
if 'priority' in results['qsd'] and len(results['qsd']['priority']):
|
||||||
_map = {
|
results['priority'] = \
|
||||||
'l': GotifyPriority.LOW,
|
NotifyGotify.unquote(results['qsd']['priority'])
|
||||||
'm': GotifyPriority.MODERATE,
|
|
||||||
'n': GotifyPriority.NORMAL,
|
|
||||||
'h': GotifyPriority.HIGH,
|
|
||||||
'e': GotifyPriority.EMERGENCY,
|
|
||||||
}
|
|
||||||
try:
|
|
||||||
results['priority'] = \
|
|
||||||
_map[results['qsd']['priority'][0].lower()]
|
|
||||||
|
|
||||||
except KeyError:
|
|
||||||
# No priority was set
|
|
||||||
pass
|
|
||||||
|
|
||||||
return results
|
return results
|
||||||
|
|
|
@ -46,7 +46,7 @@ except ImportError:
|
||||||
|
|
||||||
|
|
||||||
# Priorities
|
# Priorities
|
||||||
class GrowlPriority(object):
|
class GrowlPriority:
|
||||||
LOW = -2
|
LOW = -2
|
||||||
MODERATE = -1
|
MODERATE = -1
|
||||||
NORMAL = 0
|
NORMAL = 0
|
||||||
|
@ -54,13 +54,34 @@ class GrowlPriority(object):
|
||||||
EMERGENCY = 2
|
EMERGENCY = 2
|
||||||
|
|
||||||
|
|
||||||
GROWL_PRIORITIES = (
|
GROWL_PRIORITIES = {
|
||||||
GrowlPriority.LOW,
|
# Note: This also acts as a reverse lookup mapping
|
||||||
GrowlPriority.MODERATE,
|
GrowlPriority.LOW: 'low',
|
||||||
GrowlPriority.NORMAL,
|
GrowlPriority.MODERATE: 'moderate',
|
||||||
GrowlPriority.HIGH,
|
GrowlPriority.NORMAL: 'normal',
|
||||||
GrowlPriority.EMERGENCY,
|
GrowlPriority.HIGH: 'high',
|
||||||
)
|
GrowlPriority.EMERGENCY: 'emergency',
|
||||||
|
}
|
||||||
|
|
||||||
|
GROWL_PRIORITY_MAP = {
|
||||||
|
# Maps against string 'low'
|
||||||
|
'l': GrowlPriority.LOW,
|
||||||
|
# Maps against string 'moderate'
|
||||||
|
'm': GrowlPriority.MODERATE,
|
||||||
|
# Maps against string 'normal'
|
||||||
|
'n': GrowlPriority.NORMAL,
|
||||||
|
# Maps against string 'high'
|
||||||
|
'h': GrowlPriority.HIGH,
|
||||||
|
# Maps against string 'emergency'
|
||||||
|
'e': GrowlPriority.EMERGENCY,
|
||||||
|
|
||||||
|
# Entries to additionally support (so more like Growl's API)
|
||||||
|
'-2': GrowlPriority.LOW,
|
||||||
|
'-1': GrowlPriority.MODERATE,
|
||||||
|
'0': GrowlPriority.NORMAL,
|
||||||
|
'1': GrowlPriority.HIGH,
|
||||||
|
'2': GrowlPriority.EMERGENCY,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class NotifyGrowl(NotifyBase):
|
class NotifyGrowl(NotifyBase):
|
||||||
|
@ -172,11 +193,12 @@ class NotifyGrowl(NotifyBase):
|
||||||
self.port = self.default_port
|
self.port = self.default_port
|
||||||
|
|
||||||
# The Priority of the message
|
# The Priority of the message
|
||||||
if priority not in GROWL_PRIORITIES:
|
self.priority = NotifyGrowl.template_args['priority']['default'] \
|
||||||
self.priority = GrowlPriority.NORMAL
|
if not priority else \
|
||||||
|
next((
|
||||||
else:
|
v for k, v in GROWL_PRIORITY_MAP.items()
|
||||||
self.priority = priority
|
if str(priority).lower().startswith(k)),
|
||||||
|
NotifyGrowl.template_args['priority']['default'])
|
||||||
|
|
||||||
# Our Registered object
|
# Our Registered object
|
||||||
self.growl = None
|
self.growl = None
|
||||||
|
@ -318,21 +340,14 @@ class NotifyGrowl(NotifyBase):
|
||||||
Returns the URL built dynamically based on specified arguments.
|
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 URL parameters
|
# Define any URL parameters
|
||||||
params = {
|
params = {
|
||||||
'image': 'yes' if self.include_image else 'no',
|
'image': 'yes' if self.include_image else 'no',
|
||||||
'sticky': 'yes' if self.sticky else 'no',
|
'sticky': 'yes' if self.sticky else 'no',
|
||||||
'priority':
|
'priority':
|
||||||
_map[GrowlPriority.NORMAL] if self.priority not in _map
|
GROWL_PRIORITIES[self.template_args['priority']['default']]
|
||||||
else _map[self.priority],
|
if self.priority not in GROWL_PRIORITIES
|
||||||
|
else GROWL_PRIORITIES[self.priority],
|
||||||
'version': self.version,
|
'version': self.version,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -384,33 +399,10 @@ class NotifyGrowl(NotifyBase):
|
||||||
)
|
)
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
# Set our priority
|
||||||
if 'priority' in results['qsd'] and len(results['qsd']['priority']):
|
if 'priority' in results['qsd'] and len(results['qsd']['priority']):
|
||||||
_map = {
|
results['priority'] = \
|
||||||
# Letter Assignments
|
NotifyGrowl.unquote(results['qsd']['priority'])
|
||||||
'l': GrowlPriority.LOW,
|
|
||||||
'm': GrowlPriority.MODERATE,
|
|
||||||
'n': GrowlPriority.NORMAL,
|
|
||||||
'h': GrowlPriority.HIGH,
|
|
||||||
'e': GrowlPriority.EMERGENCY,
|
|
||||||
'lo': GrowlPriority.LOW,
|
|
||||||
'me': GrowlPriority.MODERATE,
|
|
||||||
'no': GrowlPriority.NORMAL,
|
|
||||||
'hi': GrowlPriority.HIGH,
|
|
||||||
'em': GrowlPriority.EMERGENCY,
|
|
||||||
# Support 3rd Party Documented Scale
|
|
||||||
'-2': GrowlPriority.LOW,
|
|
||||||
'-1': GrowlPriority.MODERATE,
|
|
||||||
'0': GrowlPriority.NORMAL,
|
|
||||||
'1': GrowlPriority.HIGH,
|
|
||||||
'2': GrowlPriority.EMERGENCY,
|
|
||||||
}
|
|
||||||
try:
|
|
||||||
results['priority'] = \
|
|
||||||
_map[results['qsd']['priority'][0:2].lower()]
|
|
||||||
|
|
||||||
except KeyError:
|
|
||||||
# No priority was set
|
|
||||||
pass
|
|
||||||
|
|
||||||
# Because of the URL formatting, the password is actually where the
|
# Because of the URL formatting, the password is actually where the
|
||||||
# username field is. For this reason, we just preform this small hack
|
# username field is. For this reason, we just preform this small hack
|
||||||
|
|
91
libs/apprise/plugins/NotifyGuilded.py
Normal file
91
libs/apprise/plugins/NotifyGuilded.py
Normal file
|
@ -0,0 +1,91 @@
|
||||||
|
# -*- 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.
|
||||||
|
|
||||||
|
# For this to work correctly you need to create a webhook. To do this just
|
||||||
|
# click on the little gear icon next to the channel you're part of. From
|
||||||
|
# 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://media.guilded.gg/webhooks/417429632418316298/\
|
||||||
|
# JHZ7lQml277CDHmQKMHI8qBe7bk2ZwO5UKjCiOAF7711o33MyqU344Qpgv7YTpadV_js
|
||||||
|
#
|
||||||
|
# Simplified, it looks like this:
|
||||||
|
# https://media.guilded.gg/webhooks/WEBHOOK_ID/WEBHOOK_TOKEN
|
||||||
|
#
|
||||||
|
# This plugin will simply work using the url of:
|
||||||
|
# guilded://WEBHOOK_ID/WEBHOOK_TOKEN
|
||||||
|
#
|
||||||
|
# API Documentation on Webhooks:
|
||||||
|
# - https://discord.com/developers/docs/resources/webhook
|
||||||
|
#
|
||||||
|
|
||||||
|
import re
|
||||||
|
from .NotifyDiscord import NotifyDiscord
|
||||||
|
|
||||||
|
|
||||||
|
class NotifyGuilded(NotifyDiscord):
|
||||||
|
"""
|
||||||
|
A wrapper to Guilded Notifications
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
# The default descriptive name associated with the Notification
|
||||||
|
service_name = 'Guilded'
|
||||||
|
|
||||||
|
# The services URL
|
||||||
|
service_url = 'https://guilded.gg/'
|
||||||
|
|
||||||
|
# A URL that takes you to the setup/help of the specific protocol
|
||||||
|
setup_url = 'https://github.com/caronc/apprise/wiki/Notify_guilded'
|
||||||
|
|
||||||
|
# The default secure protocol
|
||||||
|
secure_protocol = 'guilded'
|
||||||
|
|
||||||
|
# Guilded Webhook
|
||||||
|
notify_url = 'https://media.guilded.gg/webhooks'
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def parse_native_url(url):
|
||||||
|
"""
|
||||||
|
Support https://media.guilded.gg/webhooks/WEBHOOK_ID/WEBHOOK_TOKEN
|
||||||
|
"""
|
||||||
|
|
||||||
|
result = re.match(
|
||||||
|
r'^https?://(media\.)?guilded\.gg/webhooks/'
|
||||||
|
# a UUID, but we do we really need to be _that_ picky?
|
||||||
|
r'(?P<webhook_id>[-0-9a-f]+)/'
|
||||||
|
r'(?P<webhook_token>[A-Z0-9_-]+)/?'
|
||||||
|
r'(?P<params>\?.+)?$', url, re.I)
|
||||||
|
|
||||||
|
if result:
|
||||||
|
return NotifyGuilded.parse_url(
|
||||||
|
'{schema}://{webhook_id}/{webhook_token}/{params}'.format(
|
||||||
|
schema=NotifyGuilded.secure_protocol,
|
||||||
|
webhook_id=result.group('webhook_id'),
|
||||||
|
webhook_token=result.group('webhook_token'),
|
||||||
|
params='' if not result.group('params')
|
||||||
|
else result.group('params')))
|
||||||
|
|
||||||
|
return None
|
|
@ -107,7 +107,7 @@ class NotifyHomeAssistant(NotifyBase):
|
||||||
# Optional Unique Notification ID
|
# Optional Unique Notification ID
|
||||||
'name': _('Notification ID'),
|
'name': _('Notification ID'),
|
||||||
'type': 'string',
|
'type': 'string',
|
||||||
'regex': (r'^[a-f0-9_-]+$', 'i'),
|
'regex': (r'^[a-z0-9_-]+$', 'i'),
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -23,7 +23,6 @@
|
||||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
# THE SOFTWARE.
|
# THE SOFTWARE.
|
||||||
|
|
||||||
import six
|
|
||||||
import requests
|
import requests
|
||||||
import base64
|
import base64
|
||||||
from json import dumps
|
from json import dumps
|
||||||
|
@ -139,11 +138,11 @@ class NotifyJSON(NotifyBase):
|
||||||
super(NotifyJSON, self).__init__(**kwargs)
|
super(NotifyJSON, self).__init__(**kwargs)
|
||||||
|
|
||||||
self.fullpath = kwargs.get('fullpath')
|
self.fullpath = kwargs.get('fullpath')
|
||||||
if not isinstance(self.fullpath, six.string_types):
|
if not isinstance(self.fullpath, str):
|
||||||
self.fullpath = ''
|
self.fullpath = ''
|
||||||
|
|
||||||
self.method = self.template_args['method']['default'] \
|
self.method = self.template_args['method']['default'] \
|
||||||
if not isinstance(method, six.string_types) else method.upper()
|
if not isinstance(method, str) else method.upper()
|
||||||
|
|
||||||
if self.method not in METHODS:
|
if self.method not in METHODS:
|
||||||
msg = 'The method specified ({}) is invalid.'.format(method)
|
msg = 'The method specified ({}) is invalid.'.format(method)
|
||||||
|
@ -361,17 +360,9 @@ class NotifyJSON(NotifyBase):
|
||||||
for x, y in results['qsd:'].items()}
|
for x, y in results['qsd:'].items()}
|
||||||
|
|
||||||
# Add our headers that the user can potentially over-ride if they wish
|
# Add our headers that the user can potentially over-ride if they wish
|
||||||
# to to our returned result set
|
# to to our returned result set and tidy entries by unquoting them
|
||||||
results['headers'] = results['qsd+']
|
|
||||||
if results['qsd-']:
|
|
||||||
results['headers'].update(results['qsd-'])
|
|
||||||
NotifyBase.logger.deprecate(
|
|
||||||
"minus (-) based JSON header tokens are being "
|
|
||||||
" removed; use the plus (+) symbol instead.")
|
|
||||||
|
|
||||||
# Tidy our header entries by unquoting them
|
|
||||||
results['headers'] = {NotifyJSON.unquote(x): NotifyJSON.unquote(y)
|
results['headers'] = {NotifyJSON.unquote(x): NotifyJSON.unquote(y)
|
||||||
for x, y in results['headers'].items()}
|
for x, y in results['qsd+'].items()}
|
||||||
|
|
||||||
# Set method if not otherwise set
|
# Set method if not otherwise set
|
||||||
if 'method' in results['qsd'] and len(results['qsd']['method']):
|
if 'method' in results['qsd'] and len(results['qsd']['method']):
|
||||||
|
|
|
@ -63,7 +63,7 @@ JOIN_IMAGE_XY = NotifyImageSize.XY_72
|
||||||
|
|
||||||
|
|
||||||
# Priorities
|
# Priorities
|
||||||
class JoinPriority(object):
|
class JoinPriority:
|
||||||
LOW = -2
|
LOW = -2
|
||||||
MODERATE = -1
|
MODERATE = -1
|
||||||
NORMAL = 0
|
NORMAL = 0
|
||||||
|
@ -71,13 +71,34 @@ class JoinPriority(object):
|
||||||
EMERGENCY = 2
|
EMERGENCY = 2
|
||||||
|
|
||||||
|
|
||||||
JOIN_PRIORITIES = (
|
JOIN_PRIORITIES = {
|
||||||
JoinPriority.LOW,
|
# Note: This also acts as a reverse lookup mapping
|
||||||
JoinPriority.MODERATE,
|
JoinPriority.LOW: 'low',
|
||||||
JoinPriority.NORMAL,
|
JoinPriority.MODERATE: 'moderate',
|
||||||
JoinPriority.HIGH,
|
JoinPriority.NORMAL: 'normal',
|
||||||
JoinPriority.EMERGENCY,
|
JoinPriority.HIGH: 'high',
|
||||||
)
|
JoinPriority.EMERGENCY: 'emergency',
|
||||||
|
}
|
||||||
|
|
||||||
|
JOIN_PRIORITY_MAP = {
|
||||||
|
# Maps against string 'low'
|
||||||
|
'l': JoinPriority.LOW,
|
||||||
|
# Maps against string 'moderate'
|
||||||
|
'm': JoinPriority.MODERATE,
|
||||||
|
# Maps against string 'normal'
|
||||||
|
'n': JoinPriority.NORMAL,
|
||||||
|
# Maps against string 'high'
|
||||||
|
'h': JoinPriority.HIGH,
|
||||||
|
# Maps against string 'emergency'
|
||||||
|
'e': JoinPriority.EMERGENCY,
|
||||||
|
|
||||||
|
# Entries to additionally support (so more like Join's API)
|
||||||
|
'-2': JoinPriority.LOW,
|
||||||
|
'-1': JoinPriority.MODERATE,
|
||||||
|
'0': JoinPriority.NORMAL,
|
||||||
|
'1': JoinPriority.HIGH,
|
||||||
|
'2': JoinPriority.EMERGENCY,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class NotifyJoin(NotifyBase):
|
class NotifyJoin(NotifyBase):
|
||||||
|
@ -189,11 +210,13 @@ class NotifyJoin(NotifyBase):
|
||||||
raise TypeError(msg)
|
raise TypeError(msg)
|
||||||
|
|
||||||
# The Priority of the message
|
# The Priority of the message
|
||||||
if priority not in JOIN_PRIORITIES:
|
self.priority = int(
|
||||||
self.priority = self.template_args['priority']['default']
|
NotifyJoin.template_args['priority']['default']
|
||||||
|
if priority is None else
|
||||||
else:
|
next((
|
||||||
self.priority = priority
|
v for k, v in JOIN_PRIORITY_MAP.items()
|
||||||
|
if str(priority).lower().startswith(k)),
|
||||||
|
NotifyJoin.template_args['priority']['default']))
|
||||||
|
|
||||||
# Prepare a list of targets to store entries into
|
# Prepare a list of targets to store entries into
|
||||||
self.targets = list()
|
self.targets = list()
|
||||||
|
@ -324,19 +347,12 @@ class NotifyJoin(NotifyBase):
|
||||||
"""
|
"""
|
||||||
Returns the URL built dynamically based on specified arguments.
|
Returns the URL built dynamically based on specified arguments.
|
||||||
"""
|
"""
|
||||||
_map = {
|
|
||||||
JoinPriority.LOW: 'low',
|
|
||||||
JoinPriority.MODERATE: 'moderate',
|
|
||||||
JoinPriority.NORMAL: 'normal',
|
|
||||||
JoinPriority.HIGH: 'high',
|
|
||||||
JoinPriority.EMERGENCY: 'emergency',
|
|
||||||
}
|
|
||||||
|
|
||||||
# Define any URL parameters
|
# Define any URL parameters
|
||||||
params = {
|
params = {
|
||||||
'priority':
|
'priority':
|
||||||
_map[self.template_args['priority']['default']]
|
JOIN_PRIORITIES[self.template_args['priority']['default']]
|
||||||
if self.priority not in _map else _map[self.priority],
|
if self.priority not in JOIN_PRIORITIES
|
||||||
|
else JOIN_PRIORITIES[self.priority],
|
||||||
'image': 'yes' if self.include_image else 'no',
|
'image': 'yes' if self.include_image else 'no',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -371,20 +387,8 @@ class NotifyJoin(NotifyBase):
|
||||||
|
|
||||||
# Set our priority
|
# Set our priority
|
||||||
if 'priority' in results['qsd'] and len(results['qsd']['priority']):
|
if 'priority' in results['qsd'] and len(results['qsd']['priority']):
|
||||||
_map = {
|
results['priority'] = \
|
||||||
'l': JoinPriority.LOW,
|
NotifyJoin.unquote(results['qsd']['priority'])
|
||||||
'm': JoinPriority.MODERATE,
|
|
||||||
'n': JoinPriority.NORMAL,
|
|
||||||
'h': JoinPriority.HIGH,
|
|
||||||
'e': JoinPriority.EMERGENCY,
|
|
||||||
}
|
|
||||||
try:
|
|
||||||
results['priority'] = \
|
|
||||||
_map[results['qsd']['priority'][0].lower()]
|
|
||||||
|
|
||||||
except KeyError:
|
|
||||||
# No priority was set
|
|
||||||
pass
|
|
||||||
|
|
||||||
# Our Devices
|
# Our Devices
|
||||||
results['targets'] = list()
|
results['targets'] = list()
|
||||||
|
|
|
@ -85,7 +85,6 @@
|
||||||
|
|
||||||
|
|
||||||
import re
|
import re
|
||||||
import six
|
|
||||||
import requests
|
import requests
|
||||||
from json import dumps
|
from json import dumps
|
||||||
from .NotifyBase import NotifyBase
|
from .NotifyBase import NotifyBase
|
||||||
|
@ -104,7 +103,7 @@ LAMETRIC_APP_ID_DETECTOR_RE = re.compile(
|
||||||
LAMETRIC_IS_APP_TOKEN = re.compile(r'^[a-z0-9]{80,}==$', re.I)
|
LAMETRIC_IS_APP_TOKEN = re.compile(r'^[a-z0-9]{80,}==$', re.I)
|
||||||
|
|
||||||
|
|
||||||
class LametricMode(object):
|
class LametricMode:
|
||||||
"""
|
"""
|
||||||
Define Lametric Notification Modes
|
Define Lametric Notification Modes
|
||||||
"""
|
"""
|
||||||
|
@ -121,7 +120,7 @@ LAMETRIC_MODES = (
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class LametricPriority(object):
|
class LametricPriority:
|
||||||
"""
|
"""
|
||||||
Priority of the message
|
Priority of the message
|
||||||
"""
|
"""
|
||||||
|
@ -158,7 +157,7 @@ LAMETRIC_PRIORITIES = (
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class LametricIconType(object):
|
class LametricIconType:
|
||||||
"""
|
"""
|
||||||
Represents the nature of notification.
|
Represents the nature of notification.
|
||||||
"""
|
"""
|
||||||
|
@ -184,7 +183,7 @@ LAMETRIC_ICON_TYPES = (
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class LametricSoundCategory(object):
|
class LametricSoundCategory:
|
||||||
"""
|
"""
|
||||||
Define Sound Categories
|
Define Sound Categories
|
||||||
"""
|
"""
|
||||||
|
@ -192,7 +191,7 @@ class LametricSoundCategory(object):
|
||||||
ALARMS = "alarms"
|
ALARMS = "alarms"
|
||||||
|
|
||||||
|
|
||||||
class LametricSound(object):
|
class LametricSound:
|
||||||
"""
|
"""
|
||||||
There are 2 categories of sounds, to make things simple we just lump them
|
There are 2 categories of sounds, to make things simple we just lump them
|
||||||
all togther in one class object.
|
all togther in one class object.
|
||||||
|
@ -471,7 +470,7 @@ class NotifyLametric(NotifyBase):
|
||||||
super(NotifyLametric, self).__init__(**kwargs)
|
super(NotifyLametric, self).__init__(**kwargs)
|
||||||
|
|
||||||
self.mode = mode.strip().lower() \
|
self.mode = mode.strip().lower() \
|
||||||
if isinstance(mode, six.string_types) \
|
if isinstance(mode, str) \
|
||||||
else self.template_args['mode']['default']
|
else self.template_args['mode']['default']
|
||||||
|
|
||||||
# Default Cloud Argument
|
# Default Cloud Argument
|
||||||
|
@ -543,7 +542,7 @@ class NotifyLametric(NotifyBase):
|
||||||
# assign our icon (if it was defined); we also eliminate
|
# assign our icon (if it was defined); we also eliminate
|
||||||
# any hashtag (#) entries that might be present
|
# any hashtag (#) entries that might be present
|
||||||
self.icon = re.search(r'[#\s]*(?P<value>.+?)\s*$', icon) \
|
self.icon = re.search(r'[#\s]*(?P<value>.+?)\s*$', icon) \
|
||||||
.group('value') if isinstance(icon, six.string_types) else None
|
.group('value') if isinstance(icon, str) else None
|
||||||
|
|
||||||
if icon_type not in LAMETRIC_ICON_TYPES:
|
if icon_type not in LAMETRIC_ICON_TYPES:
|
||||||
self.icon_type = self.template_args['icon_type']['default']
|
self.icon_type = self.template_args['icon_type']['default']
|
||||||
|
@ -557,7 +556,7 @@ class NotifyLametric(NotifyBase):
|
||||||
cycles > self.template_args['cycles']['min']) else cycles
|
cycles > self.template_args['cycles']['min']) else cycles
|
||||||
|
|
||||||
self.sound = None
|
self.sound = None
|
||||||
if isinstance(sound, six.string_types):
|
if isinstance(sound, str):
|
||||||
# If sound is set, get it's match
|
# If sound is set, get it's match
|
||||||
self.sound = self.sound_lookup(sound.strip().lower())
|
self.sound = self.sound_lookup(sound.strip().lower())
|
||||||
if self.sound is None:
|
if self.sound is None:
|
||||||
|
|
309
libs/apprise/plugins/NotifyLine.py
Normal file
309
libs/apprise/plugins/NotifyLine.py
Normal file
|
@ -0,0 +1,309 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
#
|
||||||
|
# Copyright (C) 2022 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.
|
||||||
|
#
|
||||||
|
# API Docs: https://developers.line.biz/en/reference/messaging-api/
|
||||||
|
|
||||||
|
import requests
|
||||||
|
import re
|
||||||
|
from json import dumps
|
||||||
|
|
||||||
|
from .NotifyBase import NotifyBase
|
||||||
|
from ..URLBase import PrivacyMode
|
||||||
|
from ..common import NotifyType
|
||||||
|
from ..common import NotifyImageSize
|
||||||
|
from ..utils import validate_regex
|
||||||
|
from ..utils import parse_list
|
||||||
|
from ..utils import parse_bool
|
||||||
|
from ..AppriseLocale import gettext_lazy as _
|
||||||
|
|
||||||
|
|
||||||
|
# Used to break path apart into list of streams
|
||||||
|
TARGET_LIST_DELIM = re.compile(r'[ \t\r\n,#\\/]+')
|
||||||
|
|
||||||
|
|
||||||
|
class NotifyLine(NotifyBase):
|
||||||
|
"""
|
||||||
|
A wrapper for Line Notifications
|
||||||
|
"""
|
||||||
|
|
||||||
|
# The default descriptive name associated with the Notification
|
||||||
|
service_name = 'Line'
|
||||||
|
|
||||||
|
# The services URL
|
||||||
|
service_url = 'https://line.me/'
|
||||||
|
|
||||||
|
# Secure Protocol
|
||||||
|
secure_protocol = 'line'
|
||||||
|
|
||||||
|
# The URL refererenced for remote Notifications
|
||||||
|
notify_url = 'https://api.line.me/v2/bot/message/push'
|
||||||
|
|
||||||
|
# A URL that takes you to the setup/help of the specific protocol
|
||||||
|
setup_url = 'https://github.com/caronc/apprise/wiki/Notify_line'
|
||||||
|
|
||||||
|
# We don't support titles for Line notifications
|
||||||
|
title_maxlen = 0
|
||||||
|
|
||||||
|
# Maximum body length is 5000
|
||||||
|
body_maxlen = 5000
|
||||||
|
|
||||||
|
# Allows the user to specify the NotifyImageSize object; this is supported
|
||||||
|
# through the webhook
|
||||||
|
image_size = NotifyImageSize.XY_128
|
||||||
|
|
||||||
|
# Define object templates
|
||||||
|
templates = (
|
||||||
|
'{schema}://{token}/{targets}',
|
||||||
|
)
|
||||||
|
|
||||||
|
# Define our template tokens
|
||||||
|
template_tokens = dict(NotifyBase.template_tokens, **{
|
||||||
|
'token': {
|
||||||
|
'name': _('Access Token'),
|
||||||
|
'type': 'string',
|
||||||
|
'private': True,
|
||||||
|
'required': True
|
||||||
|
},
|
||||||
|
'target_user': {
|
||||||
|
'name': _('Target User'),
|
||||||
|
'type': 'string',
|
||||||
|
'map_to': 'targets',
|
||||||
|
},
|
||||||
|
'targets': {
|
||||||
|
'name': _('Targets'),
|
||||||
|
'type': 'list:string',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
# Define our template arguments
|
||||||
|
template_args = dict(NotifyBase.template_args, **{
|
||||||
|
'to': {
|
||||||
|
'alias_of': 'targets',
|
||||||
|
},
|
||||||
|
'image': {
|
||||||
|
'name': _('Include Image'),
|
||||||
|
'type': 'bool',
|
||||||
|
'default': True,
|
||||||
|
'map_to': 'include_image',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
def __init__(self, token, targets=None, include_image=True, **kwargs):
|
||||||
|
"""
|
||||||
|
Initialize Line Object
|
||||||
|
"""
|
||||||
|
super(NotifyLine, self).__init__(**kwargs)
|
||||||
|
|
||||||
|
# Long-Lived Access token (generated from User Profile)
|
||||||
|
self.token = validate_regex(token)
|
||||||
|
if not self.token:
|
||||||
|
msg = 'An invalid Access Token ' \
|
||||||
|
'({}) was specified.'.format(token)
|
||||||
|
self.logger.warning(msg)
|
||||||
|
raise TypeError(msg)
|
||||||
|
|
||||||
|
# Display our Apprise Image
|
||||||
|
self.include_image = include_image
|
||||||
|
|
||||||
|
# Set up our targets
|
||||||
|
self.targets = parse_list(targets)
|
||||||
|
|
||||||
|
# A dictionary of cached users
|
||||||
|
self.__cached_users = dict()
|
||||||
|
|
||||||
|
return
|
||||||
|
|
||||||
|
def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs):
|
||||||
|
"""
|
||||||
|
Send our Line Notification
|
||||||
|
"""
|
||||||
|
|
||||||
|
if len(self.targets) == 0:
|
||||||
|
# There were no services to notify
|
||||||
|
self.logger.warning('There were no Line targets to notify.')
|
||||||
|
return False
|
||||||
|
|
||||||
|
# error tracking (used for function return)
|
||||||
|
has_error = False
|
||||||
|
|
||||||
|
# Prepare our headers
|
||||||
|
headers = {
|
||||||
|
'User-Agent': self.app_id,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': 'Bearer {}'.format(self.token),
|
||||||
|
}
|
||||||
|
|
||||||
|
# Prepare our persistent_notification.create payload
|
||||||
|
payload = {
|
||||||
|
"to": None,
|
||||||
|
"messages": [
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"text": body,
|
||||||
|
"sender": {
|
||||||
|
"name": self.app_id,
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
# Acquire our image url if configured to do so
|
||||||
|
image_url = None if not self.include_image else \
|
||||||
|
self.image_url(notify_type)
|
||||||
|
|
||||||
|
if image_url:
|
||||||
|
payload["messages"][0]["sender"]["iconUrl"] = image_url
|
||||||
|
|
||||||
|
# Create a copy of the target list
|
||||||
|
targets = list(self.targets)
|
||||||
|
while len(targets):
|
||||||
|
target = targets.pop(0)
|
||||||
|
|
||||||
|
payload['to'] = target
|
||||||
|
|
||||||
|
self.logger.debug('Line POST URL: %s (cert_verify=%r)' % (
|
||||||
|
self.notify_url, self.verify_certificate,
|
||||||
|
))
|
||||||
|
self.logger.debug('Line Payload: %s' % str(payload))
|
||||||
|
|
||||||
|
# Always call throttle before any remote server i/o is made
|
||||||
|
self.throttle()
|
||||||
|
try:
|
||||||
|
r = requests.post(
|
||||||
|
self.notify_url,
|
||||||
|
data=dumps(payload),
|
||||||
|
headers=headers,
|
||||||
|
verify=self.verify_certificate,
|
||||||
|
timeout=self.request_timeout,
|
||||||
|
)
|
||||||
|
if r.status_code != requests.codes.ok:
|
||||||
|
# We had a problem
|
||||||
|
status_str = \
|
||||||
|
NotifyLine.http_response_code_lookup(
|
||||||
|
r.status_code)
|
||||||
|
|
||||||
|
self.logger.warning(
|
||||||
|
'Failed to send Line notification to {}: '
|
||||||
|
'{}{}error={}.'.format(
|
||||||
|
target,
|
||||||
|
status_str,
|
||||||
|
', ' if status_str else '',
|
||||||
|
r.status_code))
|
||||||
|
|
||||||
|
self.logger.debug(
|
||||||
|
'Response Details:\r\n{}'.format(r.content))
|
||||||
|
|
||||||
|
# Mark our failure
|
||||||
|
has_error = True
|
||||||
|
continue
|
||||||
|
|
||||||
|
else:
|
||||||
|
self.logger.info(
|
||||||
|
'Sent Line notification to {}.'.format(target))
|
||||||
|
|
||||||
|
except requests.RequestException as e:
|
||||||
|
self.logger.warning(
|
||||||
|
'A Connection error occurred sending Line '
|
||||||
|
'notification to {}.'.format(target))
|
||||||
|
self.logger.debug('Socket Exception: %s' % str(e))
|
||||||
|
|
||||||
|
# Mark our failure
|
||||||
|
has_error = True
|
||||||
|
continue
|
||||||
|
|
||||||
|
return not has_error
|
||||||
|
|
||||||
|
def url(self, privacy=False, *args, **kwargs):
|
||||||
|
"""
|
||||||
|
Returns the URL built dynamically based on specified arguments.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Define any URL parameters
|
||||||
|
params = {
|
||||||
|
'image': 'yes' if self.include_image else 'no',
|
||||||
|
}
|
||||||
|
|
||||||
|
# Extend our parameters
|
||||||
|
params.update(self.url_parameters(privacy=privacy, *args, **kwargs))
|
||||||
|
|
||||||
|
return '{schema}://{token}/{targets}?{params}'.format(
|
||||||
|
schema=self.secure_protocol,
|
||||||
|
# never encode hostname since we're expecting it to be a valid one
|
||||||
|
token=self.pprint(
|
||||||
|
self.token, privacy, mode=PrivacyMode.Secret, safe=''),
|
||||||
|
targets='/'.join(
|
||||||
|
[self.pprint(x, privacy, safe='') for x in self.targets]),
|
||||||
|
params=NotifyLine.urlencode(params),
|
||||||
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def parse_url(url):
|
||||||
|
"""
|
||||||
|
Parses the URL and returns enough arguments that can allow
|
||||||
|
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
|
||||||
|
|
||||||
|
# Get unquoted entries
|
||||||
|
results['targets'] = NotifyLine.split_path(results['fullpath'])
|
||||||
|
|
||||||
|
# The 'token' makes it easier to use yaml configuration
|
||||||
|
if 'token' in results['qsd'] and len(results['qsd']['token']):
|
||||||
|
results['token'] = \
|
||||||
|
NotifyLine.unquote(results['qsd']['token'])
|
||||||
|
else:
|
||||||
|
results['token'] = NotifyLine.unquote(results['host'])
|
||||||
|
|
||||||
|
# Line Long Lived Tokens included forward slashes in them.
|
||||||
|
# As a result we need to parse further into our path and look
|
||||||
|
# for the entry that ends in an equal symbol.
|
||||||
|
if not results['token'].endswith('='):
|
||||||
|
for index, entry in enumerate(
|
||||||
|
list(results['targets']), start=1):
|
||||||
|
if entry.endswith('='):
|
||||||
|
# Found
|
||||||
|
results['token'] += \
|
||||||
|
'/' + '/'.join(results['targets'][0:index])
|
||||||
|
results['targets'] = results['targets'][index:]
|
||||||
|
break
|
||||||
|
|
||||||
|
# Include images with our message
|
||||||
|
results['include_image'] = \
|
||||||
|
parse_bool(results['qsd'].get('image', True))
|
||||||
|
|
||||||
|
# Support the 'to' variable so that we can support rooms this way too
|
||||||
|
# The 'to' makes it easier to use yaml configuration
|
||||||
|
if 'to' in results['qsd'] and len(results['qsd']['to']):
|
||||||
|
results['targets'] += [x for x in filter(
|
||||||
|
bool, TARGET_LIST_DELIM.split(
|
||||||
|
NotifyLine.unquote(results['qsd']['to'])))]
|
||||||
|
|
||||||
|
return results
|
|
@ -32,7 +32,6 @@
|
||||||
# /blob/master/src/paho/mqtt/client.py
|
# /blob/master/src/paho/mqtt/client.py
|
||||||
import ssl
|
import ssl
|
||||||
import re
|
import re
|
||||||
import six
|
|
||||||
from time import sleep
|
from time import sleep
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from os.path import isfile
|
from os.path import isfile
|
||||||
|
@ -46,11 +45,6 @@ from ..AppriseLocale import gettext_lazy as _
|
||||||
# Default our global support flag
|
# Default our global support flag
|
||||||
NOTIFY_MQTT_SUPPORT_ENABLED = False
|
NOTIFY_MQTT_SUPPORT_ENABLED = False
|
||||||
|
|
||||||
if six.PY2:
|
|
||||||
# handle Python v2.7 suport
|
|
||||||
class ConnectionError(Exception):
|
|
||||||
pass
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# 3rd party modules
|
# 3rd party modules
|
||||||
import paho.mqtt.client as mqtt
|
import paho.mqtt.client as mqtt
|
||||||
|
|
|
@ -41,7 +41,7 @@ from ..utils import validate_regex
|
||||||
from ..AppriseLocale import gettext_lazy as _
|
from ..AppriseLocale import gettext_lazy as _
|
||||||
|
|
||||||
|
|
||||||
class MSG91Route(object):
|
class MSG91Route:
|
||||||
"""
|
"""
|
||||||
Transactional SMS Routes
|
Transactional SMS Routes
|
||||||
route=1 for promotional, route=4 for transactional SMS.
|
route=1 for promotional, route=4 for transactional SMS.
|
||||||
|
@ -57,7 +57,7 @@ MSG91_ROUTES = (
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class MSG91Country(object):
|
class MSG91Country:
|
||||||
"""
|
"""
|
||||||
Optional value that can be specified on the MSG91 api
|
Optional value that can be specified on the MSG91 api
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -76,6 +76,7 @@
|
||||||
import re
|
import re
|
||||||
import requests
|
import requests
|
||||||
import json
|
import json
|
||||||
|
from json.decoder import JSONDecodeError
|
||||||
|
|
||||||
from .NotifyBase import NotifyBase
|
from .NotifyBase import NotifyBase
|
||||||
from ..common import NotifyImageSize
|
from ..common import NotifyImageSize
|
||||||
|
@ -88,13 +89,6 @@ from ..utils import TemplateType
|
||||||
from ..AppriseAttachment import AppriseAttachment
|
from ..AppriseAttachment import AppriseAttachment
|
||||||
from ..AppriseLocale import gettext_lazy as _
|
from ..AppriseLocale import gettext_lazy as _
|
||||||
|
|
||||||
try:
|
|
||||||
from json.decoder import JSONDecodeError
|
|
||||||
|
|
||||||
except ImportError:
|
|
||||||
# Python v2.7 Backwards Compatibility support
|
|
||||||
JSONDecodeError = ValueError
|
|
||||||
|
|
||||||
|
|
||||||
class NotifyMSTeams(NotifyBase):
|
class NotifyMSTeams(NotifyBase):
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -74,7 +74,7 @@ MAILGUN_HTTP_ERROR_MAP = {
|
||||||
|
|
||||||
|
|
||||||
# Priorities
|
# Priorities
|
||||||
class MailgunRegion(object):
|
class MailgunRegion:
|
||||||
US = 'us'
|
US = 'us'
|
||||||
EU = 'eu'
|
EU = 'eu'
|
||||||
|
|
||||||
|
@ -383,17 +383,9 @@ class NotifyMailgun(NotifyBase):
|
||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
try:
|
reply_to = formataddr(
|
||||||
reply_to = formataddr(
|
(self.from_name if self.from_name else False,
|
||||||
(self.from_name if self.from_name else False,
|
self.from_addr), charset='utf-8')
|
||||||
self.from_addr), charset='utf-8')
|
|
||||||
|
|
||||||
except TypeError:
|
|
||||||
# Python v2.x Support (no charset keyword)
|
|
||||||
# Format our cc addresses to support the Name field
|
|
||||||
reply_to = formataddr(
|
|
||||||
(self.from_name if self.from_name else False,
|
|
||||||
self.from_addr))
|
|
||||||
|
|
||||||
# Prepare our payload
|
# Prepare our payload
|
||||||
payload = {
|
payload = {
|
||||||
|
@ -461,33 +453,17 @@ class NotifyMailgun(NotifyBase):
|
||||||
# Strip target out of bcc list if in To
|
# Strip target out of bcc list if in To
|
||||||
bcc = (bcc - set([to_addr[1]]))
|
bcc = (bcc - set([to_addr[1]]))
|
||||||
|
|
||||||
try:
|
# Prepare our `to`
|
||||||
# Prepare our to
|
to.append(formataddr(to_addr, charset='utf-8'))
|
||||||
to.append(formataddr(to_addr, charset='utf-8'))
|
|
||||||
|
|
||||||
except TypeError:
|
|
||||||
# Python v2.x Support (no charset keyword)
|
|
||||||
# Format our cc addresses to support the Name field
|
|
||||||
|
|
||||||
# Prepare our to
|
|
||||||
to.append(formataddr(to_addr))
|
|
||||||
|
|
||||||
# Prepare our To
|
# Prepare our To
|
||||||
payload['to'] = ','.join(to)
|
payload['to'] = ','.join(to)
|
||||||
|
|
||||||
if cc:
|
if cc:
|
||||||
try:
|
# Format our cc addresses to support the Name field
|
||||||
# Format our cc addresses to support the Name field
|
payload['cc'] = ','.join([formataddr(
|
||||||
payload['cc'] = ','.join([formataddr(
|
(self.names.get(addr, False), addr), charset='utf-8')
|
||||||
(self.names.get(addr, False), addr), charset='utf-8')
|
for addr in cc])
|
||||||
for addr in cc])
|
|
||||||
|
|
||||||
except TypeError:
|
|
||||||
# Python v2.x Support (no charset keyword)
|
|
||||||
# Format our cc addresses to support the Name field
|
|
||||||
payload['cc'] = ','.join([formataddr( # pragma: no branch
|
|
||||||
(self.names.get(addr, False), addr))
|
|
||||||
for addr in cc])
|
|
||||||
|
|
||||||
# Format our bcc addresses to support the Name field
|
# Format our bcc addresses to support the Name field
|
||||||
if bcc:
|
if bcc:
|
||||||
|
|
|
@ -28,7 +28,6 @@
|
||||||
# - https://github.com/matrix-org/synapse/blob/master/docs/reverse_proxy.rst
|
# - https://github.com/matrix-org/synapse/blob/master/docs/reverse_proxy.rst
|
||||||
#
|
#
|
||||||
import re
|
import re
|
||||||
import six
|
|
||||||
import requests
|
import requests
|
||||||
from markdown import markdown
|
from markdown import markdown
|
||||||
from json import dumps
|
from json import dumps
|
||||||
|
@ -67,7 +66,7 @@ IS_ROOM_ID = re.compile(
|
||||||
r'(?P<home_server>[a-z0-9.-]+))?\s*$', re.I)
|
r'(?P<home_server>[a-z0-9.-]+))?\s*$', re.I)
|
||||||
|
|
||||||
|
|
||||||
class MatrixMessageType(object):
|
class MatrixMessageType:
|
||||||
"""
|
"""
|
||||||
The Matrix Message types
|
The Matrix Message types
|
||||||
"""
|
"""
|
||||||
|
@ -82,7 +81,7 @@ MATRIX_MESSAGE_TYPES = (
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class MatrixWebhookMode(object):
|
class MatrixWebhookMode:
|
||||||
# Webhook Mode is disabled
|
# Webhook Mode is disabled
|
||||||
DISABLED = "off"
|
DISABLED = "off"
|
||||||
|
|
||||||
|
@ -263,7 +262,7 @@ class NotifyMatrix(NotifyBase):
|
||||||
|
|
||||||
# Setup our mode
|
# Setup our mode
|
||||||
self.mode = self.template_args['mode']['default'] \
|
self.mode = self.template_args['mode']['default'] \
|
||||||
if not isinstance(mode, six.string_types) else mode.lower()
|
if not isinstance(mode, str) else mode.lower()
|
||||||
if self.mode and self.mode not in MATRIX_WEBHOOK_MODES:
|
if self.mode and self.mode not in MATRIX_WEBHOOK_MODES:
|
||||||
msg = 'The mode specified ({}) is invalid.'.format(mode)
|
msg = 'The mode specified ({}) is invalid.'.format(mode)
|
||||||
self.logger.warning(msg)
|
self.logger.warning(msg)
|
||||||
|
@ -271,7 +270,7 @@ class NotifyMatrix(NotifyBase):
|
||||||
|
|
||||||
# Setup our message type
|
# Setup our message type
|
||||||
self.msgtype = self.template_args['msgtype']['default'] \
|
self.msgtype = self.template_args['msgtype']['default'] \
|
||||||
if not isinstance(msgtype, six.string_types) else msgtype.lower()
|
if not isinstance(msgtype, str) else msgtype.lower()
|
||||||
if self.msgtype and self.msgtype not in MATRIX_MESSAGE_TYPES:
|
if self.msgtype and self.msgtype not in MATRIX_MESSAGE_TYPES:
|
||||||
msg = 'The msgtype specified ({}) is invalid.'.format(msgtype)
|
msg = 'The msgtype specified ({}) is invalid.'.format(msgtype)
|
||||||
self.logger.warning(msg)
|
self.logger.warning(msg)
|
||||||
|
@ -411,7 +410,7 @@ class NotifyMatrix(NotifyBase):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if not hasattr(self, '_re_slack_formatting_rules'):
|
if not hasattr(self, '_re_slack_formatting_rules'):
|
||||||
# Prepare some one-time slack formating variables
|
# Prepare some one-time slack formatting variables
|
||||||
|
|
||||||
self._re_slack_formatting_map = {
|
self._re_slack_formatting_map = {
|
||||||
# New lines must become the string version
|
# New lines must become the string version
|
||||||
|
@ -762,7 +761,7 @@ class NotifyMatrix(NotifyBase):
|
||||||
# We can't join a room if we're not logged in
|
# We can't join a room if we're not logged in
|
||||||
return None
|
return None
|
||||||
|
|
||||||
if not isinstance(room, six.string_types):
|
if not isinstance(room, str):
|
||||||
# Not a supported string
|
# Not a supported string
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
@ -850,7 +849,7 @@ class NotifyMatrix(NotifyBase):
|
||||||
# We can't create a room if we're not logged in
|
# We can't create a room if we're not logged in
|
||||||
return None
|
return None
|
||||||
|
|
||||||
if not isinstance(room, six.string_types):
|
if not isinstance(room, str):
|
||||||
# Not a supported string
|
# Not a supported string
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
@ -930,7 +929,7 @@ class NotifyMatrix(NotifyBase):
|
||||||
# We can't get a room id if we're not logged in
|
# We can't get a room id if we're not logged in
|
||||||
return None
|
return None
|
||||||
|
|
||||||
if not isinstance(room, six.string_types):
|
if not isinstance(room, str):
|
||||||
# Not a supported string
|
# Not a supported string
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
@ -1109,20 +1108,20 @@ class NotifyMatrix(NotifyBase):
|
||||||
# - https://bugs.python.org/issue29288
|
# - https://bugs.python.org/issue29288
|
||||||
#
|
#
|
||||||
# A ~similar~ issue can be identified here in the requests
|
# A ~similar~ issue can be identified here in the requests
|
||||||
# ticket system as unresolved and has provided work-arounds
|
# ticket system as unresolved and has provided workarounds
|
||||||
# - https://github.com/kennethreitz/requests/issues/3578
|
# - https://github.com/kennethreitz/requests/issues/3578
|
||||||
pass
|
pass
|
||||||
|
|
||||||
except ImportError: # pragma: no cover
|
except ImportError: # pragma: no cover
|
||||||
# The actual exception is `ModuleNotFoundError` however ImportError
|
# The actual exception is `ModuleNotFoundError` however ImportError
|
||||||
# grants us backwards compatiblity with versions of Python older
|
# grants us backwards compatibility with versions of Python older
|
||||||
# than v3.6
|
# than v3.6
|
||||||
|
|
||||||
# Python code that makes early calls to sys.exit() can cause
|
# Python code that makes early calls to sys.exit() can cause
|
||||||
# the __del__() code to run. However in some newer versions of
|
# the __del__() code to run. However, in some newer versions of
|
||||||
# Python, this causes the `sys` library to no longer be
|
# Python, this causes the `sys` library to no longer be
|
||||||
# available. The stack overflow also goes on to suggest that
|
# available. The stack overflow also goes on to suggest that
|
||||||
# it's not wise to use the __del__() as a deconstructor
|
# it's not wise to use the __del__() as a destructor
|
||||||
# which is the case here.
|
# which is the case here.
|
||||||
|
|
||||||
# https://stackoverflow.com/questions/67218341/\
|
# https://stackoverflow.com/questions/67218341/\
|
||||||
|
@ -1134,7 +1133,7 @@ class NotifyMatrix(NotifyBase):
|
||||||
# /1481488/what-is-the-del-method-and-how-do-i-call-it
|
# /1481488/what-is-the-del-method-and-how-do-i-call-it
|
||||||
|
|
||||||
# At this time it seems clean to try to log out (if we can)
|
# At this time it seems clean to try to log out (if we can)
|
||||||
# but not throw any unessisary exceptions (like this one) to
|
# but not throw any unnecessary exceptions (like this one) to
|
||||||
# the end user if we don't have to.
|
# the end user if we don't have to.
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
|
@ -33,7 +33,6 @@
|
||||||
# - swap http with mmost
|
# - swap http with mmost
|
||||||
# - drop /hooks/ reference
|
# - drop /hooks/ reference
|
||||||
|
|
||||||
import six
|
|
||||||
import requests
|
import requests
|
||||||
from json import dumps
|
from json import dumps
|
||||||
|
|
||||||
|
@ -156,7 +155,7 @@ class NotifyMattermost(NotifyBase):
|
||||||
|
|
||||||
# our full path
|
# our full path
|
||||||
self.fullpath = '' if not isinstance(
|
self.fullpath = '' if not isinstance(
|
||||||
fullpath, six.string_types) else fullpath.strip()
|
fullpath, str) else fullpath.strip()
|
||||||
|
|
||||||
# Authorization Token (associated with project)
|
# Authorization Token (associated with project)
|
||||||
self.token = validate_regex(token)
|
self.token = validate_regex(token)
|
||||||
|
|
|
@ -331,13 +331,10 @@ class NotifyNextcloud(NotifyBase):
|
||||||
results['version'] = \
|
results['version'] = \
|
||||||
NotifyNextcloud.unquote(results['qsd']['version'])
|
NotifyNextcloud.unquote(results['qsd']['version'])
|
||||||
|
|
||||||
# Add our headers that the user can potentially over-ride if they
|
# Add our headers that the user can potentially over-ride if they wish
|
||||||
# wish to to our returned result set
|
# to to our returned result set and tidy entries by unquoting them
|
||||||
results['headers'] = results['qsd+']
|
results['headers'] = {
|
||||||
if results['qsd-']:
|
NotifyNextcloud.unquote(x): NotifyNextcloud.unquote(y)
|
||||||
results['headers'].update(results['qsd-'])
|
for x, y in results['qsd+'].items()}
|
||||||
NotifyBase.logger.deprecate(
|
|
||||||
"minus (-) based Nextcloud header tokens are being "
|
|
||||||
" removed; use the plus (+) symbol instead.")
|
|
||||||
|
|
||||||
return results
|
return results
|
||||||
|
|
|
@ -269,13 +269,10 @@ class NotifyNextcloudTalk(NotifyBase):
|
||||||
results['targets'] = \
|
results['targets'] = \
|
||||||
NotifyNextcloudTalk.split_path(results['fullpath'])
|
NotifyNextcloudTalk.split_path(results['fullpath'])
|
||||||
|
|
||||||
# Add our headers that the user can potentially over-ride if they
|
# Add our headers that the user can potentially over-ride if they wish
|
||||||
# wish to to our returned result set
|
# to to our returned result set and tidy entries by unquoting them
|
||||||
results['headers'] = results['qsd+']
|
results['headers'] = {
|
||||||
if results['qsd-']:
|
NotifyNextcloudTalk.unquote(x): NotifyNextcloudTalk.unquote(y)
|
||||||
results['headers'].update(results['qsd-'])
|
for x, y in results['qsd+'].items()}
|
||||||
NotifyBase.logger.deprecate(
|
|
||||||
"minus (-) based Nextcloud Talk header tokens are being "
|
|
||||||
" removed; use the plus (+) symbol instead.")
|
|
||||||
|
|
||||||
return results
|
return results
|
||||||
|
|
|
@ -37,7 +37,6 @@
|
||||||
# notica://abc123
|
# notica://abc123
|
||||||
#
|
#
|
||||||
import re
|
import re
|
||||||
import six
|
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
from .NotifyBase import NotifyBase
|
from .NotifyBase import NotifyBase
|
||||||
|
@ -47,7 +46,7 @@ from ..utils import validate_regex
|
||||||
from ..AppriseLocale import gettext_lazy as _
|
from ..AppriseLocale import gettext_lazy as _
|
||||||
|
|
||||||
|
|
||||||
class NoticaMode(object):
|
class NoticaMode:
|
||||||
"""
|
"""
|
||||||
Tracks if we're accessing the notica upstream server or a locally hosted
|
Tracks if we're accessing the notica upstream server or a locally hosted
|
||||||
one.
|
one.
|
||||||
|
@ -176,7 +175,7 @@ class NotifyNotica(NotifyBase):
|
||||||
|
|
||||||
# prepare our fullpath
|
# prepare our fullpath
|
||||||
self.fullpath = kwargs.get('fullpath')
|
self.fullpath = kwargs.get('fullpath')
|
||||||
if not isinstance(self.fullpath, six.string_types):
|
if not isinstance(self.fullpath, str):
|
||||||
self.fullpath = '/'
|
self.fullpath = '/'
|
||||||
|
|
||||||
self.headers = {}
|
self.headers = {}
|
||||||
|
@ -364,13 +363,11 @@ class NotifyNotica(NotifyBase):
|
||||||
'/' if not entries else '/{}/'.format('/'.join(entries))
|
'/' if not entries else '/{}/'.format('/'.join(entries))
|
||||||
|
|
||||||
# Add our headers that the user can potentially over-ride if they
|
# Add our headers that the user can potentially over-ride if they
|
||||||
# wish to to our returned result set
|
# wish to to our returned result set and tidy entries by unquoting
|
||||||
results['headers'] = results['qsd+']
|
# them
|
||||||
if results['qsd-']:
|
results['headers'] = {
|
||||||
results['headers'].update(results['qsd-'])
|
NotifyNotica.unquote(x): NotifyNotica.unquote(y)
|
||||||
NotifyBase.logger.deprecate(
|
for x, y in results['qsd+'].items()}
|
||||||
"minus (-) based Notica header tokens are being "
|
|
||||||
" removed; use the plus (+) symbol instead.")
|
|
||||||
|
|
||||||
return results
|
return results
|
||||||
|
|
||||||
|
|
|
@ -48,8 +48,8 @@ from ..utils import validate_regex
|
||||||
from ..AppriseLocale import gettext_lazy as _
|
from ..AppriseLocale import gettext_lazy as _
|
||||||
|
|
||||||
|
|
||||||
class NotificoFormat(object):
|
class NotificoFormat:
|
||||||
# Resets all formating
|
# Resets all formatting
|
||||||
Reset = '\x0F'
|
Reset = '\x0F'
|
||||||
|
|
||||||
# Formatting
|
# Formatting
|
||||||
|
@ -59,7 +59,7 @@ class NotificoFormat(object):
|
||||||
BGSwap = '\x16'
|
BGSwap = '\x16'
|
||||||
|
|
||||||
|
|
||||||
class NotificoColor(object):
|
class NotificoColor:
|
||||||
# Resets Color
|
# Resets Color
|
||||||
Reset = '\x03'
|
Reset = '\x03'
|
||||||
|
|
||||||
|
@ -248,13 +248,13 @@ class NotifyNotifico(NotifyBase):
|
||||||
if self.color:
|
if self.color:
|
||||||
# Colors were specified, make sure we capture and correctly
|
# Colors were specified, make sure we capture and correctly
|
||||||
# allow them to exist inline in the message
|
# allow them to exist inline in the message
|
||||||
# \g<1> is less ambigious than \1
|
# \g<1> is less ambiguous than \1
|
||||||
body = re.sub(r'\\x03(\d{0,2})', '\x03\g<1>', body)
|
body = re.sub(r'\\x03(\d{0,2})', r'\\x03\g<1>', body)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
# no colors specified, make sure we strip out any colors found
|
# no colors specified, make sure we strip out any colors found
|
||||||
# to make the string read-able
|
# to make the string read-able
|
||||||
body = re.sub(r'\\x03(\d{1,2}(,[0-9]{1,2})?)?', '', body)
|
body = re.sub(r'\\x03(\d{1,2}(,[0-9]{1,2})?)?', r'', body)
|
||||||
|
|
||||||
# Prepare our payload
|
# Prepare our payload
|
||||||
payload = {
|
payload = {
|
||||||
|
|
|
@ -1,24 +1,31 @@
|
||||||
# MIT License
|
# -*- coding: utf-8 -*-
|
||||||
|
#
|
||||||
# Copyright (c) 2022 Joey Espinosa <@particledecay>
|
# Copyright (C) 2022 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
|
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
# of this software and associated documentation files (the "Software"), to deal
|
# of this software and associated documentation files(the "Software"), to deal
|
||||||
# in the Software without restriction, including without limitation the rights
|
# in the Software without restriction, including without limitation the rights
|
||||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell
|
||||||
# copies of the Software, and to permit persons to whom the Software is
|
# copies of the Software, and to permit persons to whom the Software is
|
||||||
# furnished to do so, subject to the following conditions:
|
# furnished to do so, subject to the following conditions :
|
||||||
|
#
|
||||||
# The above copyright notice and this permission notice shall be included in
|
# The above copyright notice and this permission notice shall be included in
|
||||||
# all copies or substantial portions of the Software.
|
# all copies or substantial portions of the Software.
|
||||||
|
#
|
||||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE
|
||||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
# 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
|
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
# SOFTWARE.
|
# THE SOFTWARE.
|
||||||
|
|
||||||
|
# Great sources
|
||||||
|
# - https://github.com/matrix-org/matrix-python-sdk
|
||||||
|
# - https://github.com/matrix-org/synapse/blob/master/docs/reverse_proxy.rst
|
||||||
#
|
#
|
||||||
# Examples:
|
# Examples:
|
||||||
# ntfys://my-topic
|
# ntfys://my-topic
|
||||||
|
@ -27,7 +34,6 @@
|
||||||
# ntfy://ntfy.local.domain/?priority=max
|
# ntfy://ntfy.local.domain/?priority=max
|
||||||
import re
|
import re
|
||||||
import requests
|
import requests
|
||||||
import six
|
|
||||||
from json import loads
|
from json import loads
|
||||||
from json import dumps
|
from json import dumps
|
||||||
from os.path import basename
|
from os.path import basename
|
||||||
|
@ -43,7 +49,7 @@ from ..URLBase import PrivacyMode
|
||||||
from ..attachment.AttachBase import AttachBase
|
from ..attachment.AttachBase import AttachBase
|
||||||
|
|
||||||
|
|
||||||
class NtfyMode(object):
|
class NtfyMode:
|
||||||
"""
|
"""
|
||||||
Define ntfy Notification Modes
|
Define ntfy Notification Modes
|
||||||
"""
|
"""
|
||||||
|
@ -60,7 +66,7 @@ NTFY_MODES = (
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class NtfyPriority(object):
|
class NtfyPriority:
|
||||||
"""
|
"""
|
||||||
Ntfy Priority Definitions
|
Ntfy Priority Definitions
|
||||||
"""
|
"""
|
||||||
|
@ -79,6 +85,39 @@ NTFY_PRIORITIES = (
|
||||||
NtfyPriority.MIN,
|
NtfyPriority.MIN,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
NTFY_PRIORITY_MAP = {
|
||||||
|
# Maps against string 'low' but maps to Moderate to avoid
|
||||||
|
# conflicting with actual ntfy mappings
|
||||||
|
'l': NtfyPriority.LOW,
|
||||||
|
# Maps against string 'moderate'
|
||||||
|
'mo': NtfyPriority.LOW,
|
||||||
|
# Maps against string 'normal'
|
||||||
|
'n': NtfyPriority.NORMAL,
|
||||||
|
# Maps against string 'high'
|
||||||
|
'h': NtfyPriority.HIGH,
|
||||||
|
# Maps against string 'emergency'
|
||||||
|
'e': NtfyPriority.MAX,
|
||||||
|
|
||||||
|
# Entries to additionally support (so more like Ntfy's API)
|
||||||
|
# Maps against string 'min'
|
||||||
|
'mi': NtfyPriority.MIN,
|
||||||
|
# Maps against string 'max'
|
||||||
|
'ma': NtfyPriority.MAX,
|
||||||
|
# Maps against string 'default'
|
||||||
|
'd': NtfyPriority.NORMAL,
|
||||||
|
|
||||||
|
# support 1-5 values as well
|
||||||
|
'1': NtfyPriority.MIN,
|
||||||
|
# Maps against string 'moderate'
|
||||||
|
'2': NtfyPriority.LOW,
|
||||||
|
# Maps against string 'normal'
|
||||||
|
'3': NtfyPriority.NORMAL,
|
||||||
|
# Maps against string 'high'
|
||||||
|
'4': NtfyPriority.HIGH,
|
||||||
|
# Maps against string 'emergency'
|
||||||
|
'5': NtfyPriority.MAX,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class NotifyNtfy(NotifyBase):
|
class NotifyNtfy(NotifyBase):
|
||||||
"""
|
"""
|
||||||
|
@ -207,7 +246,7 @@ class NotifyNtfy(NotifyBase):
|
||||||
|
|
||||||
# Prepare our mode
|
# Prepare our mode
|
||||||
self.mode = mode.strip().lower() \
|
self.mode = mode.strip().lower() \
|
||||||
if isinstance(mode, six.string_types) \
|
if isinstance(mode, str) \
|
||||||
else self.template_args['mode']['default']
|
else self.template_args['mode']['default']
|
||||||
|
|
||||||
if self.mode not in NTFY_MODES:
|
if self.mode not in NTFY_MODES:
|
||||||
|
@ -230,18 +269,13 @@ class NotifyNtfy(NotifyBase):
|
||||||
# An email to forward notifications to
|
# An email to forward notifications to
|
||||||
self.email = email
|
self.email = email
|
||||||
|
|
||||||
# The priority of the message
|
# The Priority of the message
|
||||||
|
self.priority = NotifyNtfy.template_args['priority']['default'] \
|
||||||
if priority is None:
|
if not priority else \
|
||||||
self.priority = self.template_args['priority']['default']
|
next((
|
||||||
else:
|
v for k, v in NTFY_PRIORITY_MAP.items()
|
||||||
self.priority = priority
|
if str(priority).lower().startswith(k)),
|
||||||
|
NotifyNtfy.template_args['priority']['default'])
|
||||||
if self.priority not in NTFY_PRIORITIES:
|
|
||||||
msg = 'An invalid ntfy Priority ({}) was specified.'.format(
|
|
||||||
priority)
|
|
||||||
self.logger.warning(msg)
|
|
||||||
raise TypeError(msg)
|
|
||||||
|
|
||||||
# Any optional tags to attach to the notification
|
# Any optional tags to attach to the notification
|
||||||
self.__tags = parse_list(tags)
|
self.__tags = parse_list(tags)
|
||||||
|
@ -274,7 +308,7 @@ class NotifyNtfy(NotifyBase):
|
||||||
self.logger.warning('There are no ntfy topics to notify')
|
self.logger.warning('There are no ntfy topics to notify')
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# Create a copy of the subreddits list
|
# Create a copy of the topics
|
||||||
topics = list(self.topics)
|
topics = list(self.topics)
|
||||||
while len(topics) > 0:
|
while len(topics) > 0:
|
||||||
# Retrieve our topic
|
# Retrieve our topic
|
||||||
|
@ -558,31 +592,10 @@ class NotifyNtfy(NotifyBase):
|
||||||
# We're done early as we couldn't load the results
|
# We're done early as we couldn't load the results
|
||||||
return results
|
return results
|
||||||
|
|
||||||
|
# Set our priority
|
||||||
if 'priority' in results['qsd'] and len(results['qsd']['priority']):
|
if 'priority' in results['qsd'] and len(results['qsd']['priority']):
|
||||||
_map = {
|
results['priority'] = \
|
||||||
# Supported lookups
|
NotifyNtfy.unquote(results['qsd']['priority'])
|
||||||
'mi': NtfyPriority.MIN,
|
|
||||||
'1': NtfyPriority.MIN,
|
|
||||||
'l': NtfyPriority.LOW,
|
|
||||||
'2': NtfyPriority.LOW,
|
|
||||||
'n': NtfyPriority.NORMAL, # support normal keyword
|
|
||||||
'd': NtfyPriority.NORMAL, # default keyword
|
|
||||||
'3': NtfyPriority.NORMAL,
|
|
||||||
'h': NtfyPriority.HIGH,
|
|
||||||
'4': NtfyPriority.HIGH,
|
|
||||||
'ma': NtfyPriority.MAX,
|
|
||||||
'5': NtfyPriority.MAX,
|
|
||||||
}
|
|
||||||
try:
|
|
||||||
# pretty-format (and update short-format)
|
|
||||||
results['priority'] = \
|
|
||||||
_map[results['qsd']['priority'][0:2].lower()]
|
|
||||||
|
|
||||||
except KeyError:
|
|
||||||
# Pass along what was set so it can be handed during
|
|
||||||
# initialization
|
|
||||||
results['priority'] = str(results['qsd']['priority'])
|
|
||||||
pass
|
|
||||||
|
|
||||||
if 'attach' in results['qsd'] and len(results['qsd']['attach']):
|
if 'attach' in results['qsd'] and len(results['qsd']['attach']):
|
||||||
results['attach'] = NotifyNtfy.unquote(results['qsd']['attach'])
|
results['attach'] = NotifyNtfy.unquote(results['qsd']['attach'])
|
||||||
|
|
|
@ -74,7 +74,7 @@ OPSGENIE_CATEGORIES = (
|
||||||
|
|
||||||
|
|
||||||
# Regions
|
# Regions
|
||||||
class OpsgenieRegion(object):
|
class OpsgenieRegion:
|
||||||
US = 'us'
|
US = 'us'
|
||||||
EU = 'eu'
|
EU = 'eu'
|
||||||
|
|
||||||
|
@ -93,7 +93,7 @@ OPSGENIE_REGIONS = (
|
||||||
|
|
||||||
|
|
||||||
# Priorities
|
# Priorities
|
||||||
class OpsgeniePriority(object):
|
class OpsgeniePriority:
|
||||||
LOW = 1
|
LOW = 1
|
||||||
MODERATE = 2
|
MODERATE = 2
|
||||||
NORMAL = 3
|
NORMAL = 3
|
||||||
|
@ -101,13 +101,40 @@ class OpsgeniePriority(object):
|
||||||
EMERGENCY = 5
|
EMERGENCY = 5
|
||||||
|
|
||||||
|
|
||||||
OPSGENIE_PRIORITIES = (
|
OPSGENIE_PRIORITIES = {
|
||||||
OpsgeniePriority.LOW,
|
# Note: This also acts as a reverse lookup mapping
|
||||||
OpsgeniePriority.MODERATE,
|
OpsgeniePriority.LOW: 'low',
|
||||||
OpsgeniePriority.NORMAL,
|
OpsgeniePriority.MODERATE: 'moderate',
|
||||||
OpsgeniePriority.HIGH,
|
OpsgeniePriority.NORMAL: 'normal',
|
||||||
OpsgeniePriority.EMERGENCY,
|
OpsgeniePriority.HIGH: 'high',
|
||||||
)
|
OpsgeniePriority.EMERGENCY: 'emergency',
|
||||||
|
}
|
||||||
|
|
||||||
|
OPSGENIE_PRIORITY_MAP = {
|
||||||
|
# Maps against string 'low'
|
||||||
|
'l': OpsgeniePriority.LOW,
|
||||||
|
# Maps against string 'moderate'
|
||||||
|
'm': OpsgeniePriority.MODERATE,
|
||||||
|
# Maps against string 'normal'
|
||||||
|
'n': OpsgeniePriority.NORMAL,
|
||||||
|
# Maps against string 'high'
|
||||||
|
'h': OpsgeniePriority.HIGH,
|
||||||
|
# Maps against string 'emergency'
|
||||||
|
'e': OpsgeniePriority.EMERGENCY,
|
||||||
|
|
||||||
|
# Entries to additionally support (so more like Opsgenie's API)
|
||||||
|
'1': OpsgeniePriority.LOW,
|
||||||
|
'2': OpsgeniePriority.MODERATE,
|
||||||
|
'3': OpsgeniePriority.NORMAL,
|
||||||
|
'4': OpsgeniePriority.HIGH,
|
||||||
|
'5': OpsgeniePriority.EMERGENCY,
|
||||||
|
# Support p-prefix
|
||||||
|
'p1': OpsgeniePriority.LOW,
|
||||||
|
'p2': OpsgeniePriority.MODERATE,
|
||||||
|
'p3': OpsgeniePriority.NORMAL,
|
||||||
|
'p4': OpsgeniePriority.HIGH,
|
||||||
|
'p5': OpsgeniePriority.EMERGENCY,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class NotifyOpsgenie(NotifyBase):
|
class NotifyOpsgenie(NotifyBase):
|
||||||
|
@ -246,11 +273,12 @@ class NotifyOpsgenie(NotifyBase):
|
||||||
raise TypeError(msg)
|
raise TypeError(msg)
|
||||||
|
|
||||||
# The Priority of the message
|
# The Priority of the message
|
||||||
if priority not in OPSGENIE_PRIORITIES:
|
self.priority = NotifyOpsgenie.template_args['priority']['default'] \
|
||||||
self.priority = OpsgeniePriority.NORMAL
|
if not priority else \
|
||||||
|
next((
|
||||||
else:
|
v for k, v in OPSGENIE_PRIORITY_MAP.items()
|
||||||
self.priority = priority
|
if str(priority).lower().startswith(k)),
|
||||||
|
NotifyOpsgenie.template_args['priority']['default'])
|
||||||
|
|
||||||
# Store our region
|
# Store our region
|
||||||
try:
|
try:
|
||||||
|
@ -353,8 +381,8 @@ class NotifyOpsgenie(NotifyBase):
|
||||||
# Initialize our has_error flag
|
# Initialize our has_error flag
|
||||||
has_error = False
|
has_error = False
|
||||||
|
|
||||||
# We want to manually set the title onto the body if specified
|
# Use body if title not set
|
||||||
title_body = body if not title else '{}: {}'.format(title, body)
|
title_body = body if not title else body
|
||||||
|
|
||||||
# Create a copy ouf our details object
|
# Create a copy ouf our details object
|
||||||
details = self.details.copy()
|
details = self.details.copy()
|
||||||
|
@ -374,7 +402,7 @@ class NotifyOpsgenie(NotifyBase):
|
||||||
# limitation
|
# limitation
|
||||||
if len(payload['message']) > self.opsgenie_body_minlen:
|
if len(payload['message']) > self.opsgenie_body_minlen:
|
||||||
payload['message'] = '{}...'.format(
|
payload['message'] = '{}...'.format(
|
||||||
body[:self.opsgenie_body_minlen - 3])
|
title_body[:self.opsgenie_body_minlen - 3])
|
||||||
|
|
||||||
if self.__tags:
|
if self.__tags:
|
||||||
payload['tags'] = self.__tags
|
payload['tags'] = self.__tags
|
||||||
|
@ -450,20 +478,13 @@ class NotifyOpsgenie(NotifyBase):
|
||||||
Returns the URL built dynamically based on specified arguments.
|
Returns the URL built dynamically based on specified arguments.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
_map = {
|
|
||||||
OpsgeniePriority.LOW: 'low',
|
|
||||||
OpsgeniePriority.MODERATE: 'moderate',
|
|
||||||
OpsgeniePriority.NORMAL: 'normal',
|
|
||||||
OpsgeniePriority.HIGH: 'high',
|
|
||||||
OpsgeniePriority.EMERGENCY: 'emergency',
|
|
||||||
}
|
|
||||||
|
|
||||||
# Define any URL parameters
|
# Define any URL parameters
|
||||||
params = {
|
params = {
|
||||||
'region': self.region_name,
|
'region': self.region_name,
|
||||||
'priority':
|
'priority':
|
||||||
_map[OpsgeniePriority.NORMAL] if self.priority not in _map
|
OPSGENIE_PRIORITIES[self.template_args['priority']['default']]
|
||||||
else _map[self.priority],
|
if self.priority not in OPSGENIE_PRIORITIES
|
||||||
|
else OPSGENIE_PRIORITIES[self.priority],
|
||||||
'batch': 'yes' if self.batch_size > 1 else 'no',
|
'batch': 'yes' if self.batch_size > 1 else 'no',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -530,38 +551,10 @@ class NotifyOpsgenie(NotifyBase):
|
||||||
results['details'] = {NotifyBase.unquote(x): NotifyBase.unquote(y)
|
results['details'] = {NotifyBase.unquote(x): NotifyBase.unquote(y)
|
||||||
for x, y in results['qsd+'].items()}
|
for x, y in results['qsd+'].items()}
|
||||||
|
|
||||||
|
# Set our priority
|
||||||
if 'priority' in results['qsd'] and len(results['qsd']['priority']):
|
if 'priority' in results['qsd'] and len(results['qsd']['priority']):
|
||||||
_map = {
|
results['priority'] = \
|
||||||
# Letter Assignnments
|
NotifyOpsgenie.unquote(results['qsd']['priority'])
|
||||||
'l': OpsgeniePriority.LOW,
|
|
||||||
'm': OpsgeniePriority.MODERATE,
|
|
||||||
'n': OpsgeniePriority.NORMAL,
|
|
||||||
'h': OpsgeniePriority.HIGH,
|
|
||||||
'e': OpsgeniePriority.EMERGENCY,
|
|
||||||
'lo': OpsgeniePriority.LOW,
|
|
||||||
'me': OpsgeniePriority.MODERATE,
|
|
||||||
'no': OpsgeniePriority.NORMAL,
|
|
||||||
'hi': OpsgeniePriority.HIGH,
|
|
||||||
'em': OpsgeniePriority.EMERGENCY,
|
|
||||||
# Support 3rd Party API Documented Scale
|
|
||||||
'1': OpsgeniePriority.LOW,
|
|
||||||
'2': OpsgeniePriority.MODERATE,
|
|
||||||
'3': OpsgeniePriority.NORMAL,
|
|
||||||
'4': OpsgeniePriority.HIGH,
|
|
||||||
'5': OpsgeniePriority.EMERGENCY,
|
|
||||||
'p1': OpsgeniePriority.LOW,
|
|
||||||
'p2': OpsgeniePriority.MODERATE,
|
|
||||||
'p3': OpsgeniePriority.NORMAL,
|
|
||||||
'p4': OpsgeniePriority.HIGH,
|
|
||||||
'p5': OpsgeniePriority.EMERGENCY,
|
|
||||||
}
|
|
||||||
try:
|
|
||||||
results['priority'] = \
|
|
||||||
_map[results['qsd']['priority'][0:2].lower()]
|
|
||||||
|
|
||||||
except KeyError:
|
|
||||||
# No priority was set
|
|
||||||
pass
|
|
||||||
|
|
||||||
# Get Batch Boolean (if set)
|
# Get Batch Boolean (if set)
|
||||||
results['batch'] = \
|
results['batch'] = \
|
||||||
|
|
500
libs/apprise/plugins/NotifyPagerDuty.py
Normal file
500
libs/apprise/plugins/NotifyPagerDuty.py
Normal file
|
@ -0,0 +1,500 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
#
|
||||||
|
# Copyright (C) 2022 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.
|
||||||
|
|
||||||
|
# API Refererence:
|
||||||
|
# - https://developer.pagerduty.com/api-reference/\
|
||||||
|
# 368ae3d938c9e-send-an-event-to-pager-duty
|
||||||
|
#
|
||||||
|
|
||||||
|
import requests
|
||||||
|
from json import dumps
|
||||||
|
|
||||||
|
from .NotifyBase import NotifyBase
|
||||||
|
from ..URLBase import PrivacyMode
|
||||||
|
from ..common import NotifyType
|
||||||
|
from ..common import NotifyImageSize
|
||||||
|
from ..utils import validate_regex
|
||||||
|
from ..utils import parse_bool
|
||||||
|
from ..AppriseLocale import gettext_lazy as _
|
||||||
|
|
||||||
|
|
||||||
|
class PagerDutySeverity:
|
||||||
|
"""
|
||||||
|
Defines the Pager Duty Severity Levels
|
||||||
|
"""
|
||||||
|
INFO = 'info'
|
||||||
|
|
||||||
|
WARNING = 'warning'
|
||||||
|
|
||||||
|
ERROR = 'error'
|
||||||
|
|
||||||
|
CRITICAL = 'critical'
|
||||||
|
|
||||||
|
|
||||||
|
# Map all support Apprise Categories with the Pager Duty ones
|
||||||
|
PAGERDUTY_SEVERITY_MAP = {
|
||||||
|
NotifyType.INFO: PagerDutySeverity.INFO,
|
||||||
|
NotifyType.SUCCESS: PagerDutySeverity.INFO,
|
||||||
|
NotifyType.WARNING: PagerDutySeverity.WARNING,
|
||||||
|
NotifyType.FAILURE: PagerDutySeverity.CRITICAL,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# Priorities
|
||||||
|
class PagerDutyRegion:
|
||||||
|
US = 'us'
|
||||||
|
EU = 'eu'
|
||||||
|
|
||||||
|
|
||||||
|
# SparkPost APIs
|
||||||
|
PAGERDUTY_API_LOOKUP = {
|
||||||
|
PagerDutyRegion.US: 'https://events.pagerduty.com/v2/enqueue',
|
||||||
|
PagerDutyRegion.EU: 'https://events.eu.pagerduty.com/v2/enqueue',
|
||||||
|
}
|
||||||
|
|
||||||
|
# A List of our regions we can use for verification
|
||||||
|
PAGERDUTY_REGIONS = (
|
||||||
|
PagerDutyRegion.US,
|
||||||
|
PagerDutyRegion.EU,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class NotifyPagerDuty(NotifyBase):
|
||||||
|
"""
|
||||||
|
A wrapper for Pager Duty Notifications
|
||||||
|
"""
|
||||||
|
|
||||||
|
# The default descriptive name associated with the Notification
|
||||||
|
service_name = 'Pager Duty'
|
||||||
|
|
||||||
|
# The services URL
|
||||||
|
service_url = 'https://pagerduty.com/'
|
||||||
|
|
||||||
|
# Secure Protocol
|
||||||
|
secure_protocol = 'pagerduty'
|
||||||
|
|
||||||
|
# A URL that takes you to the setup/help of the specific protocol
|
||||||
|
setup_url = 'https://github.com/caronc/apprise/wiki/Notify_pagerduty'
|
||||||
|
|
||||||
|
# We don't support titles for Pager Duty notifications
|
||||||
|
title_maxlen = 0
|
||||||
|
|
||||||
|
# Allows the user to specify the NotifyImageSize object; this is supported
|
||||||
|
# through the webhook
|
||||||
|
image_size = NotifyImageSize.XY_128
|
||||||
|
|
||||||
|
# Our event action type
|
||||||
|
event_action = 'trigger'
|
||||||
|
|
||||||
|
# The default region to use if one isn't otherwise specified
|
||||||
|
default_region = PagerDutyRegion.US
|
||||||
|
|
||||||
|
# Define object templates
|
||||||
|
templates = (
|
||||||
|
'{schema}://{integrationkey}@{apikey}',
|
||||||
|
'{schema}://{integrationkey}@{apikey}/{source}',
|
||||||
|
'{schema}://{integrationkey}@{apikey}/{source}/{component}',
|
||||||
|
)
|
||||||
|
|
||||||
|
# Define our template tokens
|
||||||
|
template_tokens = dict(NotifyBase.template_tokens, **{
|
||||||
|
'apikey': {
|
||||||
|
'name': _('API Key'),
|
||||||
|
'type': 'string',
|
||||||
|
'private': True,
|
||||||
|
'required': True
|
||||||
|
},
|
||||||
|
# Optional but triggers V2 API
|
||||||
|
'integrationkey': {
|
||||||
|
'name': _('Routing Key'),
|
||||||
|
'type': 'string',
|
||||||
|
'private': True,
|
||||||
|
'required': True
|
||||||
|
},
|
||||||
|
'source': {
|
||||||
|
# Optional Source Identifier (preferably a FQDN)
|
||||||
|
'name': _('Source'),
|
||||||
|
'type': 'string',
|
||||||
|
'default': 'Apprise',
|
||||||
|
},
|
||||||
|
'component': {
|
||||||
|
# Optional Component Identifier
|
||||||
|
'name': _('Component'),
|
||||||
|
'type': 'string',
|
||||||
|
'default': 'Notification',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
# Define our template arguments
|
||||||
|
template_args = dict(NotifyBase.template_args, **{
|
||||||
|
'group': {
|
||||||
|
'name': _('Group'),
|
||||||
|
'type': 'string',
|
||||||
|
},
|
||||||
|
'class': {
|
||||||
|
'name': _('Class'),
|
||||||
|
'type': 'string',
|
||||||
|
'map_to': 'class_id',
|
||||||
|
},
|
||||||
|
'click': {
|
||||||
|
'name': _('Click'),
|
||||||
|
'type': 'string',
|
||||||
|
},
|
||||||
|
'region': {
|
||||||
|
'name': _('Region Name'),
|
||||||
|
'type': 'choice:string',
|
||||||
|
'values': PAGERDUTY_REGIONS,
|
||||||
|
'default': PagerDutyRegion.US,
|
||||||
|
'map_to': 'region_name',
|
||||||
|
},
|
||||||
|
'image': {
|
||||||
|
'name': _('Include Image'),
|
||||||
|
'type': 'bool',
|
||||||
|
'default': True,
|
||||||
|
'map_to': 'include_image',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
# Define any kwargs we're using
|
||||||
|
template_kwargs = {
|
||||||
|
'details': {
|
||||||
|
'name': _('Custom Details'),
|
||||||
|
'prefix': '+',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
def __init__(self, apikey, integrationkey=None, source=None,
|
||||||
|
component=None, group=None, class_id=None,
|
||||||
|
include_image=True, click=None, details=None,
|
||||||
|
region_name=None, **kwargs):
|
||||||
|
"""
|
||||||
|
Initialize Pager Duty Object
|
||||||
|
"""
|
||||||
|
super(NotifyPagerDuty, self).__init__(**kwargs)
|
||||||
|
|
||||||
|
# Long-Lived Access token (generated from User Profile)
|
||||||
|
self.apikey = validate_regex(apikey)
|
||||||
|
if not self.apikey:
|
||||||
|
msg = 'An invalid Pager Duty API Key ' \
|
||||||
|
'({}) was specified.'.format(apikey)
|
||||||
|
self.logger.warning(msg)
|
||||||
|
raise TypeError(msg)
|
||||||
|
|
||||||
|
self.integration_key = validate_regex(integrationkey)
|
||||||
|
if not self.integration_key:
|
||||||
|
msg = 'An invalid Pager Duty Routing Key ' \
|
||||||
|
'({}) was specified.'.format(integrationkey)
|
||||||
|
self.logger.warning(msg)
|
||||||
|
raise TypeError(msg)
|
||||||
|
|
||||||
|
# An Optional Source
|
||||||
|
self.source = self.template_tokens['source']['default']
|
||||||
|
if source:
|
||||||
|
self.source = validate_regex(source)
|
||||||
|
if not self.source:
|
||||||
|
msg = 'An invalid Pager Duty Notification Source ' \
|
||||||
|
'({}) was specified.'.format(source)
|
||||||
|
self.logger.warning(msg)
|
||||||
|
raise TypeError(msg)
|
||||||
|
else:
|
||||||
|
self.component = self.template_tokens['source']['default']
|
||||||
|
|
||||||
|
# An Optional Component
|
||||||
|
self.component = self.template_tokens['component']['default']
|
||||||
|
if component:
|
||||||
|
self.component = validate_regex(component)
|
||||||
|
if not self.component:
|
||||||
|
msg = 'An invalid Pager Duty Notification Component ' \
|
||||||
|
'({}) was specified.'.format(component)
|
||||||
|
self.logger.warning(msg)
|
||||||
|
raise TypeError(msg)
|
||||||
|
else:
|
||||||
|
self.component = self.template_tokens['component']['default']
|
||||||
|
|
||||||
|
# Store our region
|
||||||
|
try:
|
||||||
|
self.region_name = self.default_region \
|
||||||
|
if region_name is None else region_name.lower()
|
||||||
|
|
||||||
|
if self.region_name not in PAGERDUTY_REGIONS:
|
||||||
|
# allow the outer except to handle this common response
|
||||||
|
raise
|
||||||
|
except:
|
||||||
|
# Invalid region specified
|
||||||
|
msg = 'The PagerDuty region specified ({}) is invalid.' \
|
||||||
|
.format(region_name)
|
||||||
|
self.logger.warning(msg)
|
||||||
|
raise TypeError(msg)
|
||||||
|
|
||||||
|
# A clickthrough option for notifications
|
||||||
|
self.click = click
|
||||||
|
|
||||||
|
# Store Class ID if specified
|
||||||
|
self.class_id = class_id
|
||||||
|
|
||||||
|
# Store Group if specified
|
||||||
|
self.group = group
|
||||||
|
|
||||||
|
self.details = {}
|
||||||
|
if details:
|
||||||
|
# Store our extra details
|
||||||
|
self.details.update(details)
|
||||||
|
|
||||||
|
# Display our Apprise Image
|
||||||
|
self.include_image = include_image
|
||||||
|
|
||||||
|
return
|
||||||
|
|
||||||
|
def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs):
|
||||||
|
"""
|
||||||
|
Send our PagerDuty Notification
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Prepare our headers
|
||||||
|
headers = {
|
||||||
|
'User-Agent': self.app_id,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': 'Token token={}'.format(self.apikey),
|
||||||
|
}
|
||||||
|
|
||||||
|
# Prepare our persistent_notification.create payload
|
||||||
|
payload = {
|
||||||
|
# Define our integration key
|
||||||
|
'routing_key': self.integration_key,
|
||||||
|
|
||||||
|
# Prepare our payload
|
||||||
|
'payload': {
|
||||||
|
'summary': body,
|
||||||
|
|
||||||
|
# Set our severity
|
||||||
|
'severity': PAGERDUTY_SEVERITY_MAP[notify_type],
|
||||||
|
|
||||||
|
|
||||||
|
# Our Alerting Source/Component
|
||||||
|
'source': self.source,
|
||||||
|
'component': self.component,
|
||||||
|
},
|
||||||
|
'client': self.app_id,
|
||||||
|
# Our Event Action
|
||||||
|
'event_action': self.event_action,
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.group:
|
||||||
|
payload['payload']['group'] = self.group
|
||||||
|
|
||||||
|
if self.class_id:
|
||||||
|
payload['payload']['class'] = self.class_id
|
||||||
|
|
||||||
|
if self.click:
|
||||||
|
payload['links'] = [{
|
||||||
|
"href": self.click,
|
||||||
|
}]
|
||||||
|
|
||||||
|
# Acquire our image url if configured to do so
|
||||||
|
image_url = None if not self.include_image else \
|
||||||
|
self.image_url(notify_type)
|
||||||
|
|
||||||
|
if image_url:
|
||||||
|
payload['images'] = [{
|
||||||
|
'src': image_url,
|
||||||
|
'alt': notify_type,
|
||||||
|
}]
|
||||||
|
|
||||||
|
if self.details:
|
||||||
|
payload['payload']['custom_details'] = {}
|
||||||
|
# Apply any provided custom details
|
||||||
|
for k, v in self.details.items():
|
||||||
|
payload['payload']['custom_details'][k] = v
|
||||||
|
|
||||||
|
# Prepare our URL based on region
|
||||||
|
notify_url = PAGERDUTY_API_LOOKUP[self.region_name]
|
||||||
|
|
||||||
|
self.logger.debug('Pager Duty POST URL: %s (cert_verify=%r)' % (
|
||||||
|
notify_url, self.verify_certificate,
|
||||||
|
))
|
||||||
|
self.logger.debug('Pager Duty Payload: %s' % str(payload))
|
||||||
|
|
||||||
|
# Always call throttle before any remote server i/o is made
|
||||||
|
self.throttle()
|
||||||
|
|
||||||
|
try:
|
||||||
|
r = requests.post(
|
||||||
|
notify_url,
|
||||||
|
data=dumps(payload),
|
||||||
|
headers=headers,
|
||||||
|
verify=self.verify_certificate,
|
||||||
|
timeout=self.request_timeout,
|
||||||
|
)
|
||||||
|
if r.status_code not in (
|
||||||
|
requests.codes.ok, requests.codes.created,
|
||||||
|
requests.codes.accepted):
|
||||||
|
# We had a problem
|
||||||
|
status_str = \
|
||||||
|
NotifyPagerDuty.http_response_code_lookup(
|
||||||
|
r.status_code)
|
||||||
|
|
||||||
|
self.logger.warning(
|
||||||
|
'Failed to send Pager Duty notification: '
|
||||||
|
'{}{}error={}.'.format(
|
||||||
|
status_str,
|
||||||
|
', ' if status_str else '',
|
||||||
|
r.status_code))
|
||||||
|
|
||||||
|
self.logger.debug('Response Details:\r\n{}'.format(r.content))
|
||||||
|
|
||||||
|
# Return; we're done
|
||||||
|
return False
|
||||||
|
|
||||||
|
else:
|
||||||
|
self.logger.info('Sent Pager Duty notification.')
|
||||||
|
|
||||||
|
except requests.RequestException as e:
|
||||||
|
self.logger.warning(
|
||||||
|
'A Connection error occurred sending Pager Duty '
|
||||||
|
'notification to %s.' % self.host)
|
||||||
|
self.logger.debug('Socket 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.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Define any URL parameters
|
||||||
|
params = {
|
||||||
|
'region': self.region_name,
|
||||||
|
'image': 'yes' if self.include_image else 'no',
|
||||||
|
}
|
||||||
|
if self.class_id:
|
||||||
|
params['class'] = self.class_id
|
||||||
|
|
||||||
|
if self.group:
|
||||||
|
params['group'] = self.group
|
||||||
|
|
||||||
|
if self.click is not None:
|
||||||
|
params['click'] = self.click
|
||||||
|
|
||||||
|
# Append our custom entries our parameters
|
||||||
|
params.update({'+{}'.format(k): v for k, v in self.details.items()})
|
||||||
|
|
||||||
|
# Extend our parameters
|
||||||
|
params.update(self.url_parameters(privacy=privacy, *args, **kwargs))
|
||||||
|
|
||||||
|
url = '{schema}://{integration_key}@{apikey}/' \
|
||||||
|
'{source}/{component}?{params}'
|
||||||
|
|
||||||
|
return url.format(
|
||||||
|
schema=self.secure_protocol,
|
||||||
|
# never encode hostname since we're expecting it to be a valid one
|
||||||
|
integration_key=self.pprint(
|
||||||
|
self.integration_key, privacy, mode=PrivacyMode.Secret,
|
||||||
|
safe=''),
|
||||||
|
apikey=self.pprint(
|
||||||
|
self.apikey, privacy, mode=PrivacyMode.Secret, safe=''),
|
||||||
|
source=self.pprint(
|
||||||
|
self.source, privacy, safe=''),
|
||||||
|
component=self.pprint(
|
||||||
|
self.component, privacy, safe=''),
|
||||||
|
params=NotifyPagerDuty.urlencode(params),
|
||||||
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def parse_url(url):
|
||||||
|
"""
|
||||||
|
Parses the URL and returns enough arguments that can allow
|
||||||
|
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
|
||||||
|
|
||||||
|
# The 'apikey' makes it easier to use yaml configuration
|
||||||
|
if 'apikey' in results['qsd'] and len(results['qsd']['apikey']):
|
||||||
|
results['apikey'] = \
|
||||||
|
NotifyPagerDuty.unquote(results['qsd']['apikey'])
|
||||||
|
else:
|
||||||
|
results['apikey'] = NotifyPagerDuty.unquote(results['host'])
|
||||||
|
|
||||||
|
# The 'integrationkey' makes it easier to use yaml configuration
|
||||||
|
if 'integrationkey' in results['qsd'] and \
|
||||||
|
len(results['qsd']['integrationkey']):
|
||||||
|
results['integrationkey'] = \
|
||||||
|
NotifyPagerDuty.unquote(results['qsd']['integrationkey'])
|
||||||
|
else:
|
||||||
|
results['integrationkey'] = \
|
||||||
|
NotifyPagerDuty.unquote(results['user'])
|
||||||
|
|
||||||
|
if 'click' in results['qsd'] and len(results['qsd']['click']):
|
||||||
|
results['click'] = NotifyPagerDuty.unquote(results['qsd']['click'])
|
||||||
|
|
||||||
|
if 'group' in results['qsd'] and len(results['qsd']['group']):
|
||||||
|
results['group'] = \
|
||||||
|
NotifyPagerDuty.unquote(results['qsd']['group'])
|
||||||
|
|
||||||
|
if 'class' in results['qsd'] and len(results['qsd']['class']):
|
||||||
|
results['class_id'] = \
|
||||||
|
NotifyPagerDuty.unquote(results['qsd']['class'])
|
||||||
|
|
||||||
|
# Acquire our full path
|
||||||
|
fullpath = NotifyPagerDuty.split_path(results['fullpath'])
|
||||||
|
|
||||||
|
# Get our source
|
||||||
|
if 'source' in results['qsd'] and len(results['qsd']['source']):
|
||||||
|
results['source'] = \
|
||||||
|
NotifyPagerDuty.unquote(results['qsd']['source'])
|
||||||
|
else:
|
||||||
|
results['source'] = fullpath.pop(0) if fullpath else None
|
||||||
|
|
||||||
|
# Get our component
|
||||||
|
if 'component' in results['qsd'] and len(results['qsd']['component']):
|
||||||
|
results['component'] = \
|
||||||
|
NotifyPagerDuty.unquote(results['qsd']['component'])
|
||||||
|
else:
|
||||||
|
results['component'] = fullpath.pop(0) if fullpath else None
|
||||||
|
|
||||||
|
# Add our custom details key/value pairs that the user can potentially
|
||||||
|
# over-ride if they wish to to our returned result set and tidy
|
||||||
|
# entries by unquoting them
|
||||||
|
results['details'] = {
|
||||||
|
NotifyPagerDuty.unquote(x): NotifyPagerDuty.unquote(y)
|
||||||
|
for x, y in results['qsd+'].items()}
|
||||||
|
|
||||||
|
if 'region' in results['qsd'] and len(results['qsd']['region']):
|
||||||
|
# Extract from name to associate with from address
|
||||||
|
results['region_name'] = \
|
||||||
|
NotifyPagerDuty.unquote(results['qsd']['region'])
|
||||||
|
|
||||||
|
# Include images with our message
|
||||||
|
results['include_image'] = \
|
||||||
|
parse_bool(results['qsd'].get('image', True))
|
||||||
|
|
||||||
|
return results
|
|
@ -26,7 +26,6 @@
|
||||||
# Official API reference: https://developer.gitter.im/docs/user-resource
|
# Official API reference: https://developer.gitter.im/docs/user-resource
|
||||||
|
|
||||||
import re
|
import re
|
||||||
import six
|
|
||||||
import requests
|
import requests
|
||||||
from json import dumps
|
from json import dumps
|
||||||
|
|
||||||
|
@ -40,7 +39,7 @@ TARGET_LIST_DELIM = re.compile(r'[ \t\r\n,\\/]+')
|
||||||
|
|
||||||
|
|
||||||
# Priorities
|
# Priorities
|
||||||
class ParsePlatformDevice(object):
|
class ParsePlatformDevice:
|
||||||
# All Devices
|
# All Devices
|
||||||
ALL = 'all'
|
ALL = 'all'
|
||||||
|
|
||||||
|
@ -134,7 +133,7 @@ class NotifyParsePlatform(NotifyBase):
|
||||||
super(NotifyParsePlatform, self).__init__(**kwargs)
|
super(NotifyParsePlatform, self).__init__(**kwargs)
|
||||||
|
|
||||||
self.fullpath = kwargs.get('fullpath')
|
self.fullpath = kwargs.get('fullpath')
|
||||||
if not isinstance(self.fullpath, six.string_types):
|
if not isinstance(self.fullpath, str):
|
||||||
self.fullpath = '/'
|
self.fullpath = '/'
|
||||||
|
|
||||||
# Application ID
|
# Application ID
|
||||||
|
|
|
@ -32,7 +32,7 @@ from ..AppriseLocale import gettext_lazy as _
|
||||||
|
|
||||||
|
|
||||||
# Priorities
|
# Priorities
|
||||||
class ProwlPriority(object):
|
class ProwlPriority:
|
||||||
LOW = -2
|
LOW = -2
|
||||||
MODERATE = -1
|
MODERATE = -1
|
||||||
NORMAL = 0
|
NORMAL = 0
|
||||||
|
@ -40,13 +40,34 @@ class ProwlPriority(object):
|
||||||
EMERGENCY = 2
|
EMERGENCY = 2
|
||||||
|
|
||||||
|
|
||||||
PROWL_PRIORITIES = (
|
PROWL_PRIORITIES = {
|
||||||
ProwlPriority.LOW,
|
# Note: This also acts as a reverse lookup mapping
|
||||||
ProwlPriority.MODERATE,
|
ProwlPriority.LOW: 'low',
|
||||||
ProwlPriority.NORMAL,
|
ProwlPriority.MODERATE: 'moderate',
|
||||||
ProwlPriority.HIGH,
|
ProwlPriority.NORMAL: 'normal',
|
||||||
ProwlPriority.EMERGENCY,
|
ProwlPriority.HIGH: 'high',
|
||||||
)
|
ProwlPriority.EMERGENCY: 'emergency',
|
||||||
|
}
|
||||||
|
|
||||||
|
PROWL_PRIORITY_MAP = {
|
||||||
|
# Maps against string 'low'
|
||||||
|
'l': ProwlPriority.LOW,
|
||||||
|
# Maps against string 'moderate'
|
||||||
|
'm': ProwlPriority.MODERATE,
|
||||||
|
# Maps against string 'normal'
|
||||||
|
'n': ProwlPriority.NORMAL,
|
||||||
|
# Maps against string 'high'
|
||||||
|
'h': ProwlPriority.HIGH,
|
||||||
|
# Maps against string 'emergency'
|
||||||
|
'e': ProwlPriority.EMERGENCY,
|
||||||
|
|
||||||
|
# Entries to additionally support (so more like Prowl's API)
|
||||||
|
'-2': ProwlPriority.LOW,
|
||||||
|
'-1': ProwlPriority.MODERATE,
|
||||||
|
'0': ProwlPriority.NORMAL,
|
||||||
|
'1': ProwlPriority.HIGH,
|
||||||
|
'2': ProwlPriority.EMERGENCY,
|
||||||
|
}
|
||||||
|
|
||||||
# Provide some known codes Prowl uses and what they translate to:
|
# Provide some known codes Prowl uses and what they translate to:
|
||||||
PROWL_HTTP_ERROR_MAP = {
|
PROWL_HTTP_ERROR_MAP = {
|
||||||
|
@ -124,11 +145,13 @@ class NotifyProwl(NotifyBase):
|
||||||
"""
|
"""
|
||||||
super(NotifyProwl, self).__init__(**kwargs)
|
super(NotifyProwl, self).__init__(**kwargs)
|
||||||
|
|
||||||
if priority not in PROWL_PRIORITIES:
|
# The Priority of the message
|
||||||
self.priority = self.template_args['priority']['default']
|
self.priority = NotifyProwl.template_args['priority']['default'] \
|
||||||
|
if not priority else \
|
||||||
else:
|
next((
|
||||||
self.priority = priority
|
v for k, v in PROWL_PRIORITY_MAP.items()
|
||||||
|
if str(priority).lower().startswith(k)),
|
||||||
|
NotifyProwl.template_args['priority']['default'])
|
||||||
|
|
||||||
# API Key (associated with project)
|
# API Key (associated with project)
|
||||||
self.apikey = validate_regex(
|
self.apikey = validate_regex(
|
||||||
|
@ -229,18 +252,12 @@ class NotifyProwl(NotifyBase):
|
||||||
Returns the URL built dynamically based on specified arguments.
|
Returns the URL built dynamically based on specified arguments.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
_map = {
|
|
||||||
ProwlPriority.LOW: 'low',
|
|
||||||
ProwlPriority.MODERATE: 'moderate',
|
|
||||||
ProwlPriority.NORMAL: 'normal',
|
|
||||||
ProwlPriority.HIGH: 'high',
|
|
||||||
ProwlPriority.EMERGENCY: 'emergency',
|
|
||||||
}
|
|
||||||
|
|
||||||
# Define any URL parameters
|
# Define any URL parameters
|
||||||
params = {
|
params = {
|
||||||
'priority': 'normal' if self.priority not in _map
|
'priority':
|
||||||
else _map[self.priority],
|
PROWL_PRIORITIES[self.template_args['priority']['default']]
|
||||||
|
if self.priority not in PROWL_PRIORITIES
|
||||||
|
else PROWL_PRIORITIES[self.priority],
|
||||||
}
|
}
|
||||||
|
|
||||||
# Extend our parameters
|
# Extend our parameters
|
||||||
|
@ -276,32 +293,9 @@ class NotifyProwl(NotifyBase):
|
||||||
except IndexError:
|
except IndexError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
# Set our priority
|
||||||
if 'priority' in results['qsd'] and len(results['qsd']['priority']):
|
if 'priority' in results['qsd'] and len(results['qsd']['priority']):
|
||||||
_map = {
|
results['priority'] = \
|
||||||
# Letter Assignments
|
NotifyProwl.unquote(results['qsd']['priority'])
|
||||||
'l': ProwlPriority.LOW,
|
|
||||||
'm': ProwlPriority.MODERATE,
|
|
||||||
'n': ProwlPriority.NORMAL,
|
|
||||||
'h': ProwlPriority.HIGH,
|
|
||||||
'e': ProwlPriority.EMERGENCY,
|
|
||||||
'lo': ProwlPriority.LOW,
|
|
||||||
'me': ProwlPriority.MODERATE,
|
|
||||||
'no': ProwlPriority.NORMAL,
|
|
||||||
'hi': ProwlPriority.HIGH,
|
|
||||||
'em': ProwlPriority.EMERGENCY,
|
|
||||||
# Support 3rd Party Documented Scale
|
|
||||||
'-2': ProwlPriority.LOW,
|
|
||||||
'-1': ProwlPriority.MODERATE,
|
|
||||||
'0': ProwlPriority.NORMAL,
|
|
||||||
'1': ProwlPriority.HIGH,
|
|
||||||
'2': ProwlPriority.EMERGENCY,
|
|
||||||
}
|
|
||||||
try:
|
|
||||||
results['priority'] = \
|
|
||||||
_map[results['qsd']['priority'][0:2].lower()]
|
|
||||||
|
|
||||||
except KeyError:
|
|
||||||
# No priority was set
|
|
||||||
pass
|
|
||||||
|
|
||||||
return results
|
return results
|
||||||
|
|
|
@ -23,8 +23,6 @@
|
||||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
# THE SOFTWARE.
|
# THE SOFTWARE.
|
||||||
|
|
||||||
# We use io because it allows us to test the open() call
|
|
||||||
import io
|
|
||||||
import base64
|
import base64
|
||||||
import requests
|
import requests
|
||||||
from json import loads
|
from json import loads
|
||||||
|
@ -36,7 +34,7 @@ from ..utils import validate_regex
|
||||||
from ..AppriseLocale import gettext_lazy as _
|
from ..AppriseLocale import gettext_lazy as _
|
||||||
|
|
||||||
|
|
||||||
class PushSaferSound(object):
|
class PushSaferSound:
|
||||||
"""
|
"""
|
||||||
Defines all of the supported PushSafe sounds
|
Defines all of the supported PushSafe sounds
|
||||||
"""
|
"""
|
||||||
|
@ -248,7 +246,7 @@ PUSHSAFER_SOUND_MAP = {
|
||||||
|
|
||||||
|
|
||||||
# Priorities
|
# Priorities
|
||||||
class PushSaferPriority(object):
|
class PushSaferPriority:
|
||||||
LOW = -2
|
LOW = -2
|
||||||
MODERATE = -1
|
MODERATE = -1
|
||||||
NORMAL = 0
|
NORMAL = 0
|
||||||
|
@ -282,7 +280,7 @@ DEFAULT_PRIORITY = "normal"
|
||||||
|
|
||||||
|
|
||||||
# Vibrations
|
# Vibrations
|
||||||
class PushSaferVibration(object):
|
class PushSaferVibration:
|
||||||
"""
|
"""
|
||||||
Defines the acceptable vibration settings for notification
|
Defines the acceptable vibration settings for notification
|
||||||
"""
|
"""
|
||||||
|
@ -565,7 +563,7 @@ class NotifyPushSafer(NotifyBase):
|
||||||
attachment.url(privacy=True)))
|
attachment.url(privacy=True)))
|
||||||
|
|
||||||
try:
|
try:
|
||||||
with io.open(attachment.path, 'rb') as f:
|
with open(attachment.path, 'rb') as f:
|
||||||
# Output must be in a DataURL format (that's what
|
# Output must be in a DataURL format (that's what
|
||||||
# PushSafer calls it):
|
# PushSafer calls it):
|
||||||
attachment = (
|
attachment = (
|
||||||
|
|
|
@ -24,12 +24,12 @@
|
||||||
# THE SOFTWARE.
|
# THE SOFTWARE.
|
||||||
|
|
||||||
import re
|
import re
|
||||||
import six
|
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
from .NotifyBase import NotifyBase
|
from .NotifyBase import NotifyBase
|
||||||
from ..common import NotifyType
|
from ..common import NotifyType
|
||||||
from ..common import NotifyFormat
|
from ..common import NotifyFormat
|
||||||
|
from ..conversion import convert_between
|
||||||
from ..utils import parse_list
|
from ..utils import parse_list
|
||||||
from ..utils import validate_regex
|
from ..utils import validate_regex
|
||||||
from ..AppriseLocale import gettext_lazy as _
|
from ..AppriseLocale import gettext_lazy as _
|
||||||
|
@ -43,7 +43,7 @@ VALIDATE_DEVICE = re.compile(r'^[a-z0-9_]{1,25}$', re.I)
|
||||||
|
|
||||||
|
|
||||||
# Priorities
|
# Priorities
|
||||||
class PushoverPriority(object):
|
class PushoverPriority:
|
||||||
LOW = -2
|
LOW = -2
|
||||||
MODERATE = -1
|
MODERATE = -1
|
||||||
NORMAL = 0
|
NORMAL = 0
|
||||||
|
@ -52,7 +52,7 @@ class PushoverPriority(object):
|
||||||
|
|
||||||
|
|
||||||
# Sounds
|
# Sounds
|
||||||
class PushoverSound(object):
|
class PushoverSound:
|
||||||
PUSHOVER = 'pushover'
|
PUSHOVER = 'pushover'
|
||||||
BIKE = 'bike'
|
BIKE = 'bike'
|
||||||
BUGLE = 'bugle'
|
BUGLE = 'bugle'
|
||||||
|
@ -102,13 +102,34 @@ PUSHOVER_SOUNDS = (
|
||||||
PushoverSound.NONE,
|
PushoverSound.NONE,
|
||||||
)
|
)
|
||||||
|
|
||||||
PUSHOVER_PRIORITIES = (
|
PUSHOVER_PRIORITIES = {
|
||||||
PushoverPriority.LOW,
|
# Note: This also acts as a reverse lookup mapping
|
||||||
PushoverPriority.MODERATE,
|
PushoverPriority.LOW: 'low',
|
||||||
PushoverPriority.NORMAL,
|
PushoverPriority.MODERATE: 'moderate',
|
||||||
PushoverPriority.HIGH,
|
PushoverPriority.NORMAL: 'normal',
|
||||||
PushoverPriority.EMERGENCY,
|
PushoverPriority.HIGH: 'high',
|
||||||
)
|
PushoverPriority.EMERGENCY: 'emergency',
|
||||||
|
}
|
||||||
|
|
||||||
|
PUSHOVER_PRIORITY_MAP = {
|
||||||
|
# Maps against string 'low'
|
||||||
|
'l': PushoverPriority.LOW,
|
||||||
|
# Maps against string 'moderate'
|
||||||
|
'm': PushoverPriority.MODERATE,
|
||||||
|
# Maps against string 'normal'
|
||||||
|
'n': PushoverPriority.NORMAL,
|
||||||
|
# Maps against string 'high'
|
||||||
|
'h': PushoverPriority.HIGH,
|
||||||
|
# Maps against string 'emergency'
|
||||||
|
'e': PushoverPriority.EMERGENCY,
|
||||||
|
|
||||||
|
# Entries to additionally support (so more like Pushover's API)
|
||||||
|
'-2': PushoverPriority.LOW,
|
||||||
|
'-1': PushoverPriority.MODERATE,
|
||||||
|
'0': PushoverPriority.NORMAL,
|
||||||
|
'1': PushoverPriority.HIGH,
|
||||||
|
'2': PushoverPriority.EMERGENCY,
|
||||||
|
}
|
||||||
|
|
||||||
# Extend HTTP Error Messages
|
# Extend HTTP Error Messages
|
||||||
PUSHOVER_HTTP_ERROR_MAP = {
|
PUSHOVER_HTTP_ERROR_MAP = {
|
||||||
|
@ -258,18 +279,20 @@ class NotifyPushover(NotifyBase):
|
||||||
|
|
||||||
# Setup our sound
|
# Setup our sound
|
||||||
self.sound = NotifyPushover.default_pushover_sound \
|
self.sound = NotifyPushover.default_pushover_sound \
|
||||||
if not isinstance(sound, six.string_types) else sound.lower()
|
if not isinstance(sound, str) else sound.lower()
|
||||||
if self.sound and self.sound not in PUSHOVER_SOUNDS:
|
if self.sound and self.sound not in PUSHOVER_SOUNDS:
|
||||||
msg = 'The sound specified ({}) is invalid.'.format(sound)
|
msg = 'The sound specified ({}) is invalid.'.format(sound)
|
||||||
self.logger.warning(msg)
|
self.logger.warning(msg)
|
||||||
raise TypeError(msg)
|
raise TypeError(msg)
|
||||||
|
|
||||||
# The Priority of the message
|
# The Priority of the message
|
||||||
if priority not in PUSHOVER_PRIORITIES:
|
self.priority = int(
|
||||||
self.priority = self.template_args['priority']['default']
|
NotifyPushover.template_args['priority']['default']
|
||||||
|
if priority is None else
|
||||||
else:
|
next((
|
||||||
self.priority = priority
|
v for k, v in PUSHOVER_PRIORITY_MAP.items()
|
||||||
|
if str(priority).lower().startswith(k)),
|
||||||
|
NotifyPushover.template_args['priority']['default']))
|
||||||
|
|
||||||
# The following are for emergency alerts
|
# The following are for emergency alerts
|
||||||
if self.priority == PushoverPriority.EMERGENCY:
|
if self.priority == PushoverPriority.EMERGENCY:
|
||||||
|
@ -344,6 +367,10 @@ class NotifyPushover(NotifyBase):
|
||||||
if self.notify_format == NotifyFormat.HTML:
|
if self.notify_format == NotifyFormat.HTML:
|
||||||
# https://pushover.net/api#html
|
# https://pushover.net/api#html
|
||||||
payload['html'] = 1
|
payload['html'] = 1
|
||||||
|
elif self.notify_format == NotifyFormat.MARKDOWN:
|
||||||
|
payload['message'] = convert_between(
|
||||||
|
NotifyFormat.MARKDOWN, NotifyFormat.HTML, body)
|
||||||
|
payload['html'] = 1
|
||||||
|
|
||||||
if self.priority == PushoverPriority.EMERGENCY:
|
if self.priority == PushoverPriority.EMERGENCY:
|
||||||
payload.update({'retry': self.retry, 'expire': self.expire})
|
payload.update({'retry': self.retry, 'expire': self.expire})
|
||||||
|
@ -404,24 +431,25 @@ class NotifyPushover(NotifyBase):
|
||||||
attach.mimetype,
|
attach.mimetype,
|
||||||
attach.url(privacy=True)))
|
attach.url(privacy=True)))
|
||||||
|
|
||||||
return True
|
attach = None
|
||||||
|
|
||||||
# If we get here, we're dealing with a supported image.
|
else:
|
||||||
# Verify that the filesize is okay though.
|
# If we get here, we're dealing with a supported image.
|
||||||
file_size = len(attach)
|
# Verify that the filesize is okay though.
|
||||||
if not (file_size > 0
|
file_size = len(attach)
|
||||||
and file_size <= self.attach_max_size_bytes):
|
if not (file_size > 0
|
||||||
|
and file_size <= self.attach_max_size_bytes):
|
||||||
|
|
||||||
# File size is no good
|
# File size is no good
|
||||||
self.logger.warning(
|
self.logger.warning(
|
||||||
'Pushover attachment size ({}B) exceeds limit: {}'
|
'Pushover attachment size ({}B) exceeds limit: {}'
|
||||||
.format(file_size, attach.url(privacy=True)))
|
.format(file_size, attach.url(privacy=True)))
|
||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
self.logger.debug(
|
self.logger.debug(
|
||||||
'Posting Pushover attachment {}'.format(
|
'Posting Pushover attachment {}'.format(
|
||||||
attach.url(privacy=True)))
|
attach.url(privacy=True)))
|
||||||
|
|
||||||
# Default Header
|
# Default Header
|
||||||
headers = {
|
headers = {
|
||||||
|
@ -510,19 +538,12 @@ class NotifyPushover(NotifyBase):
|
||||||
Returns the URL built dynamically based on specified arguments.
|
Returns the URL built dynamically based on specified arguments.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
_map = {
|
|
||||||
PushoverPriority.LOW: 'low',
|
|
||||||
PushoverPriority.MODERATE: 'moderate',
|
|
||||||
PushoverPriority.NORMAL: 'normal',
|
|
||||||
PushoverPriority.HIGH: 'high',
|
|
||||||
PushoverPriority.EMERGENCY: 'emergency',
|
|
||||||
}
|
|
||||||
|
|
||||||
# Define any URL parameters
|
# Define any URL parameters
|
||||||
params = {
|
params = {
|
||||||
'priority':
|
'priority':
|
||||||
_map[self.template_args['priority']['default']]
|
PUSHOVER_PRIORITIES[self.template_args['priority']['default']]
|
||||||
if self.priority not in _map else _map[self.priority],
|
if self.priority not in PUSHOVER_PRIORITIES
|
||||||
|
else PUSHOVER_PRIORITIES[self.priority],
|
||||||
}
|
}
|
||||||
|
|
||||||
# Only add expire and retry for emergency messages,
|
# Only add expire and retry for emergency messages,
|
||||||
|
@ -563,20 +584,8 @@ class NotifyPushover(NotifyBase):
|
||||||
|
|
||||||
# Set our priority
|
# Set our priority
|
||||||
if 'priority' in results['qsd'] and len(results['qsd']['priority']):
|
if 'priority' in results['qsd'] and len(results['qsd']['priority']):
|
||||||
_map = {
|
results['priority'] = \
|
||||||
'l': PushoverPriority.LOW,
|
NotifyPushover.unquote(results['qsd']['priority'])
|
||||||
'm': PushoverPriority.MODERATE,
|
|
||||||
'n': PushoverPriority.NORMAL,
|
|
||||||
'h': PushoverPriority.HIGH,
|
|
||||||
'e': PushoverPriority.EMERGENCY,
|
|
||||||
}
|
|
||||||
try:
|
|
||||||
results['priority'] = \
|
|
||||||
_map[results['qsd']['priority'][0].lower()]
|
|
||||||
|
|
||||||
except KeyError:
|
|
||||||
# No priority was set
|
|
||||||
pass
|
|
||||||
|
|
||||||
# Retrieve all of our targets
|
# Retrieve all of our targets
|
||||||
results['targets'] = NotifyPushover.split_path(results['fullpath'])
|
results['targets'] = NotifyPushover.split_path(results['fullpath'])
|
||||||
|
|
|
@ -44,7 +44,6 @@
|
||||||
# - https://www.reddit.com/dev/api/
|
# - https://www.reddit.com/dev/api/
|
||||||
# - https://www.reddit.com/dev/api/#POST_api_submit
|
# - https://www.reddit.com/dev/api/#POST_api_submit
|
||||||
# - https://github.com/reddit-archive/reddit/wiki/API
|
# - https://github.com/reddit-archive/reddit/wiki/API
|
||||||
import six
|
|
||||||
import requests
|
import requests
|
||||||
from json import loads
|
from json import loads
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
|
@ -66,7 +65,7 @@ REDDIT_HTTP_ERROR_MAP = {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class RedditMessageKind(object):
|
class RedditMessageKind:
|
||||||
"""
|
"""
|
||||||
Define the kinds of messages supported
|
Define the kinds of messages supported
|
||||||
"""
|
"""
|
||||||
|
@ -271,7 +270,7 @@ class NotifyReddit(NotifyBase):
|
||||||
self.__access_token_expiry = datetime.utcnow()
|
self.__access_token_expiry = datetime.utcnow()
|
||||||
|
|
||||||
self.kind = kind.strip().lower() \
|
self.kind = kind.strip().lower() \
|
||||||
if isinstance(kind, six.string_types) \
|
if isinstance(kind, str) \
|
||||||
else self.template_args['kind']['default']
|
else self.template_args['kind']['default']
|
||||||
|
|
||||||
if self.kind not in REDDIT_MESSAGE_KINDS:
|
if self.kind not in REDDIT_MESSAGE_KINDS:
|
||||||
|
|
|
@ -24,7 +24,6 @@
|
||||||
# THE SOFTWARE.
|
# THE SOFTWARE.
|
||||||
|
|
||||||
import re
|
import re
|
||||||
import six
|
|
||||||
import requests
|
import requests
|
||||||
from json import loads
|
from json import loads
|
||||||
from json import dumps
|
from json import dumps
|
||||||
|
@ -54,7 +53,7 @@ RC_HTTP_ERROR_MAP = {
|
||||||
LIST_DELIM = re.compile(r'[ \t\r\n,\\/]+')
|
LIST_DELIM = re.compile(r'[ \t\r\n,\\/]+')
|
||||||
|
|
||||||
|
|
||||||
class RocketChatAuthMode(object):
|
class RocketChatAuthMode:
|
||||||
"""
|
"""
|
||||||
The Chat Authentication mode is detected
|
The Chat Authentication mode is detected
|
||||||
"""
|
"""
|
||||||
|
@ -218,7 +217,7 @@ class NotifyRocketChat(NotifyBase):
|
||||||
|
|
||||||
# Authentication mode
|
# Authentication mode
|
||||||
self.mode = None \
|
self.mode = None \
|
||||||
if not isinstance(mode, six.string_types) \
|
if not isinstance(mode, str) \
|
||||||
else mode.lower()
|
else mode.lower()
|
||||||
|
|
||||||
if self.mode and self.mode not in ROCKETCHAT_AUTH_MODES:
|
if self.mode and self.mode not in ROCKETCHAT_AUTH_MODES:
|
||||||
|
|
|
@ -32,7 +32,6 @@
|
||||||
# These are important <---^----------------------------------------^
|
# These are important <---^----------------------------------------^
|
||||||
#
|
#
|
||||||
import re
|
import re
|
||||||
import six
|
|
||||||
import requests
|
import requests
|
||||||
from json import dumps
|
from json import dumps
|
||||||
|
|
||||||
|
@ -44,7 +43,7 @@ from ..utils import validate_regex
|
||||||
from ..AppriseLocale import gettext_lazy as _
|
from ..AppriseLocale import gettext_lazy as _
|
||||||
|
|
||||||
|
|
||||||
class RyverWebhookMode(object):
|
class RyverWebhookMode:
|
||||||
"""
|
"""
|
||||||
Ryver supports to webhook modes
|
Ryver supports to webhook modes
|
||||||
"""
|
"""
|
||||||
|
@ -152,7 +151,7 @@ class NotifyRyver(NotifyBase):
|
||||||
|
|
||||||
# Store our webhook mode
|
# Store our webhook mode
|
||||||
self.mode = None \
|
self.mode = None \
|
||||||
if not isinstance(mode, six.string_types) else mode.lower()
|
if not isinstance(mode, str) else mode.lower()
|
||||||
|
|
||||||
if self.mode not in RYVER_WEBHOOK_MODES:
|
if self.mode not in RYVER_WEBHOOK_MODES:
|
||||||
msg = 'The Ryver webhook mode specified ({}) is invalid.' \
|
msg = 'The Ryver webhook mode specified ({}) is invalid.' \
|
||||||
|
|
|
@ -89,13 +89,7 @@ from email.mime.application import MIMEApplication
|
||||||
from email.mime.multipart import MIMEMultipart
|
from email.mime.multipart import MIMEMultipart
|
||||||
from email.utils import formataddr
|
from email.utils import formataddr
|
||||||
from email.header import Header
|
from email.header import Header
|
||||||
try:
|
from urllib.parse import quote
|
||||||
# Python v3.x
|
|
||||||
from urllib.parse import quote
|
|
||||||
|
|
||||||
except ImportError:
|
|
||||||
# Python v2.x
|
|
||||||
from urllib import quote
|
|
||||||
|
|
||||||
from .NotifyBase import NotifyBase
|
from .NotifyBase import NotifyBase
|
||||||
from ..URLBase import PrivacyMode
|
from ..URLBase import PrivacyMode
|
||||||
|
@ -395,26 +389,15 @@ class NotifySES(NotifyBase):
|
||||||
# Strip target out of bcc list if in To
|
# Strip target out of bcc list if in To
|
||||||
bcc = (self.bcc - set([to_addr]))
|
bcc = (self.bcc - set([to_addr]))
|
||||||
|
|
||||||
try:
|
# Format our cc addresses to support the Name field
|
||||||
# Format our cc addresses to support the Name field
|
cc = [formataddr(
|
||||||
cc = [formataddr(
|
(self.names.get(addr, False), addr), charset='utf-8')
|
||||||
(self.names.get(addr, False), addr), charset='utf-8')
|
for addr in cc]
|
||||||
for addr in cc]
|
|
||||||
|
|
||||||
# Format our bcc addresses to support the Name field
|
# Format our bcc addresses to support the Name field
|
||||||
bcc = [formataddr(
|
bcc = [formataddr(
|
||||||
(self.names.get(addr, False), addr), charset='utf-8')
|
(self.names.get(addr, False), addr), charset='utf-8')
|
||||||
for addr in bcc]
|
for addr in bcc]
|
||||||
|
|
||||||
except TypeError:
|
|
||||||
# Python v2.x Support (no charset keyword)
|
|
||||||
# Format our cc addresses to support the Name field
|
|
||||||
cc = [formataddr( # pragma: no branch
|
|
||||||
(self.names.get(addr, False), addr)) for addr in cc]
|
|
||||||
|
|
||||||
# Format our bcc addresses to support the Name field
|
|
||||||
bcc = [formataddr( # pragma: no branch
|
|
||||||
(self.names.get(addr, False), addr)) for addr in bcc]
|
|
||||||
|
|
||||||
self.logger.debug('Email From: {} <{}>'.format(
|
self.logger.debug('Email From: {} <{}>'.format(
|
||||||
quote(reply_to[0], ' '),
|
quote(reply_to[0], ' '),
|
||||||
|
@ -436,23 +419,14 @@ class NotifySES(NotifyBase):
|
||||||
# Create a Multipart container if there is an attachment
|
# Create a Multipart container if there is an attachment
|
||||||
base = MIMEMultipart() if attach else content
|
base = MIMEMultipart() if attach else content
|
||||||
|
|
||||||
|
# TODO: Deduplicate with `NotifyEmail`?
|
||||||
base['Subject'] = Header(title, 'utf-8')
|
base['Subject'] = Header(title, 'utf-8')
|
||||||
try:
|
base['From'] = formataddr(
|
||||||
base['From'] = formataddr(
|
(from_name if from_name else False, self.from_addr),
|
||||||
(from_name if from_name else False, self.from_addr),
|
charset='utf-8')
|
||||||
charset='utf-8')
|
base['To'] = formataddr((to_name, to_addr), charset='utf-8')
|
||||||
base['To'] = formataddr((to_name, to_addr), charset='utf-8')
|
if reply_to[1] != self.from_addr:
|
||||||
if reply_to[1] != self.from_addr:
|
base['Reply-To'] = formataddr(reply_to, charset='utf-8')
|
||||||
base['Reply-To'] = formataddr(reply_to, 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))
|
|
||||||
if reply_to[1] != self.from_addr:
|
|
||||||
base['Reply-To'] = formataddr(reply_to)
|
|
||||||
|
|
||||||
base['Cc'] = ','.join(cc)
|
base['Cc'] = ','.join(cc)
|
||||||
base['Date'] = \
|
base['Date'] = \
|
||||||
datetime.utcnow().strftime("%a, %d %b %Y %H:%M:%S +0000")
|
datetime.utcnow().strftime("%a, %d %b %Y %H:%M:%S +0000")
|
||||||
|
|
640
libs/apprise/plugins/NotifySMSEagle.py
Normal file
640
libs/apprise/plugins/NotifySMSEagle.py
Normal file
|
@ -0,0 +1,640 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
#
|
||||||
|
# Copyright (C) 2022 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.
|
||||||
|
|
||||||
|
import re
|
||||||
|
import requests
|
||||||
|
from json import dumps, loads
|
||||||
|
import base64
|
||||||
|
from itertools import chain
|
||||||
|
|
||||||
|
from .NotifyBase import NotifyBase
|
||||||
|
from ..common import NotifyType
|
||||||
|
from ..utils import validate_regex
|
||||||
|
from ..utils import is_phone_no
|
||||||
|
from ..utils import parse_phone_no
|
||||||
|
from ..utils import parse_bool
|
||||||
|
from ..URLBase import PrivacyMode
|
||||||
|
from ..AppriseLocale import gettext_lazy as _
|
||||||
|
|
||||||
|
|
||||||
|
GROUP_REGEX = re.compile(
|
||||||
|
r'^\s*(\#|\%35)(?P<group>[a-z0-9_-]+)', re.I)
|
||||||
|
|
||||||
|
CONTACT_REGEX = re.compile(
|
||||||
|
r'^\s*(\@|\%40)?(?P<contact>[a-z0-9_-]+)', re.I)
|
||||||
|
|
||||||
|
|
||||||
|
# Priorities
|
||||||
|
class SMSEaglePriority:
|
||||||
|
NORMAL = 0
|
||||||
|
HIGH = 1
|
||||||
|
|
||||||
|
|
||||||
|
SMSEAGLE_PRIORITIES = (
|
||||||
|
SMSEaglePriority.NORMAL,
|
||||||
|
SMSEaglePriority.HIGH,
|
||||||
|
)
|
||||||
|
|
||||||
|
SMSEAGLE_PRIORITY_MAP = {
|
||||||
|
# short for 'normal'
|
||||||
|
'normal': SMSEaglePriority.NORMAL,
|
||||||
|
# short for 'high'
|
||||||
|
'+': SMSEaglePriority.HIGH,
|
||||||
|
'high': SMSEaglePriority.HIGH,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class NotifySMSEagle(NotifyBase):
|
||||||
|
"""
|
||||||
|
A wrapper for SMSEagle Notifications
|
||||||
|
"""
|
||||||
|
|
||||||
|
# The default descriptive name associated with the Notification
|
||||||
|
service_name = 'SMS Eagle'
|
||||||
|
|
||||||
|
# The services URL
|
||||||
|
service_url = 'https://smseagle.eu'
|
||||||
|
|
||||||
|
# The default protocol
|
||||||
|
protocol = 'smseagle'
|
||||||
|
|
||||||
|
# The default protocol
|
||||||
|
secure_protocol = 'smseagles'
|
||||||
|
|
||||||
|
# A URL that takes you to the setup/help of the specific protocol
|
||||||
|
setup_url = 'https://github.com/caronc/apprise/wiki/Notify_smseagle'
|
||||||
|
|
||||||
|
# The path we send our notification to
|
||||||
|
notify_path = '/jsonrpc/sms'
|
||||||
|
|
||||||
|
# The maxumum length of the text message
|
||||||
|
# The actual limit is 160 but SMSEagle looks after the handling
|
||||||
|
# of large messages in it's upstream service
|
||||||
|
body_maxlen = 1200
|
||||||
|
|
||||||
|
# The maximum targets to include when doing batch transfers
|
||||||
|
default_batch_size = 10
|
||||||
|
|
||||||
|
# We don't support titles for SMSEagle notifications
|
||||||
|
title_maxlen = 0
|
||||||
|
|
||||||
|
# Define object templates
|
||||||
|
templates = (
|
||||||
|
'{schema}://{token}@{host}/{targets}',
|
||||||
|
'{schema}://{token}@{host}:{port}/{targets}',
|
||||||
|
)
|
||||||
|
|
||||||
|
# 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,
|
||||||
|
},
|
||||||
|
'token': {
|
||||||
|
'name': _('Access Token'),
|
||||||
|
'type': 'string',
|
||||||
|
},
|
||||||
|
'target_phone': {
|
||||||
|
'name': _('Target Phone No'),
|
||||||
|
'type': 'string',
|
||||||
|
'prefix': '+',
|
||||||
|
'regex': (r'^[0-9\s)(+-]+$', 'i'),
|
||||||
|
'map_to': 'targets',
|
||||||
|
},
|
||||||
|
'target_group': {
|
||||||
|
'name': _('Target Group ID'),
|
||||||
|
'type': 'string',
|
||||||
|
'prefix': '#',
|
||||||
|
'regex': (r'^[a-z0-9_-]+$', 'i'),
|
||||||
|
'map_to': 'targets',
|
||||||
|
},
|
||||||
|
'target_contact': {
|
||||||
|
'name': _('Target Contact'),
|
||||||
|
'type': 'string',
|
||||||
|
'prefix': '@',
|
||||||
|
'regex': (r'^[a-z0-9_-]+$', 'i'),
|
||||||
|
'map_to': 'targets',
|
||||||
|
},
|
||||||
|
'targets': {
|
||||||
|
'name': _('Targets'),
|
||||||
|
'type': 'list:string',
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
# Define our template arguments
|
||||||
|
template_args = dict(NotifyBase.template_args, **{
|
||||||
|
'to': {
|
||||||
|
'alias_of': 'targets',
|
||||||
|
},
|
||||||
|
'token': {
|
||||||
|
'alias_of': 'token',
|
||||||
|
},
|
||||||
|
'batch': {
|
||||||
|
'name': _('Batch Mode'),
|
||||||
|
'type': 'bool',
|
||||||
|
'default': False,
|
||||||
|
},
|
||||||
|
'status': {
|
||||||
|
'name': _('Show Status'),
|
||||||
|
'type': 'bool',
|
||||||
|
'default': False,
|
||||||
|
},
|
||||||
|
'test': {
|
||||||
|
'name': _('Test Only'),
|
||||||
|
'type': 'bool',
|
||||||
|
'default': False,
|
||||||
|
},
|
||||||
|
'flash': {
|
||||||
|
'name': _('Flash'),
|
||||||
|
'type': 'bool',
|
||||||
|
'default': False,
|
||||||
|
},
|
||||||
|
'priority': {
|
||||||
|
'name': _('Priority'),
|
||||||
|
'type': 'choice:int',
|
||||||
|
'values': SMSEAGLE_PRIORITIES,
|
||||||
|
'default': SMSEaglePriority.NORMAL,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
def __init__(self, token=None, targets=None, priority=None, batch=False,
|
||||||
|
status=False, flash=False, test=False, **kwargs):
|
||||||
|
"""
|
||||||
|
Initialize SMSEagle Object
|
||||||
|
"""
|
||||||
|
super(NotifySMSEagle, self).__init__(**kwargs)
|
||||||
|
|
||||||
|
# Prepare Flash Mode Flag
|
||||||
|
self.flash = flash
|
||||||
|
|
||||||
|
# Prepare Test Mode Flag
|
||||||
|
self.test = test
|
||||||
|
|
||||||
|
# Prepare Batch Mode Flag
|
||||||
|
self.batch = batch
|
||||||
|
|
||||||
|
# Set Status type
|
||||||
|
self.status = status
|
||||||
|
|
||||||
|
# Parse our targets
|
||||||
|
self.target_phones = list()
|
||||||
|
self.target_groups = list()
|
||||||
|
self.target_contacts = list()
|
||||||
|
|
||||||
|
# Used for URL generation afterwards only
|
||||||
|
self.invalid_targets = list()
|
||||||
|
|
||||||
|
# We always use a token if provided
|
||||||
|
self.token = validate_regex(self.user if not token else token)
|
||||||
|
if not self.token:
|
||||||
|
msg = \
|
||||||
|
'An invalid SMSEagle Access Token ({}) was specified.'.format(
|
||||||
|
self.user if not token else token)
|
||||||
|
self.logger.warning(msg)
|
||||||
|
raise TypeError(msg)
|
||||||
|
|
||||||
|
#
|
||||||
|
# Priority
|
||||||
|
#
|
||||||
|
try:
|
||||||
|
# Acquire our priority if we can:
|
||||||
|
# - We accept both the integer form as well as a string
|
||||||
|
# representation
|
||||||
|
self.priority = int(priority)
|
||||||
|
|
||||||
|
except TypeError:
|
||||||
|
# NoneType means use Default; this is an okay exception
|
||||||
|
self.priority = self.template_args['priority']['default']
|
||||||
|
|
||||||
|
except ValueError:
|
||||||
|
# Input is a string; attempt to get the lookup from our
|
||||||
|
# priority mapping
|
||||||
|
priority = priority.lower().strip()
|
||||||
|
|
||||||
|
# This little bit of black magic allows us to match against
|
||||||
|
# low, lo, l (for low);
|
||||||
|
# normal, norma, norm, nor, no, n (for normal)
|
||||||
|
# ... etc
|
||||||
|
result = next((key for key in SMSEAGLE_PRIORITY_MAP.keys()
|
||||||
|
if key.startswith(priority)), None) \
|
||||||
|
if priority else None
|
||||||
|
|
||||||
|
# Now test to see if we got a match
|
||||||
|
if not result:
|
||||||
|
msg = 'An invalid SMSEagle priority ' \
|
||||||
|
'({}) was specified.'.format(priority)
|
||||||
|
self.logger.warning(msg)
|
||||||
|
raise TypeError(msg)
|
||||||
|
|
||||||
|
# store our successfully looked up priority
|
||||||
|
self.priority = SMSEAGLE_PRIORITY_MAP[result]
|
||||||
|
|
||||||
|
if self.priority is not None and \
|
||||||
|
self.priority not in SMSEAGLE_PRIORITY_MAP.values():
|
||||||
|
msg = 'An invalid SMSEagle priority ' \
|
||||||
|
'({}) was specified.'.format(priority)
|
||||||
|
self.logger.warning(msg)
|
||||||
|
raise TypeError(msg)
|
||||||
|
|
||||||
|
# Validate our targerts
|
||||||
|
for target in parse_phone_no(targets):
|
||||||
|
# Validate targets and drop bad ones:
|
||||||
|
# Allow 9 digit numbers (without country code)
|
||||||
|
result = is_phone_no(target, min_len=9)
|
||||||
|
if result:
|
||||||
|
# store valid phone number
|
||||||
|
self.target_phones.append(
|
||||||
|
'{}{}'.format(
|
||||||
|
'' if target[0] != '+' else '+', result['full']))
|
||||||
|
continue
|
||||||
|
|
||||||
|
result = GROUP_REGEX.match(target)
|
||||||
|
if result:
|
||||||
|
# Just store group information
|
||||||
|
self.target_groups.append(result.group('group'))
|
||||||
|
continue
|
||||||
|
|
||||||
|
result = CONTACT_REGEX.match(target)
|
||||||
|
if result:
|
||||||
|
# Just store contact information
|
||||||
|
self.target_contacts.append(result.group('contact'))
|
||||||
|
continue
|
||||||
|
|
||||||
|
self.logger.warning(
|
||||||
|
'Dropped invalid phone/group/contact '
|
||||||
|
'({}) specified.'.format(target),
|
||||||
|
)
|
||||||
|
self.invalid_targets.append(target)
|
||||||
|
continue
|
||||||
|
|
||||||
|
return
|
||||||
|
|
||||||
|
def send(self, body, title='', notify_type=NotifyType.INFO, attach=None,
|
||||||
|
**kwargs):
|
||||||
|
"""
|
||||||
|
Perform SMSEagle Notification
|
||||||
|
"""
|
||||||
|
|
||||||
|
if not self.target_groups and not self.target_phones \
|
||||||
|
and not self.target_contacts:
|
||||||
|
# There were no services to notify
|
||||||
|
self.logger.warning(
|
||||||
|
'There were no SMSEagle targets to notify.')
|
||||||
|
return False
|
||||||
|
|
||||||
|
# error tracking (used for function return)
|
||||||
|
has_error = False
|
||||||
|
|
||||||
|
attachments = []
|
||||||
|
if attach:
|
||||||
|
for attachment in attach:
|
||||||
|
# Perform some simple error checking
|
||||||
|
if not attachment:
|
||||||
|
# We could not access the attachment
|
||||||
|
self.logger.error(
|
||||||
|
'Could not access attachment {}.'.format(
|
||||||
|
attachment.url(privacy=True)))
|
||||||
|
return False
|
||||||
|
|
||||||
|
if not re.match(r'^image/.*', attachment.mimetype, re.I):
|
||||||
|
# Only support images at this time
|
||||||
|
self.logger.warning(
|
||||||
|
'Ignoring unsupported SMSEagle attachment {}.'.format(
|
||||||
|
attachment.url(privacy=True)))
|
||||||
|
continue
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open(attachment.path, 'rb') as f:
|
||||||
|
# Prepare our Attachment in Base64
|
||||||
|
attachments.append({
|
||||||
|
'content_type': attachment.mimetype,
|
||||||
|
'content': base64.b64encode(
|
||||||
|
f.read()).decode('utf-8'),
|
||||||
|
})
|
||||||
|
|
||||||
|
except (OSError, IOError) as e:
|
||||||
|
self.logger.warning(
|
||||||
|
'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
|
||||||
|
|
||||||
|
# Prepare our headers
|
||||||
|
headers = {
|
||||||
|
'User-Agent': self.app_id,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
}
|
||||||
|
|
||||||
|
# Prepare our payload
|
||||||
|
params_template = {
|
||||||
|
# Our Access Token
|
||||||
|
'access_token': self.token,
|
||||||
|
|
||||||
|
# The message to send (populated below)
|
||||||
|
"message": None,
|
||||||
|
|
||||||
|
# 0 = normal priority, 1 = high priority
|
||||||
|
"highpriority": self.priority,
|
||||||
|
|
||||||
|
# Support unicode characters
|
||||||
|
"unicode": 1,
|
||||||
|
|
||||||
|
# sms or mms (if attachment)
|
||||||
|
"message_type": 'sms',
|
||||||
|
|
||||||
|
# Response Types:
|
||||||
|
# simple: format response as simple object with one result field
|
||||||
|
# extended: format response as extended JSON object
|
||||||
|
"responsetype": 'extended',
|
||||||
|
|
||||||
|
# SMS will be sent as flash message (1 = yes, 0 = no)
|
||||||
|
"flash": 1 if self.flash else 0,
|
||||||
|
|
||||||
|
# Message Simulation
|
||||||
|
"test": 1 if self.test else 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
# Set our schema
|
||||||
|
schema = 'https' if self.secure else 'http'
|
||||||
|
|
||||||
|
# Construct our URL
|
||||||
|
notify_url = '%s://%s' % (schema, self.host)
|
||||||
|
if isinstance(self.port, int):
|
||||||
|
notify_url += ':%d' % self.port
|
||||||
|
notify_url += self.notify_path
|
||||||
|
|
||||||
|
# Send in batches if identified to do so
|
||||||
|
batch_size = 1 if not self.batch else self.default_batch_size
|
||||||
|
|
||||||
|
notify_by = {
|
||||||
|
'phone': {
|
||||||
|
"method": "sms.send_sms",
|
||||||
|
'target': 'to',
|
||||||
|
},
|
||||||
|
'group': {
|
||||||
|
"method": "sms.send_togroup",
|
||||||
|
'target': 'groupname',
|
||||||
|
},
|
||||||
|
'contact': {
|
||||||
|
"method": "sms.send_tocontact",
|
||||||
|
'target': 'contactname',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
# categories separated into a tuple since notify_by.keys()
|
||||||
|
# returns an unpredicable list in Python 2.7 which causes
|
||||||
|
# tests to fail every so often
|
||||||
|
for category in ('phone', 'group', 'contact'):
|
||||||
|
# Create a copy of our template
|
||||||
|
payload = {
|
||||||
|
'method': notify_by[category]['method'],
|
||||||
|
'params': {
|
||||||
|
notify_by[category]['target']: None,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
# Apply Template
|
||||||
|
payload['params'].update(params_template)
|
||||||
|
|
||||||
|
# Set our Message
|
||||||
|
payload["params"]["message"] = "{}{}".format(
|
||||||
|
'' if not self.status else '{} '.format(
|
||||||
|
self.asset.ascii(notify_type)), body)
|
||||||
|
|
||||||
|
if attachments:
|
||||||
|
# Store our attachments
|
||||||
|
payload['params']['message_type'] = 'mms'
|
||||||
|
payload['params']['attachments'] = attachments
|
||||||
|
|
||||||
|
targets = getattr(self, 'target_{}s'.format(category))
|
||||||
|
for index in range(0, len(targets), batch_size):
|
||||||
|
# Prepare our recipients
|
||||||
|
payload['params'][notify_by[category]['target']] = \
|
||||||
|
','.join(targets[index:index + batch_size])
|
||||||
|
|
||||||
|
self.logger.debug('SMSEagle POST URL: %s (cert_verify=%r)' % (
|
||||||
|
notify_url, self.verify_certificate,
|
||||||
|
))
|
||||||
|
self.logger.debug('SMSEagle Payload: %s' % str(payload))
|
||||||
|
|
||||||
|
# Always call throttle before any remote server i/o is made
|
||||||
|
self.throttle()
|
||||||
|
try:
|
||||||
|
r = requests.post(
|
||||||
|
notify_url,
|
||||||
|
data=dumps(payload),
|
||||||
|
headers=headers,
|
||||||
|
verify=self.verify_certificate,
|
||||||
|
timeout=self.request_timeout,
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
content = loads(r.content)
|
||||||
|
|
||||||
|
# Store our status
|
||||||
|
status_str = str(content['result'])
|
||||||
|
|
||||||
|
except (AttributeError, TypeError, ValueError, KeyError):
|
||||||
|
# ValueError = r.content is Unparsable
|
||||||
|
# TypeError = r.content is None
|
||||||
|
# AttributeError = r is None
|
||||||
|
# KeyError = 'result' is not found in result
|
||||||
|
content = {}
|
||||||
|
|
||||||
|
# The result set can be a list such as:
|
||||||
|
# b'{"result":[{"message_id":4753,"status":"ok"}]}'
|
||||||
|
#
|
||||||
|
# It can also just be as a dictionary:
|
||||||
|
# b'{"result":{"message_id":4753,"status":"ok"}}'
|
||||||
|
#
|
||||||
|
# The below code handles both cases only only fails if a
|
||||||
|
# non-ok value was returned
|
||||||
|
|
||||||
|
if r.status_code not in (
|
||||||
|
requests.codes.ok, requests.codes.created) or \
|
||||||
|
not isinstance(content.get('result'),
|
||||||
|
(dict, list)) or \
|
||||||
|
(isinstance(content.get('result'), dict) and
|
||||||
|
content['result'].get('status') != 'ok') or \
|
||||||
|
(isinstance(content.get('result'), list) and
|
||||||
|
next((True for entry in content.get('result')
|
||||||
|
if isinstance(entry, dict) and
|
||||||
|
entry.get('status') != 'ok'), False
|
||||||
|
) # pragma: no cover
|
||||||
|
):
|
||||||
|
|
||||||
|
# We had a problem
|
||||||
|
status_str = content.get('result') \
|
||||||
|
if content.get('result') else \
|
||||||
|
NotifySMSEagle.http_response_code_lookup(
|
||||||
|
r.status_code)
|
||||||
|
|
||||||
|
self.logger.warning(
|
||||||
|
'Failed to send {} {} SMSEagle {} notification: '
|
||||||
|
'{}{}error={}.'.format(
|
||||||
|
len(targets[index:index + batch_size]),
|
||||||
|
'to {}'.format(targets[index])
|
||||||
|
if batch_size == 1 else '(s)',
|
||||||
|
category,
|
||||||
|
status_str,
|
||||||
|
', ' if status_str else '',
|
||||||
|
r.status_code))
|
||||||
|
|
||||||
|
self.logger.debug(
|
||||||
|
'Response {} Details:\r\n{}'.format(
|
||||||
|
category.upper(), r.content))
|
||||||
|
|
||||||
|
# Mark our failure
|
||||||
|
has_error = True
|
||||||
|
continue
|
||||||
|
|
||||||
|
else:
|
||||||
|
self.logger.info(
|
||||||
|
'Sent {} SMSEagle {} notification{}.'
|
||||||
|
.format(
|
||||||
|
len(targets[index:index + batch_size]),
|
||||||
|
category,
|
||||||
|
' to {}'.format(targets[index])
|
||||||
|
if batch_size == 1 else '(s)',
|
||||||
|
))
|
||||||
|
|
||||||
|
except requests.RequestException as e:
|
||||||
|
self.logger.warning(
|
||||||
|
'A Connection error occured sending {} SMSEagle '
|
||||||
|
'{} notification(s).'.format(
|
||||||
|
len(targets[index:index + batch_size]), category))
|
||||||
|
self.logger.debug('Socket Exception: %s' % str(e))
|
||||||
|
|
||||||
|
# Mark our failure
|
||||||
|
has_error = True
|
||||||
|
continue
|
||||||
|
|
||||||
|
return not has_error
|
||||||
|
|
||||||
|
def url(self, privacy=False, *args, **kwargs):
|
||||||
|
"""
|
||||||
|
Returns the URL built dynamically based on specified arguments.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Define any URL parameters
|
||||||
|
params = {
|
||||||
|
'batch': 'yes' if self.batch else 'no',
|
||||||
|
'status': 'yes' if self.status else 'no',
|
||||||
|
'flash': 'yes' if self.flash else 'no',
|
||||||
|
'test': 'yes' if self.test else 'no',
|
||||||
|
}
|
||||||
|
|
||||||
|
# Extend our parameters
|
||||||
|
params.update(self.url_parameters(privacy=privacy, *args, **kwargs))
|
||||||
|
|
||||||
|
default_priority = self.template_args['priority']['default']
|
||||||
|
if self.priority is not None:
|
||||||
|
# Store our priority; but only if it was specified
|
||||||
|
params['priority'] = \
|
||||||
|
next((key for key, value in SMSEAGLE_PRIORITY_MAP.items()
|
||||||
|
if value == self.priority),
|
||||||
|
default_priority) # pragma: no cover
|
||||||
|
|
||||||
|
# Default port handling
|
||||||
|
default_port = 443 if self.secure else 80
|
||||||
|
|
||||||
|
return '{schema}://{token}@{hostname}{port}/{targets}?{params}'.format(
|
||||||
|
schema=self.secure_protocol if self.secure else self.protocol,
|
||||||
|
token=self.pprint(
|
||||||
|
self.token, privacy, mode=PrivacyMode.Secret, 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(
|
||||||
|
[NotifySMSEagle.quote(x, safe='#@') for x in chain(
|
||||||
|
# Pass phones directly as is
|
||||||
|
self.target_phones,
|
||||||
|
# Contacts
|
||||||
|
['@{}'.format(x) for x in self.target_contacts],
|
||||||
|
# Groups
|
||||||
|
['#{}'.format(x) for x in self.target_groups],
|
||||||
|
)]),
|
||||||
|
params=NotifySMSEagle.urlencode(params),
|
||||||
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def parse_url(url):
|
||||||
|
"""
|
||||||
|
Parses the URL and returns enough arguments that can allow
|
||||||
|
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
|
||||||
|
|
||||||
|
# Get our entries; split_path() looks after unquoting content for us
|
||||||
|
# by default
|
||||||
|
results['targets'] = \
|
||||||
|
NotifySMSEagle.split_path(results['fullpath'])
|
||||||
|
|
||||||
|
if 'token' in results['qsd'] and len(results['qsd']['token']):
|
||||||
|
results['token'] = NotifySMSEagle.unquote(results['qsd']['token'])
|
||||||
|
|
||||||
|
elif not results['password'] and results['user']:
|
||||||
|
results['token'] = NotifySMSEagle.unquote(results['user'])
|
||||||
|
|
||||||
|
# Support the 'to' variable so that we can support targets this way too
|
||||||
|
# The 'to' makes it easier to use yaml configuration
|
||||||
|
if 'to' in results['qsd'] and len(results['qsd']['to']):
|
||||||
|
results['targets'] += \
|
||||||
|
NotifySMSEagle.parse_phone_no(results['qsd']['to'])
|
||||||
|
|
||||||
|
# Get Batch Mode Flag
|
||||||
|
results['batch'] = \
|
||||||
|
parse_bool(results['qsd'].get('batch', False))
|
||||||
|
|
||||||
|
# Get Flash Mode Flag
|
||||||
|
results['flash'] = \
|
||||||
|
parse_bool(results['qsd'].get('flash', False))
|
||||||
|
|
||||||
|
# Get Test Mode Flag
|
||||||
|
results['test'] = \
|
||||||
|
parse_bool(results['qsd'].get('test', False))
|
||||||
|
|
||||||
|
# Get status switch
|
||||||
|
results['status'] = \
|
||||||
|
parse_bool(results['qsd'].get('status', False))
|
||||||
|
|
||||||
|
if 'priority' in results['qsd'] and len(results['qsd']['priority']):
|
||||||
|
results['priority'] = \
|
||||||
|
NotifySMSEagle.unquote(results['qsd']['priority'])
|
||||||
|
|
||||||
|
return results
|
|
@ -315,17 +315,9 @@ class NotifySMTP2Go(NotifyBase):
|
||||||
self.logger.debug('I/O Exception: %s' % str(e))
|
self.logger.debug('I/O Exception: %s' % str(e))
|
||||||
return False
|
return False
|
||||||
|
|
||||||
try:
|
sender = formataddr(
|
||||||
sender = formataddr(
|
(self.from_name if self.from_name else False,
|
||||||
(self.from_name if self.from_name else False,
|
self.from_addr), charset='utf-8')
|
||||||
self.from_addr), charset='utf-8')
|
|
||||||
|
|
||||||
except TypeError:
|
|
||||||
# Python v2.x Support (no charset keyword)
|
|
||||||
# Format our cc addresses to support the Name field
|
|
||||||
sender = formataddr(
|
|
||||||
(self.from_name if self.from_name else False,
|
|
||||||
self.from_addr))
|
|
||||||
|
|
||||||
# Prepare our payload
|
# Prepare our payload
|
||||||
payload = {
|
payload = {
|
||||||
|
@ -369,33 +361,17 @@ class NotifySMTP2Go(NotifyBase):
|
||||||
# Strip target out of bcc list if in To
|
# Strip target out of bcc list if in To
|
||||||
bcc = (bcc - set([to_addr[1]]))
|
bcc = (bcc - set([to_addr[1]]))
|
||||||
|
|
||||||
try:
|
# Prepare our `to`
|
||||||
# Prepare our to
|
to.append(formataddr(to_addr, charset='utf-8'))
|
||||||
to.append(formataddr(to_addr, charset='utf-8'))
|
|
||||||
|
|
||||||
except TypeError:
|
|
||||||
# Python v2.x Support (no charset keyword)
|
|
||||||
# Format our cc addresses to support the Name field
|
|
||||||
|
|
||||||
# Prepare our to
|
|
||||||
to.append(formataddr(to_addr))
|
|
||||||
|
|
||||||
# Prepare our To
|
# Prepare our To
|
||||||
payload['to'] = to
|
payload['to'] = to
|
||||||
|
|
||||||
if cc:
|
if cc:
|
||||||
try:
|
# Format our cc addresses to support the Name field
|
||||||
# Format our cc addresses to support the Name field
|
payload['cc'] = [formataddr(
|
||||||
payload['cc'] = [formataddr(
|
(self.names.get(addr, False), addr), charset='utf-8')
|
||||||
(self.names.get(addr, False), addr), charset='utf-8')
|
for addr in cc]
|
||||||
for addr in cc]
|
|
||||||
|
|
||||||
except TypeError:
|
|
||||||
# Python v2.x Support (no charset keyword)
|
|
||||||
# Format our cc addresses to support the Name field
|
|
||||||
payload['cc'] = [formataddr( # pragma: no branch
|
|
||||||
(self.names.get(addr, False), addr))
|
|
||||||
for addr in cc]
|
|
||||||
|
|
||||||
# Format our bcc addresses to support the Name field
|
# Format our bcc addresses to support the Name field
|
||||||
if bcc:
|
if bcc:
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
#
|
#
|
||||||
# Copyright (C) 2020 Chris Caron <lead2gold@gmail.com>
|
# Copyright (C) 2022 Chris Caron <lead2gold@gmail.com>
|
||||||
# All rights reserved.
|
# All rights reserved.
|
||||||
#
|
#
|
||||||
# This code is licensed under the MIT License.
|
# This code is licensed under the MIT License.
|
||||||
|
@ -23,8 +23,10 @@
|
||||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
# THE SOFTWARE.
|
# THE SOFTWARE.
|
||||||
|
|
||||||
|
import re
|
||||||
import requests
|
import requests
|
||||||
from json import dumps
|
from json import dumps
|
||||||
|
import base64
|
||||||
|
|
||||||
from .NotifyBase import NotifyBase
|
from .NotifyBase import NotifyBase
|
||||||
from ..common import NotifyType
|
from ..common import NotifyType
|
||||||
|
@ -35,6 +37,10 @@ from ..URLBase import PrivacyMode
|
||||||
from ..AppriseLocale import gettext_lazy as _
|
from ..AppriseLocale import gettext_lazy as _
|
||||||
|
|
||||||
|
|
||||||
|
GROUP_REGEX = re.compile(
|
||||||
|
r'^\s*((\@|\%40)?(group\.)|\@|\%40)(?P<group>[a-z0-9_=-]+)', re.I)
|
||||||
|
|
||||||
|
|
||||||
class NotifySignalAPI(NotifyBase):
|
class NotifySignalAPI(NotifyBase):
|
||||||
"""
|
"""
|
||||||
A wrapper for SignalAPI Notifications
|
A wrapper for SignalAPI Notifications
|
||||||
|
@ -113,6 +119,13 @@ class NotifySignalAPI(NotifyBase):
|
||||||
'regex': (r'^[0-9\s)(+-]+$', 'i'),
|
'regex': (r'^[0-9\s)(+-]+$', 'i'),
|
||||||
'map_to': 'targets',
|
'map_to': 'targets',
|
||||||
},
|
},
|
||||||
|
'target_channel': {
|
||||||
|
'name': _('Target Group ID'),
|
||||||
|
'type': 'string',
|
||||||
|
'prefix': '@',
|
||||||
|
'regex': (r'^[a-z0-9_=-]+$', 'i'),
|
||||||
|
'map_to': 'targets',
|
||||||
|
},
|
||||||
'targets': {
|
'targets': {
|
||||||
'name': _('Targets'),
|
'name': _('Targets'),
|
||||||
'type': 'list:string',
|
'type': 'list:string',
|
||||||
|
@ -173,23 +186,33 @@ class NotifySignalAPI(NotifyBase):
|
||||||
for target in parse_phone_no(targets):
|
for target in parse_phone_no(targets):
|
||||||
# Validate targets and drop bad ones:
|
# Validate targets and drop bad ones:
|
||||||
result = is_phone_no(target)
|
result = is_phone_no(target)
|
||||||
if not result:
|
if result:
|
||||||
self.logger.warning(
|
# store valid phone number
|
||||||
'Dropped invalid phone # '
|
self.targets.append('+{}'.format(result['full']))
|
||||||
'({}) specified.'.format(target),
|
|
||||||
)
|
|
||||||
self.invalid_targets.append(target)
|
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# store valid phone number
|
result = GROUP_REGEX.match(target)
|
||||||
self.targets.append('+{}'.format(result['full']))
|
if result:
|
||||||
|
# Just store group information
|
||||||
|
self.targets.append(
|
||||||
|
'group.{}'.format(result.group('group')))
|
||||||
|
continue
|
||||||
|
|
||||||
|
self.logger.warning(
|
||||||
|
'Dropped invalid phone/group '
|
||||||
|
'({}) specified.'.format(target),
|
||||||
|
)
|
||||||
|
self.invalid_targets.append(target)
|
||||||
|
continue
|
||||||
|
|
||||||
else:
|
else:
|
||||||
# Send a message to ourselves
|
# Send a message to ourselves
|
||||||
self.targets.append(self.source)
|
self.targets.append(self.source)
|
||||||
|
|
||||||
return
|
return
|
||||||
|
|
||||||
def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs):
|
def send(self, body, title='', notify_type=NotifyType.INFO, attach=None,
|
||||||
|
**kwargs):
|
||||||
"""
|
"""
|
||||||
Perform Signal API Notification
|
Perform Signal API Notification
|
||||||
"""
|
"""
|
||||||
|
@ -203,12 +226,50 @@ class NotifySignalAPI(NotifyBase):
|
||||||
# error tracking (used for function return)
|
# error tracking (used for function return)
|
||||||
has_error = False
|
has_error = False
|
||||||
|
|
||||||
|
attachments = []
|
||||||
|
if attach:
|
||||||
|
for attachment in attach:
|
||||||
|
# Perform some simple error checking
|
||||||
|
if not attachment:
|
||||||
|
# We could not access the attachment
|
||||||
|
self.logger.error(
|
||||||
|
'Could not access attachment {}.'.format(
|
||||||
|
attachment.url(privacy=True)))
|
||||||
|
return False
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open(attachment.path, 'rb') as f:
|
||||||
|
# Prepare our Attachment in Base64
|
||||||
|
attachments.append(
|
||||||
|
base64.b64encode(f.read()).decode('utf-8'))
|
||||||
|
|
||||||
|
except (OSError, IOError) as e:
|
||||||
|
self.logger.warning(
|
||||||
|
'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
|
||||||
|
|
||||||
# Prepare our headers
|
# Prepare our headers
|
||||||
headers = {
|
headers = {
|
||||||
'User-Agent': self.app_id,
|
'User-Agent': self.app_id,
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Format defined here:
|
||||||
|
# https://bbernhard.github.io/signal-cli-rest-api\
|
||||||
|
# /#/Messages/post_v2_send
|
||||||
|
# Example:
|
||||||
|
# {
|
||||||
|
# "base64_attachments": [
|
||||||
|
# "string"
|
||||||
|
# ],
|
||||||
|
# "message": "string",
|
||||||
|
# "number": "string",
|
||||||
|
# "recipients": [
|
||||||
|
# "string"
|
||||||
|
# ]
|
||||||
|
# }
|
||||||
# Prepare our payload
|
# Prepare our payload
|
||||||
payload = {
|
payload = {
|
||||||
'message': "{}{}".format(
|
'message': "{}{}".format(
|
||||||
|
@ -218,6 +279,10 @@ class NotifySignalAPI(NotifyBase):
|
||||||
"recipients": []
|
"recipients": []
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if attachments:
|
||||||
|
# Store our attachments
|
||||||
|
payload['base64_attachments'] = attachments
|
||||||
|
|
||||||
# Determine Authentication
|
# Determine Authentication
|
||||||
auth = None
|
auth = None
|
||||||
if self.user:
|
if self.user:
|
||||||
|
@ -255,7 +320,8 @@ class NotifySignalAPI(NotifyBase):
|
||||||
verify=self.verify_certificate,
|
verify=self.verify_certificate,
|
||||||
timeout=self.request_timeout,
|
timeout=self.request_timeout,
|
||||||
)
|
)
|
||||||
if r.status_code != requests.codes.ok:
|
if r.status_code not in (
|
||||||
|
requests.codes.ok, requests.codes.created):
|
||||||
# We had a problem
|
# We had a problem
|
||||||
status_str = \
|
status_str = \
|
||||||
NotifySignalAPI.http_response_code_lookup(
|
NotifySignalAPI.http_response_code_lookup(
|
||||||
|
@ -339,7 +405,11 @@ class NotifySignalAPI(NotifyBase):
|
||||||
targets = self.invalid_targets
|
targets = self.invalid_targets
|
||||||
|
|
||||||
else:
|
else:
|
||||||
targets = list(self.targets)
|
# append @ to non-phone number entries as they are groups
|
||||||
|
# Remove group. prefix as well
|
||||||
|
targets = \
|
||||||
|
['@{}'.format(x[6:]) if x[0] != '+'
|
||||||
|
else x for x in self.targets]
|
||||||
|
|
||||||
return '{schema}://{auth}{hostname}{port}/{src}/{dst}?{params}'.format(
|
return '{schema}://{auth}{hostname}{port}/{src}/{dst}?{params}'.format(
|
||||||
schema=self.secure_protocol if self.secure else self.protocol,
|
schema=self.secure_protocol if self.secure else self.protocol,
|
||||||
|
@ -350,7 +420,7 @@ class NotifySignalAPI(NotifyBase):
|
||||||
else ':{}'.format(self.port),
|
else ':{}'.format(self.port),
|
||||||
src=self.source,
|
src=self.source,
|
||||||
dst='/'.join(
|
dst='/'.join(
|
||||||
[NotifySignalAPI.quote(x, safe='') for x in targets]),
|
[NotifySignalAPI.quote(x, safe='@+') for x in targets]),
|
||||||
params=NotifySignalAPI.urlencode(params),
|
params=NotifySignalAPI.urlencode(params),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -33,7 +33,6 @@
|
||||||
# from). Activated phone numbers can be found on your dashboard here:
|
# from). Activated phone numbers can be found on your dashboard here:
|
||||||
# - https://dashboard.sinch.com/numbers/your-numbers/numbers
|
# - https://dashboard.sinch.com/numbers/your-numbers/numbers
|
||||||
#
|
#
|
||||||
import six
|
|
||||||
import requests
|
import requests
|
||||||
import json
|
import json
|
||||||
|
|
||||||
|
@ -46,7 +45,7 @@ from ..utils import validate_regex
|
||||||
from ..AppriseLocale import gettext_lazy as _
|
from ..AppriseLocale import gettext_lazy as _
|
||||||
|
|
||||||
|
|
||||||
class SinchRegion(object):
|
class SinchRegion:
|
||||||
"""
|
"""
|
||||||
Defines the Sinch Server Regions
|
Defines the Sinch Server Regions
|
||||||
"""
|
"""
|
||||||
|
@ -192,7 +191,7 @@ class NotifySinch(NotifyBase):
|
||||||
|
|
||||||
# Setup our region
|
# Setup our region
|
||||||
self.region = self.template_args['region']['default'] \
|
self.region = self.template_args['region']['default'] \
|
||||||
if not isinstance(region, six.string_types) else region.lower()
|
if not isinstance(region, str) else region.lower()
|
||||||
if self.region and self.region not in SINCH_REGIONS:
|
if self.region and self.region not in SINCH_REGIONS:
|
||||||
msg = 'The region specified ({}) is invalid.'.format(region)
|
msg = 'The region specified ({}) is invalid.'.format(region)
|
||||||
self.logger.warning(msg)
|
self.logger.warning(msg)
|
||||||
|
|
|
@ -94,7 +94,7 @@ SLACK_HTTP_ERROR_MAP = {
|
||||||
CHANNEL_LIST_DELIM = re.compile(r'[ \t\r\n,#\\/]+')
|
CHANNEL_LIST_DELIM = re.compile(r'[ \t\r\n,#\\/]+')
|
||||||
|
|
||||||
|
|
||||||
class SlackMode(object):
|
class SlackMode:
|
||||||
"""
|
"""
|
||||||
Tracks the mode of which we're using Slack
|
Tracks the mode of which we're using Slack
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -80,7 +80,7 @@ SPARKPOST_HTTP_ERROR_MAP = {
|
||||||
|
|
||||||
|
|
||||||
# Priorities
|
# Priorities
|
||||||
class SparkPostRegion(object):
|
class SparkPostRegion:
|
||||||
US = 'us'
|
US = 'us'
|
||||||
EU = 'eu'
|
EU = 'eu'
|
||||||
|
|
||||||
|
@ -503,14 +503,9 @@ class NotifySparkPost(NotifyBase):
|
||||||
# Send in batches if identified to do so
|
# Send in batches if identified to do so
|
||||||
batch_size = 1 if not self.batch else self.default_batch_size
|
batch_size = 1 if not self.batch else self.default_batch_size
|
||||||
|
|
||||||
try:
|
reply_to = formataddr((self.from_name if self.from_name else False,
|
||||||
reply_to = formataddr((self.from_name if self.from_name else False,
|
self.from_addr), charset='utf-8')
|
||||||
self.from_addr), charset='utf-8')
|
|
||||||
except TypeError:
|
|
||||||
# Python v2.x Support (no charset keyword)
|
|
||||||
# Format our cc addresses to support the Name field
|
|
||||||
reply_to = formataddr((self.from_name if self.from_name else False,
|
|
||||||
self.from_addr))
|
|
||||||
payload = {
|
payload = {
|
||||||
"options": {
|
"options": {
|
||||||
# When set to True, an image is included with the email which
|
# When set to True, an image is included with the email which
|
||||||
|
|
|
@ -52,7 +52,7 @@ from ..AppriseLocale import gettext_lazy as _
|
||||||
# specified. If not, we use the user of the person sending the notification
|
# specified. If not, we use the user of the person sending the notification
|
||||||
# Finally the channel identifier is detected
|
# Finally the channel identifier is detected
|
||||||
CHANNEL_REGEX = re.compile(
|
CHANNEL_REGEX = re.compile(
|
||||||
r'^\s*(#|%23)?((@|%40)?(?P<user>[a-z0-9_]+)([/\\]|%2F))?'
|
r'^\s*(\#|\%23)?((\@|\%40)?(?P<user>[a-z0-9_]+)([/\\]|\%2F))?'
|
||||||
r'(?P<channel>[a-z0-9_-]+)\s*$', re.I)
|
r'(?P<channel>[a-z0-9_-]+)\s*$', re.I)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
#
|
#
|
||||||
# Copyright (C) 2021 <example@example.com>
|
# Copyright (C) 2021 Chris Caron <lead2gold@gmail.com>
|
||||||
# All rights reserved.
|
# All rights reserved.
|
||||||
#
|
#
|
||||||
# This code is licensed under the MIT License.
|
# This code is licensed under the MIT License.
|
||||||
|
@ -42,7 +42,7 @@ from ..AppriseLocale import gettext_lazy as _
|
||||||
|
|
||||||
|
|
||||||
# calls
|
# calls
|
||||||
class StrmlabsCall(object):
|
class StrmlabsCall:
|
||||||
ALERT = 'ALERTS'
|
ALERT = 'ALERTS'
|
||||||
DONATION = 'DONATIONS'
|
DONATION = 'DONATIONS'
|
||||||
|
|
||||||
|
@ -55,7 +55,7 @@ STRMLABS_CALLS = (
|
||||||
|
|
||||||
|
|
||||||
# alerts
|
# alerts
|
||||||
class StrmlabsAlert(object):
|
class StrmlabsAlert:
|
||||||
FOLLOW = 'follow'
|
FOLLOW = 'follow'
|
||||||
SUBSCRIPTION = 'subscription'
|
SUBSCRIPTION = 'subscription'
|
||||||
DONATION = 'donation'
|
DONATION = 'donation'
|
||||||
|
|
|
@ -23,7 +23,6 @@
|
||||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
# THE SOFTWARE.
|
# THE SOFTWARE.
|
||||||
import os
|
import os
|
||||||
import six
|
|
||||||
import syslog
|
import syslog
|
||||||
import socket
|
import socket
|
||||||
|
|
||||||
|
@ -101,7 +100,7 @@ SYSLOG_FACILITY_RMAP = {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class SyslogMode(object):
|
class SyslogMode:
|
||||||
# A local query
|
# A local query
|
||||||
LOCAL = "local"
|
LOCAL = "local"
|
||||||
|
|
||||||
|
@ -217,7 +216,7 @@ class NotifySyslog(NotifyBase):
|
||||||
self.template_tokens['facility']['default']]
|
self.template_tokens['facility']['default']]
|
||||||
|
|
||||||
self.mode = self.template_args['mode']['default'] \
|
self.mode = self.template_args['mode']['default'] \
|
||||||
if not isinstance(mode, six.string_types) else mode.lower()
|
if not isinstance(mode, str) else mode.lower()
|
||||||
|
|
||||||
if self.mode not in SYSLOG_MODES:
|
if self.mode not in SYSLOG_MODES:
|
||||||
msg = 'The mode specified ({}) is invalid.'.format(mode)
|
msg = 'The mode specified ({}) is invalid.'.format(mode)
|
||||||
|
|
|
@ -177,44 +177,85 @@ class NotifyTelegram(NotifyBase):
|
||||||
# characters passed into it. to handle this situation, we need to
|
# characters passed into it. to handle this situation, we need to
|
||||||
# search the body for these sequences and convert them to the
|
# search the body for these sequences and convert them to the
|
||||||
# output the user expected
|
# output the user expected
|
||||||
__telegram_escape_html_dict = {
|
__telegram_escape_html_entries = (
|
||||||
# New Lines
|
# Comments
|
||||||
re.compile(r'<\s*/?br\s*/?>\r*\n?', re.I): '\r\n',
|
(re.compile(
|
||||||
re.compile(r'<\s*/(br|p|div|li)[^>]*>\r*\n?', re.I): '\r\n',
|
r'\s*<!.+?-->\s*',
|
||||||
|
(re.I | re.M | re.S)), '', {}),
|
||||||
# The following characters can be altered to become supported
|
|
||||||
re.compile(r'<\s*pre[^>]*>', re.I): '<code>',
|
|
||||||
re.compile(r'<\s*/pre[^>]*>', re.I): '</code>',
|
|
||||||
|
|
||||||
# the following tags are not supported
|
# the following tags are not supported
|
||||||
re.compile(
|
(re.compile(
|
||||||
r'<\s*(br|p|div|span|body|script|meta|html|font'
|
r'\s*<\s*(!?DOCTYPE|p|div|span|body|script|link|'
|
||||||
r'|label|iframe|li|ol|ul|source|script)[^>]*>', re.I): '',
|
r'meta|html|font|head|label|form|input|textarea|select|iframe|'
|
||||||
|
r'source|script)([^a-z0-9>][^>]*)?>\s*',
|
||||||
|
(re.I | re.M | re.S)), '', {}),
|
||||||
|
|
||||||
re.compile(
|
# All closing tags to be removed are put here
|
||||||
r'<\s*/(span|body|script|meta|html|font'
|
(re.compile(
|
||||||
r'|label|iframe|ol|ul|source|script)[^>]*>', re.I): '',
|
r'\s*<\s*/(span|body|script|meta|html|font|head|'
|
||||||
|
r'label|form|input|textarea|select|ol|ul|link|'
|
||||||
# Italic
|
r'iframe|source|script)([^a-z0-9>][^>]*)?>\s*',
|
||||||
re.compile(r'<\s*(caption|em)[^>]*>', re.I): '<i>',
|
(re.I | re.M | re.S)), '', {}),
|
||||||
re.compile(r'<\s*/(caption|em)[^>]*>', re.I): '</i>',
|
|
||||||
|
|
||||||
# Bold
|
# Bold
|
||||||
re.compile(r'<\s*(h[1-6]|title|strong)[^>]*>', re.I): '<b>',
|
(re.compile(
|
||||||
re.compile(r'<\s*/(h[1-6]|title|strong)[^>]*>', re.I): '</b>',
|
r'<\s*(strong)([^a-z0-9>][^>]*)?>',
|
||||||
|
(re.I | re.M | re.S)), '<b>', {}),
|
||||||
|
(re.compile(
|
||||||
|
r'<\s*/\s*(strong)([^a-z0-9>][^>]*)?>',
|
||||||
|
(re.I | re.M | re.S)), '</b>', {}),
|
||||||
|
(re.compile(
|
||||||
|
r'\s*<\s*(h[1-6]|title)([^a-z0-9>][^>]*)?>\s*',
|
||||||
|
(re.I | re.M | re.S)), '{}<b>', {'html': '\r\n'}),
|
||||||
|
(re.compile(
|
||||||
|
r'\s*<\s*/\s*(h[1-6]|title)([^a-z0-9>][^>]*)?>\s*',
|
||||||
|
(re.I | re.M | re.S)),
|
||||||
|
'</b>{}', {'html': '<br/>'}),
|
||||||
|
|
||||||
|
# Italic
|
||||||
|
(re.compile(
|
||||||
|
r'<\s*(caption|em)([^a-z0-9>][^>]*)?>',
|
||||||
|
(re.I | re.M | re.S)), '<i>', {}),
|
||||||
|
(re.compile(
|
||||||
|
r'<\s*/\s*(caption|em)([^a-z0-9>][^>]*)?>',
|
||||||
|
(re.I | re.M | re.S)), '</i>', {}),
|
||||||
|
|
||||||
|
# Bullet Lists
|
||||||
|
(re.compile(
|
||||||
|
r'<\s*li([^a-z0-9>][^>]*)?>\s*',
|
||||||
|
(re.I | re.M | re.S)), ' -', {}),
|
||||||
|
|
||||||
|
# convert pre tags to code (supported by Telegram)
|
||||||
|
(re.compile(
|
||||||
|
r'<\s*pre([^a-z0-9>][^>]*)?>',
|
||||||
|
(re.I | re.M | re.S)), '{}<code>', {'html': '\r\n'}),
|
||||||
|
(re.compile(
|
||||||
|
r'<\s*/\s*pre([^a-z0-9>][^>]*)?>',
|
||||||
|
(re.I | re.M | re.S)), '</code>{}', {'html': '\r\n'}),
|
||||||
|
|
||||||
|
# New Lines
|
||||||
|
(re.compile(
|
||||||
|
r'\s*<\s*/?\s*(ol|ul|br|hr)\s*/?>\s*',
|
||||||
|
(re.I | re.M | re.S)), '\r\n', {}),
|
||||||
|
(re.compile(
|
||||||
|
r'\s*<\s*/\s*(br|p|hr|li|div)([^a-z0-9>][^>]*)?>\s*',
|
||||||
|
(re.I | re.M | re.S)), '\r\n', {}),
|
||||||
|
|
||||||
# HTML Spaces ( ) and tabs ( ) aren't supported
|
# HTML Spaces ( ) and tabs ( ) aren't supported
|
||||||
# See https://core.telegram.org/bots/api#html-style
|
# See https://core.telegram.org/bots/api#html-style
|
||||||
re.compile(r'\ ?', re.I): ' ',
|
(re.compile(r'\ ?', re.I), ' ', {}),
|
||||||
|
|
||||||
# Tabs become 3 spaces
|
# Tabs become 3 spaces
|
||||||
re.compile(r'\ ?', re.I): ' ',
|
(re.compile(r'\ ?', re.I), ' ', {}),
|
||||||
|
|
||||||
# Some characters get re-escaped by the Telegram upstream
|
# Some characters get re-escaped by the Telegram upstream
|
||||||
# service so we need to convert these back,
|
# service so we need to convert these back,
|
||||||
re.compile(r'\'?', re.I): '\'',
|
(re.compile(r'\'?', re.I), '\'', {}),
|
||||||
re.compile(r'\"?', re.I): '"',
|
(re.compile(r'\"?', re.I), '"', {}),
|
||||||
}
|
|
||||||
|
# New line cleanup
|
||||||
|
(re.compile(r'\r*\n[\r\n]+', re.I), '\r\n', {}),
|
||||||
|
)
|
||||||
|
|
||||||
# Define our template tokens
|
# Define our template tokens
|
||||||
template_tokens = dict(NotifyBase.template_tokens, **{
|
template_tokens = dict(NotifyBase.template_tokens, **{
|
||||||
|
@ -597,38 +638,19 @@ class NotifyTelegram(NotifyBase):
|
||||||
|
|
||||||
# Use Telegram's HTML mode
|
# Use Telegram's HTML mode
|
||||||
payload['parse_mode'] = 'HTML'
|
payload['parse_mode'] = 'HTML'
|
||||||
for r, v in self.__telegram_escape_html_dict.items():
|
for r, v, m in self.__telegram_escape_html_entries:
|
||||||
body = r.sub(v, body, re.I)
|
|
||||||
|
if 'html' in m:
|
||||||
|
# Handle special cases where we need to alter new lines
|
||||||
|
# for presentation purposes
|
||||||
|
v = v.format(m['html'] if body_format in (
|
||||||
|
NotifyFormat.HTML, NotifyFormat.MARKDOWN) else '')
|
||||||
|
|
||||||
|
body = r.sub(v, body)
|
||||||
|
|
||||||
# Prepare our payload based on HTML or TEXT
|
# Prepare our payload based on HTML or TEXT
|
||||||
payload['text'] = body
|
payload['text'] = body
|
||||||
|
|
||||||
# else: # self.notify_format == NotifyFormat.TEXT:
|
|
||||||
# # Use Telegram's HTML mode
|
|
||||||
# payload['parse_mode'] = 'HTML'
|
|
||||||
|
|
||||||
# # Further html escaping required...
|
|
||||||
# telegram_escape_text_dict = {
|
|
||||||
# # We need to escape characters that conflict with html
|
|
||||||
# # entity blocks (< and >) when displaying text
|
|
||||||
# r'>': '>',
|
|
||||||
# r'<': '<',
|
|
||||||
# r'\&': '&',
|
|
||||||
# }
|
|
||||||
|
|
||||||
# # Create a regular expression from the dictionary keys
|
|
||||||
# text_regex = re.compile("(%s)" % "|".join(
|
|
||||||
# map(re.escape, telegram_escape_text_dict.keys())).lower(),
|
|
||||||
# re.I)
|
|
||||||
|
|
||||||
# # For each match, look-up corresponding value in dictionary
|
|
||||||
# body = text_regex.sub( # pragma: no branch
|
|
||||||
# lambda mo: telegram_escape_text_dict[
|
|
||||||
# mo.string[mo.start():mo.end()]], body)
|
|
||||||
|
|
||||||
# # prepare our payload based on HTML or TEXT
|
|
||||||
# payload['text'] = body
|
|
||||||
|
|
||||||
# Create a copy of the chat_ids list
|
# Create a copy of the chat_ids list
|
||||||
targets = list(self.targets)
|
targets = list(self.targets)
|
||||||
while len(targets):
|
while len(targets):
|
||||||
|
|
|
@ -785,7 +785,7 @@ class NotifyTwist(NotifyBase):
|
||||||
|
|
||||||
def __del__(self):
|
def __del__(self):
|
||||||
"""
|
"""
|
||||||
Deconstructor
|
Destructor
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
self.logout()
|
self.logout()
|
||||||
|
@ -808,14 +808,14 @@ class NotifyTwist(NotifyBase):
|
||||||
|
|
||||||
except ImportError: # pragma: no cover
|
except ImportError: # pragma: no cover
|
||||||
# The actual exception is `ModuleNotFoundError` however ImportError
|
# The actual exception is `ModuleNotFoundError` however ImportError
|
||||||
# grants us backwards compatiblity with versions of Python older
|
# grants us backwards compatibility with versions of Python older
|
||||||
# than v3.6
|
# than v3.6
|
||||||
|
|
||||||
# Python code that makes early calls to sys.exit() can cause
|
# Python code that makes early calls to sys.exit() can cause
|
||||||
# the __del__() code to run. However in some newer versions of
|
# the __del__() code to run. However, in some newer versions of
|
||||||
# Python, this causes the `sys` library to no longer be
|
# Python, this causes the `sys` library to no longer be
|
||||||
# available. The stack overflow also goes on to suggest that
|
# available. The stack overflow also goes on to suggest that
|
||||||
# it's not wise to use the __del__() as a deconstructor
|
# it's not wise to use the __del__() as a destructor
|
||||||
# which is the case here.
|
# which is the case here.
|
||||||
|
|
||||||
# https://stackoverflow.com/questions/67218341/\
|
# https://stackoverflow.com/questions/67218341/\
|
||||||
|
@ -827,6 +827,6 @@ class NotifyTwist(NotifyBase):
|
||||||
# /1481488/what-is-the-del-method-and-how-do-i-call-it
|
# /1481488/what-is-the-del-method-and-how-do-i-call-it
|
||||||
|
|
||||||
# At this time it seems clean to try to log out (if we can)
|
# At this time it seems clean to try to log out (if we can)
|
||||||
# but not throw any unessisary exceptions (like this one) to
|
# but not throw any unnecessary exceptions (like this one) to
|
||||||
# the end user if we don't have to.
|
# the end user if we don't have to.
|
||||||
pass
|
pass
|
||||||
|
|
|
@ -26,7 +26,6 @@
|
||||||
# See https://developer.twitter.com/en/docs/direct-messages/\
|
# See https://developer.twitter.com/en/docs/direct-messages/\
|
||||||
# sending-and-receiving/api-reference/new-event.html
|
# sending-and-receiving/api-reference/new-event.html
|
||||||
import re
|
import re
|
||||||
import six
|
|
||||||
import requests
|
import requests
|
||||||
from copy import deepcopy
|
from copy import deepcopy
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
@ -45,7 +44,7 @@ from ..attachment.AttachBase import AttachBase
|
||||||
IS_USER = re.compile(r'^\s*@?(?P<user>[A-Z0-9_]+)$', re.I)
|
IS_USER = re.compile(r'^\s*@?(?P<user>[A-Z0-9_]+)$', re.I)
|
||||||
|
|
||||||
|
|
||||||
class TwitterMessageMode(object):
|
class TwitterMessageMode:
|
||||||
"""
|
"""
|
||||||
Twitter Message Mode
|
Twitter Message Mode
|
||||||
"""
|
"""
|
||||||
|
@ -223,7 +222,7 @@ class NotifyTwitter(NotifyBase):
|
||||||
|
|
||||||
# Store our webhook mode
|
# Store our webhook mode
|
||||||
self.mode = None \
|
self.mode = None \
|
||||||
if not isinstance(mode, six.string_types) else mode.lower()
|
if not isinstance(mode, str) else mode.lower()
|
||||||
|
|
||||||
# Set Cache Flag
|
# Set Cache Flag
|
||||||
self.cache = cache
|
self.cache = cache
|
||||||
|
|
|
@ -39,24 +39,24 @@ from ..utils import validate_regex
|
||||||
from ..AppriseLocale import gettext_lazy as _
|
from ..AppriseLocale import gettext_lazy as _
|
||||||
|
|
||||||
|
|
||||||
class NotifyNexmo(NotifyBase):
|
class NotifyVonage(NotifyBase):
|
||||||
"""
|
"""
|
||||||
A wrapper for Nexmo Notifications
|
A wrapper for Vonage Notifications
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# The default descriptive name associated with the Notification
|
# The default descriptive name associated with the Notification
|
||||||
service_name = 'Nexmo'
|
service_name = 'Vonage'
|
||||||
|
|
||||||
# The services URL
|
# The services URL
|
||||||
service_url = 'https://dashboard.nexmo.com/'
|
service_url = 'https://dashboard.nexmo.com/'
|
||||||
|
|
||||||
# The default protocol
|
# The default protocol (nexmo kept for backwards compatibility)
|
||||||
secure_protocol = 'nexmo'
|
secure_protocol = ('vonage', 'nexmo')
|
||||||
|
|
||||||
# A URL that takes you to the setup/help of the specific protocol
|
# A URL that takes you to the setup/help of the specific protocol
|
||||||
setup_url = 'https://github.com/caronc/apprise/wiki/Notify_nexmo'
|
setup_url = 'https://github.com/caronc/apprise/wiki/Notify_nexmo'
|
||||||
|
|
||||||
# Nexmo uses the http protocol with JSON requests
|
# Vonage uses the http protocol with JSON requests
|
||||||
notify_url = 'https://rest.nexmo.com/sms/json'
|
notify_url = 'https://rest.nexmo.com/sms/json'
|
||||||
|
|
||||||
# The maximum length of the body
|
# The maximum length of the body
|
||||||
|
@ -124,7 +124,7 @@ class NotifyNexmo(NotifyBase):
|
||||||
},
|
},
|
||||||
|
|
||||||
# Default Time To Live
|
# Default Time To Live
|
||||||
# By default Nexmo attempt delivery for 72 hours, however the maximum
|
# By default Vonage attempt delivery for 72 hours, however the maximum
|
||||||
# effective value depends on the operator and is typically 24 - 48
|
# effective value depends on the operator and is typically 24 - 48
|
||||||
# hours. We recommend this value should be kept at its default or at
|
# hours. We recommend this value should be kept at its default or at
|
||||||
# least 30 minutes.
|
# least 30 minutes.
|
||||||
|
@ -140,15 +140,15 @@ class NotifyNexmo(NotifyBase):
|
||||||
def __init__(self, apikey, secret, source, targets=None, ttl=None,
|
def __init__(self, apikey, secret, source, targets=None, ttl=None,
|
||||||
**kwargs):
|
**kwargs):
|
||||||
"""
|
"""
|
||||||
Initialize Nexmo Object
|
Initialize Vonage Object
|
||||||
"""
|
"""
|
||||||
super(NotifyNexmo, self).__init__(**kwargs)
|
super(NotifyVonage, self).__init__(**kwargs)
|
||||||
|
|
||||||
# API Key (associated with project)
|
# API Key (associated with project)
|
||||||
self.apikey = validate_regex(
|
self.apikey = validate_regex(
|
||||||
apikey, *self.template_tokens['apikey']['regex'])
|
apikey, *self.template_tokens['apikey']['regex'])
|
||||||
if not self.apikey:
|
if not self.apikey:
|
||||||
msg = 'An invalid Nexmo API Key ' \
|
msg = 'An invalid Vonage API Key ' \
|
||||||
'({}) was specified.'.format(apikey)
|
'({}) was specified.'.format(apikey)
|
||||||
self.logger.warning(msg)
|
self.logger.warning(msg)
|
||||||
raise TypeError(msg)
|
raise TypeError(msg)
|
||||||
|
@ -157,7 +157,7 @@ class NotifyNexmo(NotifyBase):
|
||||||
self.secret = validate_regex(
|
self.secret = validate_regex(
|
||||||
secret, *self.template_tokens['secret']['regex'])
|
secret, *self.template_tokens['secret']['regex'])
|
||||||
if not self.secret:
|
if not self.secret:
|
||||||
msg = 'An invalid Nexmo API Secret ' \
|
msg = 'An invalid Vonage API Secret ' \
|
||||||
'({}) was specified.'.format(secret)
|
'({}) was specified.'.format(secret)
|
||||||
self.logger.warning(msg)
|
self.logger.warning(msg)
|
||||||
raise TypeError(msg)
|
raise TypeError(msg)
|
||||||
|
@ -173,7 +173,7 @@ class NotifyNexmo(NotifyBase):
|
||||||
|
|
||||||
if self.ttl < self.template_args['ttl']['min'] or \
|
if self.ttl < self.template_args['ttl']['min'] or \
|
||||||
self.ttl > self.template_args['ttl']['max']:
|
self.ttl > self.template_args['ttl']['max']:
|
||||||
msg = 'The Nexmo TTL specified ({}) is out of range.'\
|
msg = 'The Vonage TTL specified ({}) is out of range.'\
|
||||||
.format(self.ttl)
|
.format(self.ttl)
|
||||||
self.logger.warning(msg)
|
self.logger.warning(msg)
|
||||||
raise TypeError(msg)
|
raise TypeError(msg)
|
||||||
|
@ -211,7 +211,7 @@ class NotifyNexmo(NotifyBase):
|
||||||
|
|
||||||
def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs):
|
def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs):
|
||||||
"""
|
"""
|
||||||
Perform Nexmo Notification
|
Perform Vonage Notification
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# error tracking (used for function return)
|
# error tracking (used for function return)
|
||||||
|
@ -250,9 +250,9 @@ class NotifyNexmo(NotifyBase):
|
||||||
payload['to'] = target
|
payload['to'] = target
|
||||||
|
|
||||||
# Some Debug Logging
|
# Some Debug Logging
|
||||||
self.logger.debug('Nexmo POST URL: {} (cert_verify={})'.format(
|
self.logger.debug('Vonage POST URL: {} (cert_verify={})'.format(
|
||||||
self.notify_url, self.verify_certificate))
|
self.notify_url, self.verify_certificate))
|
||||||
self.logger.debug('Nexmo Payload: {}' .format(payload))
|
self.logger.debug('Vonage Payload: {}' .format(payload))
|
||||||
|
|
||||||
# Always call throttle before any remote server i/o is made
|
# Always call throttle before any remote server i/o is made
|
||||||
self.throttle()
|
self.throttle()
|
||||||
|
@ -269,11 +269,11 @@ class NotifyNexmo(NotifyBase):
|
||||||
if r.status_code != requests.codes.ok:
|
if r.status_code != requests.codes.ok:
|
||||||
# We had a problem
|
# We had a problem
|
||||||
status_str = \
|
status_str = \
|
||||||
NotifyNexmo.http_response_code_lookup(
|
NotifyVonage.http_response_code_lookup(
|
||||||
r.status_code)
|
r.status_code)
|
||||||
|
|
||||||
self.logger.warning(
|
self.logger.warning(
|
||||||
'Failed to send Nexmo notification to {}: '
|
'Failed to send Vonage notification to {}: '
|
||||||
'{}{}error={}.'.format(
|
'{}{}error={}.'.format(
|
||||||
target,
|
target,
|
||||||
status_str,
|
status_str,
|
||||||
|
@ -288,11 +288,12 @@ class NotifyNexmo(NotifyBase):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
else:
|
else:
|
||||||
self.logger.info('Sent Nexmo notification to %s.' % target)
|
self.logger.info(
|
||||||
|
'Sent Vonage notification to %s.' % target)
|
||||||
|
|
||||||
except requests.RequestException as e:
|
except requests.RequestException as e:
|
||||||
self.logger.warning(
|
self.logger.warning(
|
||||||
'A Connection error occurred sending Nexmo:%s '
|
'A Connection error occurred sending Vonage:%s '
|
||||||
'notification.' % target
|
'notification.' % target
|
||||||
)
|
)
|
||||||
self.logger.debug('Socket Exception: %s' % str(e))
|
self.logger.debug('Socket Exception: %s' % str(e))
|
||||||
|
@ -317,14 +318,14 @@ class NotifyNexmo(NotifyBase):
|
||||||
params.update(self.url_parameters(privacy=privacy, *args, **kwargs))
|
params.update(self.url_parameters(privacy=privacy, *args, **kwargs))
|
||||||
|
|
||||||
return '{schema}://{key}:{secret}@{source}/{targets}/?{params}'.format(
|
return '{schema}://{key}:{secret}@{source}/{targets}/?{params}'.format(
|
||||||
schema=self.secure_protocol,
|
schema=self.secure_protocol[0],
|
||||||
key=self.pprint(self.apikey, privacy, safe=''),
|
key=self.pprint(self.apikey, privacy, safe=''),
|
||||||
secret=self.pprint(
|
secret=self.pprint(
|
||||||
self.secret, privacy, mode=PrivacyMode.Secret, safe=''),
|
self.secret, privacy, mode=PrivacyMode.Secret, safe=''),
|
||||||
source=NotifyNexmo.quote(self.source, safe=''),
|
source=NotifyVonage.quote(self.source, safe=''),
|
||||||
targets='/'.join(
|
targets='/'.join(
|
||||||
[NotifyNexmo.quote(x, safe='') for x in self.targets]),
|
[NotifyVonage.quote(x, safe='') for x in self.targets]),
|
||||||
params=NotifyNexmo.urlencode(params))
|
params=NotifyVonage.urlencode(params))
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def parse_url(url):
|
def parse_url(url):
|
||||||
|
@ -340,46 +341,46 @@ class NotifyNexmo(NotifyBase):
|
||||||
|
|
||||||
# Get our entries; split_path() looks after unquoting content for us
|
# Get our entries; split_path() looks after unquoting content for us
|
||||||
# by default
|
# by default
|
||||||
results['targets'] = NotifyNexmo.split_path(results['fullpath'])
|
results['targets'] = NotifyVonage.split_path(results['fullpath'])
|
||||||
|
|
||||||
# The hostname is our source number
|
# The hostname is our source number
|
||||||
results['source'] = NotifyNexmo.unquote(results['host'])
|
results['source'] = NotifyVonage.unquote(results['host'])
|
||||||
|
|
||||||
# Get our account_side and auth_token from the user/pass config
|
# Get our account_side and auth_token from the user/pass config
|
||||||
results['apikey'] = NotifyNexmo.unquote(results['user'])
|
results['apikey'] = NotifyVonage.unquote(results['user'])
|
||||||
results['secret'] = NotifyNexmo.unquote(results['password'])
|
results['secret'] = NotifyVonage.unquote(results['password'])
|
||||||
|
|
||||||
# API Key
|
# API Key
|
||||||
if 'key' in results['qsd'] and len(results['qsd']['key']):
|
if 'key' in results['qsd'] and len(results['qsd']['key']):
|
||||||
# Extract the API Key from an argument
|
# Extract the API Key from an argument
|
||||||
results['apikey'] = \
|
results['apikey'] = \
|
||||||
NotifyNexmo.unquote(results['qsd']['key'])
|
NotifyVonage.unquote(results['qsd']['key'])
|
||||||
|
|
||||||
# API Secret
|
# API Secret
|
||||||
if 'secret' in results['qsd'] and len(results['qsd']['secret']):
|
if 'secret' in results['qsd'] and len(results['qsd']['secret']):
|
||||||
# Extract the API Secret from an argument
|
# Extract the API Secret from an argument
|
||||||
results['secret'] = \
|
results['secret'] = \
|
||||||
NotifyNexmo.unquote(results['qsd']['secret'])
|
NotifyVonage.unquote(results['qsd']['secret'])
|
||||||
|
|
||||||
# Support the 'from' and 'source' variable so that we can support
|
# Support the 'from' and 'source' variable so that we can support
|
||||||
# targets this way too.
|
# targets this way too.
|
||||||
# The 'from' makes it easier to use yaml configuration
|
# The 'from' makes it easier to use yaml configuration
|
||||||
if 'from' in results['qsd'] and len(results['qsd']['from']):
|
if 'from' in results['qsd'] and len(results['qsd']['from']):
|
||||||
results['source'] = \
|
results['source'] = \
|
||||||
NotifyNexmo.unquote(results['qsd']['from'])
|
NotifyVonage.unquote(results['qsd']['from'])
|
||||||
if 'source' in results['qsd'] and len(results['qsd']['source']):
|
if 'source' in results['qsd'] and len(results['qsd']['source']):
|
||||||
results['source'] = \
|
results['source'] = \
|
||||||
NotifyNexmo.unquote(results['qsd']['source'])
|
NotifyVonage.unquote(results['qsd']['source'])
|
||||||
|
|
||||||
# Support the 'ttl' variable
|
# Support the 'ttl' variable
|
||||||
if 'ttl' in results['qsd'] and len(results['qsd']['ttl']):
|
if 'ttl' in results['qsd'] and len(results['qsd']['ttl']):
|
||||||
results['ttl'] = \
|
results['ttl'] = \
|
||||||
NotifyNexmo.unquote(results['qsd']['ttl'])
|
NotifyVonage.unquote(results['qsd']['ttl'])
|
||||||
|
|
||||||
# Support the 'to' variable so that we can support rooms this way too
|
# Support the 'to' variable so that we can support rooms this way too
|
||||||
# The 'to' makes it easier to use yaml configuration
|
# The 'to' makes it easier to use yaml configuration
|
||||||
if 'to' in results['qsd'] and len(results['qsd']['to']):
|
if 'to' in results['qsd'] and len(results['qsd']['to']):
|
||||||
results['targets'] += \
|
results['targets'] += \
|
||||||
NotifyNexmo.parse_phone_no(results['qsd']['to'])
|
NotifyVonage.parse_phone_no(results['qsd']['to'])
|
||||||
|
|
||||||
return results
|
return results
|
|
@ -203,9 +203,9 @@ class NotifyWindows(NotifyBase):
|
||||||
|
|
||||||
self.logger.info('Sent Windows notification.')
|
self.logger.info('Sent Windows notification.')
|
||||||
|
|
||||||
except Exception:
|
except Exception as e:
|
||||||
self.logger.warning('Failed to send Windows notification.')
|
self.logger.warning('Failed to send Windows notification.')
|
||||||
self.logger.exception('Windows Exception')
|
self.logger.debug('Windows Exception: {}', str(e))
|
||||||
return False
|
return False
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
|
@ -24,7 +24,6 @@
|
||||||
# THE SOFTWARE.
|
# THE SOFTWARE.
|
||||||
|
|
||||||
import re
|
import re
|
||||||
import six
|
|
||||||
import requests
|
import requests
|
||||||
import base64
|
import base64
|
||||||
|
|
||||||
|
@ -157,11 +156,11 @@ class NotifyXML(NotifyBase):
|
||||||
</soapenv:Envelope>"""
|
</soapenv:Envelope>"""
|
||||||
|
|
||||||
self.fullpath = kwargs.get('fullpath')
|
self.fullpath = kwargs.get('fullpath')
|
||||||
if not isinstance(self.fullpath, six.string_types):
|
if not isinstance(self.fullpath, str):
|
||||||
self.fullpath = ''
|
self.fullpath = ''
|
||||||
|
|
||||||
self.method = self.template_args['method']['default'] \
|
self.method = self.template_args['method']['default'] \
|
||||||
if not isinstance(method, six.string_types) else method.upper()
|
if not isinstance(method, str) else method.upper()
|
||||||
|
|
||||||
if self.method not in METHODS:
|
if self.method not in METHODS:
|
||||||
msg = 'The method specified ({}) is invalid.'.format(method)
|
msg = 'The method specified ({}) is invalid.'.format(method)
|
||||||
|
@ -284,8 +283,7 @@ class NotifyXML(NotifyBase):
|
||||||
|
|
||||||
try:
|
try:
|
||||||
with open(attachment.path, 'rb') as f:
|
with open(attachment.path, 'rb') as f:
|
||||||
# Output must be in a DataURL format (that's what
|
# Prepare our Attachment in Base64
|
||||||
# PushSafer calls it):
|
|
||||||
entry = \
|
entry = \
|
||||||
'<Attachment filename="{}" mimetype="{}">'.format(
|
'<Attachment filename="{}" mimetype="{}">'.format(
|
||||||
NotifyXML.escape_html(
|
NotifyXML.escape_html(
|
||||||
|
@ -415,17 +413,9 @@ class NotifyXML(NotifyBase):
|
||||||
for x, y in results['qsd:'].items()}
|
for x, y in results['qsd:'].items()}
|
||||||
|
|
||||||
# Add our headers that the user can potentially over-ride if they wish
|
# Add our headers that the user can potentially over-ride if they wish
|
||||||
# to to our returned result set
|
# to to our returned result set and tidy entries by unquoting them
|
||||||
results['headers'] = results['qsd+']
|
|
||||||
if results['qsd-']:
|
|
||||||
results['headers'].update(results['qsd-'])
|
|
||||||
NotifyBase.logger.deprecate(
|
|
||||||
"minus (-) based XML header tokens are being "
|
|
||||||
"removed; use the plus (+) symbol instead.")
|
|
||||||
|
|
||||||
# Tidy our header entries by unquoting them
|
|
||||||
results['headers'] = {NotifyXML.unquote(x): NotifyXML.unquote(y)
|
results['headers'] = {NotifyXML.unquote(x): NotifyXML.unquote(y)
|
||||||
for x, y in results['headers'].items()}
|
for x, y in results['qsd+'].items()}
|
||||||
|
|
||||||
# Set method if not otherwise set
|
# Set method if not otherwise set
|
||||||
if 'method' in results['qsd'] and len(results['qsd']['method']):
|
if 'method' in results['qsd'] and len(results['qsd']['method']):
|
||||||
|
|
|
@ -1,203 +0,0 @@
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
import ssl
|
|
||||||
from os.path import isfile
|
|
||||||
import logging
|
|
||||||
|
|
||||||
|
|
||||||
# Default our global support flag
|
|
||||||
SLIXMPP_SUPPORT_AVAILABLE = False
|
|
||||||
|
|
||||||
try:
|
|
||||||
# Import slixmpp if available
|
|
||||||
import slixmpp
|
|
||||||
import asyncio
|
|
||||||
|
|
||||||
SLIXMPP_SUPPORT_AVAILABLE = True
|
|
||||||
|
|
||||||
except ImportError:
|
|
||||||
# No problem; we just simply can't support this plugin because we're
|
|
||||||
# either using Linux, or simply do not have slixmpp installed.
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class SliXmppAdapter(object):
|
|
||||||
"""
|
|
||||||
Wrapper to slixmpp
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
# Reference to XMPP client.
|
|
||||||
xmpp = None
|
|
||||||
|
|
||||||
# Whether everything succeeded
|
|
||||||
success = False
|
|
||||||
|
|
||||||
# The default protocol
|
|
||||||
protocol = 'xmpp'
|
|
||||||
|
|
||||||
# The default secure protocol
|
|
||||||
secure_protocol = 'xmpps'
|
|
||||||
|
|
||||||
# Taken from https://golang.org/src/crypto/x509/root_linux.go
|
|
||||||
CA_CERTIFICATE_FILE_LOCATIONS = [
|
|
||||||
# Debian/Ubuntu/Gentoo etc.
|
|
||||||
"/etc/ssl/certs/ca-certificates.crt",
|
|
||||||
# Fedora/RHEL 6
|
|
||||||
"/etc/pki/tls/certs/ca-bundle.crt",
|
|
||||||
# OpenSUSE
|
|
||||||
"/etc/ssl/ca-bundle.pem",
|
|
||||||
# OpenELEC
|
|
||||||
"/etc/pki/tls/cacert.pem",
|
|
||||||
# CentOS/RHEL 7
|
|
||||||
"/etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem",
|
|
||||||
]
|
|
||||||
|
|
||||||
# This entry is a bit hacky, but it allows us to unit-test this library
|
|
||||||
# in an environment that simply doesn't have the slixmpp package
|
|
||||||
# available to us.
|
|
||||||
#
|
|
||||||
# If anyone is seeing this had knows a better way of testing this
|
|
||||||
# outside of what is defined in test/test_xmpp_plugin.py, please
|
|
||||||
# let me know! :)
|
|
||||||
_enabled = SLIXMPP_SUPPORT_AVAILABLE
|
|
||||||
|
|
||||||
def __init__(self, host=None, port=None, secure=False,
|
|
||||||
verify_certificate=True, xep=None, jid=None, password=None,
|
|
||||||
body=None, subject=None, targets=None, before_message=None,
|
|
||||||
logger=None):
|
|
||||||
"""
|
|
||||||
Initialize our SliXmppAdapter object
|
|
||||||
"""
|
|
||||||
|
|
||||||
self.host = host
|
|
||||||
self.port = port
|
|
||||||
self.secure = secure
|
|
||||||
self.verify_certificate = verify_certificate
|
|
||||||
|
|
||||||
self.xep = xep
|
|
||||||
self.jid = jid
|
|
||||||
self.password = password
|
|
||||||
|
|
||||||
self.body = body
|
|
||||||
self.subject = subject
|
|
||||||
self.targets = targets
|
|
||||||
self.before_message = before_message
|
|
||||||
|
|
||||||
self.logger = logger or logging.getLogger(__name__)
|
|
||||||
|
|
||||||
# Use the Apprise log handlers for configuring the slixmpp logger.
|
|
||||||
apprise_logger = logging.getLogger('apprise')
|
|
||||||
sli_logger = logging.getLogger('slixmpp')
|
|
||||||
for handler in apprise_logger.handlers:
|
|
||||||
sli_logger.addHandler(handler)
|
|
||||||
sli_logger.setLevel(apprise_logger.level)
|
|
||||||
|
|
||||||
if not self.load():
|
|
||||||
raise ValueError("Invalid XMPP Configuration")
|
|
||||||
|
|
||||||
def load(self):
|
|
||||||
|
|
||||||
try:
|
|
||||||
asyncio.get_event_loop()
|
|
||||||
|
|
||||||
except RuntimeError:
|
|
||||||
# slixmpp can not handle not having an event_loop
|
|
||||||
# see: https://lab.louiz.org/poezio/slixmpp/-/issues/3456
|
|
||||||
# This is a work-around to this problem
|
|
||||||
asyncio.set_event_loop(asyncio.new_event_loop())
|
|
||||||
|
|
||||||
# Prepare our object
|
|
||||||
self.xmpp = slixmpp.ClientXMPP(self.jid, self.password)
|
|
||||||
|
|
||||||
# Register our session
|
|
||||||
self.xmpp.add_event_handler("session_start", self.session_start)
|
|
||||||
|
|
||||||
for xep in self.xep:
|
|
||||||
# Load xep entries
|
|
||||||
try:
|
|
||||||
self.xmpp.register_plugin('xep_{0:04d}'.format(xep))
|
|
||||||
|
|
||||||
except slixmpp.plugins.base.PluginNotFound:
|
|
||||||
self.logger.warning(
|
|
||||||
'Could not register plugin {}'.format(
|
|
||||||
'xep_{0:04d}'.format(xep)))
|
|
||||||
return False
|
|
||||||
|
|
||||||
if self.secure:
|
|
||||||
# Don't even try to use the outdated ssl.PROTOCOL_SSLx
|
|
||||||
self.xmpp.ssl_version = ssl.PROTOCOL_TLSv1
|
|
||||||
|
|
||||||
# If the python version supports it, use highest TLS version
|
|
||||||
# automatically
|
|
||||||
if hasattr(ssl, "PROTOCOL_TLS"):
|
|
||||||
# Use the best version of TLS available to us
|
|
||||||
self.xmpp.ssl_version = ssl.PROTOCOL_TLS
|
|
||||||
|
|
||||||
self.xmpp.ca_certs = None
|
|
||||||
if self.verify_certificate:
|
|
||||||
# Set the ca_certs variable for certificate verification
|
|
||||||
self.xmpp.ca_certs = next(
|
|
||||||
(cert for cert in self.CA_CERTIFICATE_FILE_LOCATIONS
|
|
||||||
if isfile(cert)), None)
|
|
||||||
|
|
||||||
if self.xmpp.ca_certs is None:
|
|
||||||
self.logger.warning(
|
|
||||||
'XMPP Secure comunication can not be verified; '
|
|
||||||
'no local CA certificate file')
|
|
||||||
return False
|
|
||||||
|
|
||||||
# If the user specified a port, skip SRV resolving, otherwise it is a
|
|
||||||
# lot easier to let slixmpp handle DNS instead of the user.
|
|
||||||
self.override_connection = \
|
|
||||||
None if not self.port else (self.host, self.port)
|
|
||||||
|
|
||||||
# We're good
|
|
||||||
return True
|
|
||||||
|
|
||||||
def process(self):
|
|
||||||
"""
|
|
||||||
Thread that handles the server/client i/o
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
# Instruct slixmpp to connect to the XMPP service.
|
|
||||||
if not self.xmpp.connect(
|
|
||||||
self.override_connection, use_ssl=self.secure):
|
|
||||||
return False
|
|
||||||
|
|
||||||
# Run the asyncio event loop, and return once disconnected,
|
|
||||||
# for any reason.
|
|
||||||
self.xmpp.process(forever=False)
|
|
||||||
|
|
||||||
return self.success
|
|
||||||
|
|
||||||
def session_start(self, *args, **kwargs):
|
|
||||||
"""
|
|
||||||
Session Manager
|
|
||||||
"""
|
|
||||||
|
|
||||||
targets = list(self.targets)
|
|
||||||
if not targets:
|
|
||||||
# We always default to notifying ourselves
|
|
||||||
targets.append(self.jid)
|
|
||||||
|
|
||||||
while len(targets) > 0:
|
|
||||||
|
|
||||||
# Get next target (via JID)
|
|
||||||
target = targets.pop(0)
|
|
||||||
|
|
||||||
# Invoke "before_message" event hook.
|
|
||||||
self.before_message()
|
|
||||||
|
|
||||||
# The message we wish to send, and the JID that will receive it.
|
|
||||||
self.xmpp.send_message(
|
|
||||||
mto=target, msubject=self.subject,
|
|
||||||
mbody=self.body, mtype='chat')
|
|
||||||
|
|
||||||
# Using wait=True ensures that the send queue will be
|
|
||||||
# emptied before ending the session.
|
|
||||||
self.xmpp.disconnect(wait=True)
|
|
||||||
|
|
||||||
# Toggle our success flag
|
|
||||||
self.success = True
|
|
|
@ -1,311 +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.
|
|
||||||
|
|
||||||
import re
|
|
||||||
|
|
||||||
from ..NotifyBase import NotifyBase
|
|
||||||
from ...URLBase import PrivacyMode
|
|
||||||
from ...common import NotifyType
|
|
||||||
from ...utils import parse_list
|
|
||||||
from ...AppriseLocale import gettext_lazy as _
|
|
||||||
from .SliXmppAdapter import SliXmppAdapter
|
|
||||||
|
|
||||||
# xep string parser
|
|
||||||
XEP_PARSE_RE = re.compile('^[^1-9]*(?P<xep>[1-9][0-9]{0,3})$')
|
|
||||||
|
|
||||||
|
|
||||||
class NotifyXMPP(NotifyBase):
|
|
||||||
"""
|
|
||||||
A wrapper for XMPP Notifications
|
|
||||||
"""
|
|
||||||
# Set our global enabled flag
|
|
||||||
enabled = SliXmppAdapter._enabled
|
|
||||||
|
|
||||||
requirements = {
|
|
||||||
# Define our required packaging in order to work
|
|
||||||
'packages_required': [
|
|
||||||
"slixmpp; python_version >= '3.7'",
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
# The default descriptive name associated with the Notification
|
|
||||||
service_name = 'XMPP'
|
|
||||||
|
|
||||||
# The services URL
|
|
||||||
service_url = 'https://xmpp.org/'
|
|
||||||
|
|
||||||
# The default protocol
|
|
||||||
protocol = 'xmpp'
|
|
||||||
|
|
||||||
# The default secure protocol
|
|
||||||
secure_protocol = 'xmpps'
|
|
||||||
|
|
||||||
# A URL that takes you to the setup/help of the specific protocol
|
|
||||||
setup_url = 'https://github.com/caronc/apprise/wiki/Notify_xmpp'
|
|
||||||
|
|
||||||
# Lower throttle rate for XMPP
|
|
||||||
request_rate_per_sec = 0.5
|
|
||||||
|
|
||||||
# Our XMPP Adapter we use to communicate through
|
|
||||||
_adapter = SliXmppAdapter if SliXmppAdapter._enabled else None
|
|
||||||
|
|
||||||
# Define object templates
|
|
||||||
templates = (
|
|
||||||
'{schema}://{user}:{password}@{host}',
|
|
||||||
'{schema}://{user}:{password}@{host}:{port}',
|
|
||||||
'{schema}://{user}:{password}@{host}/{targets}',
|
|
||||||
'{schema}://{user}:{password}@{host}:{port}/{targets}',
|
|
||||||
)
|
|
||||||
|
|
||||||
# Define our tokens
|
|
||||||
template_tokens = dict(NotifyBase.template_tokens, **{
|
|
||||||
'host': {
|
|
||||||
'name': _('Hostname'),
|
|
||||||
'type': 'string',
|
|
||||||
'required': True,
|
|
||||||
},
|
|
||||||
'port': {
|
|
||||||
'name': _('Port'),
|
|
||||||
'type': 'int',
|
|
||||||
'min': 1,
|
|
||||||
'max': 65535,
|
|
||||||
},
|
|
||||||
'user': {
|
|
||||||
'name': _('Username'),
|
|
||||||
'type': 'string',
|
|
||||||
'required': True,
|
|
||||||
},
|
|
||||||
'password': {
|
|
||||||
'name': _('Password'),
|
|
||||||
'type': 'string',
|
|
||||||
'private': True,
|
|
||||||
'required': True,
|
|
||||||
},
|
|
||||||
'target_jid': {
|
|
||||||
'name': _('Target JID'),
|
|
||||||
'type': 'string',
|
|
||||||
'map_to': 'targets',
|
|
||||||
},
|
|
||||||
'targets': {
|
|
||||||
'name': _('Targets'),
|
|
||||||
'type': 'list:string',
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
# Define our template arguments
|
|
||||||
template_args = dict(NotifyBase.template_args, **{
|
|
||||||
'to': {
|
|
||||||
'alias_of': 'targets',
|
|
||||||
},
|
|
||||||
'xep': {
|
|
||||||
'name': _('XEP'),
|
|
||||||
'type': 'list:string',
|
|
||||||
'prefix': 'xep-',
|
|
||||||
'regex': (r'^[1-9][0-9]{0,3}$', 'i'),
|
|
||||||
},
|
|
||||||
'jid': {
|
|
||||||
'name': _('Source JID'),
|
|
||||||
'type': 'string',
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
def __init__(self, targets=None, jid=None, xep=None, **kwargs):
|
|
||||||
"""
|
|
||||||
Initialize XMPP Object
|
|
||||||
"""
|
|
||||||
super(NotifyXMPP, self).__init__(**kwargs)
|
|
||||||
|
|
||||||
# JID Details:
|
|
||||||
# - JID's normally have an @ symbol in them, but it is not required
|
|
||||||
# - Each allowable portion of a JID MUST NOT be more than 1023 bytes
|
|
||||||
# in length.
|
|
||||||
# - JID's can identify resource paths at the end separated by slashes
|
|
||||||
# hence the following is valid: user@example.com/resource/path
|
|
||||||
|
|
||||||
# Since JID's can clash with URLs offered by aprise (specifically the
|
|
||||||
# resource paths we need to allow users an alternative character to
|
|
||||||
# represent the slashes. The grammer is defined here:
|
|
||||||
# https://xmpp.org/extensions/xep-0029.html as follows:
|
|
||||||
#
|
|
||||||
# <JID> ::= [<node>"@"]<domain>["/"<resource>]
|
|
||||||
# <node> ::= <conforming-char>[<conforming-char>]*
|
|
||||||
# <domain> ::= <hname>["."<hname>]*
|
|
||||||
# <resource> ::= <any-char>[<any-char>]*
|
|
||||||
# <hname> ::= <let>|<dig>[[<let>|<dig>|"-"]*<let>|<dig>]
|
|
||||||
# <let> ::= [a-z] | [A-Z]
|
|
||||||
# <dig> ::= [0-9]
|
|
||||||
# <conforming-char> ::= #x21 | [#x23-#x25] | [#x28-#x2E] |
|
|
||||||
# [#x30-#x39] | #x3B | #x3D | #x3F |
|
|
||||||
# [#x41-#x7E] | [#x80-#xD7FF] |
|
|
||||||
# [#xE000-#xFFFD] | [#x10000-#x10FFFF]
|
|
||||||
# <any-char> ::= [#x20-#xD7FF] | [#xE000-#xFFFD] |
|
|
||||||
# [#x10000-#x10FFFF]
|
|
||||||
|
|
||||||
# The best way to do this is to choose characters that aren't allowed
|
|
||||||
# in this case we will use comma and/or space.
|
|
||||||
|
|
||||||
# Assemble our jid using the information available to us:
|
|
||||||
self.jid = jid
|
|
||||||
|
|
||||||
if not (self.user or self.password):
|
|
||||||
# you must provide a jid/pass for this to work; if no password
|
|
||||||
# is specified then the user field acts as the password instead
|
|
||||||
# so we know that if there is no user specified, our url was
|
|
||||||
# really busted up.
|
|
||||||
msg = 'You must specify a XMPP password'
|
|
||||||
self.logger.warning(msg)
|
|
||||||
raise TypeError(msg)
|
|
||||||
|
|
||||||
# See https://xmpp.org/extensions/ for details on xep values
|
|
||||||
if xep is None:
|
|
||||||
# Default xep setting
|
|
||||||
self.xep = [
|
|
||||||
# xep_0030: Service Discovery
|
|
||||||
30,
|
|
||||||
# xep_0199: XMPP Ping
|
|
||||||
199,
|
|
||||||
]
|
|
||||||
|
|
||||||
else:
|
|
||||||
# Prepare the list
|
|
||||||
_xep = parse_list(xep)
|
|
||||||
self.xep = []
|
|
||||||
|
|
||||||
for xep in _xep:
|
|
||||||
result = XEP_PARSE_RE.match(xep)
|
|
||||||
if result is not None:
|
|
||||||
self.xep.append(int(result.group('xep')))
|
|
||||||
self.logger.debug('Loaded XMPP {}'.format(xep))
|
|
||||||
|
|
||||||
else:
|
|
||||||
self.logger.warning(
|
|
||||||
"Could not load XMPP {}".format(xep))
|
|
||||||
|
|
||||||
# By default we send ourselves a message
|
|
||||||
if targets:
|
|
||||||
self.targets = parse_list(targets)
|
|
||||||
self.targets[0] = self.targets[0][1:]
|
|
||||||
|
|
||||||
else:
|
|
||||||
self.targets = list()
|
|
||||||
|
|
||||||
def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs):
|
|
||||||
"""
|
|
||||||
Perform XMPP Notification
|
|
||||||
"""
|
|
||||||
|
|
||||||
# Detect our JID if it isn't otherwise specified
|
|
||||||
jid = self.jid
|
|
||||||
password = self.password
|
|
||||||
if not jid:
|
|
||||||
jid = '{}@{}'.format(self.user, self.host)
|
|
||||||
|
|
||||||
try:
|
|
||||||
# Communicate with XMPP.
|
|
||||||
xmpp_adapter = self._adapter(
|
|
||||||
host=self.host, port=self.port, secure=self.secure,
|
|
||||||
verify_certificate=self.verify_certificate, xep=self.xep,
|
|
||||||
jid=jid, password=password, body=body, subject=title,
|
|
||||||
targets=self.targets, before_message=self.throttle,
|
|
||||||
logger=self.logger)
|
|
||||||
|
|
||||||
except ValueError:
|
|
||||||
# We failed
|
|
||||||
return False
|
|
||||||
|
|
||||||
# Initialize XMPP machinery and begin processing the XML stream.
|
|
||||||
outcome = xmpp_adapter.process()
|
|
||||||
|
|
||||||
return outcome
|
|
||||||
|
|
||||||
def url(self, privacy=False, *args, **kwargs):
|
|
||||||
"""
|
|
||||||
Returns the URL built dynamically based on specified arguments.
|
|
||||||
"""
|
|
||||||
|
|
||||||
# Our URL parameters
|
|
||||||
params = self.url_parameters(privacy=privacy, *args, **kwargs)
|
|
||||||
|
|
||||||
if 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
|
|
||||||
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
|
|
||||||
jids = '%20'.join([NotifyXMPP.quote(x, safe='') for x in self.targets])
|
|
||||||
|
|
||||||
default_schema = self.secure_protocol if self.secure else self.protocol
|
|
||||||
|
|
||||||
auth = '{user}:{password}'.format(
|
|
||||||
user=NotifyXMPP.quote(self.user, safe=''),
|
|
||||||
password=self.pprint(
|
|
||||||
self.password, privacy, mode=PrivacyMode.Secret, safe=''))
|
|
||||||
|
|
||||||
return '{schema}://{auth}@{hostname}{port}/{jids}?{params}'.format(
|
|
||||||
auth=auth,
|
|
||||||
schema=default_schema,
|
|
||||||
# never encode hostname since we're expecting it to be a valid one
|
|
||||||
hostname=self.host,
|
|
||||||
port='' if not self.port
|
|
||||||
else ':{}'.format(self.port),
|
|
||||||
jids=jids,
|
|
||||||
params=NotifyXMPP.urlencode(params),
|
|
||||||
)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def parse_url(url):
|
|
||||||
"""
|
|
||||||
Parses the URL and returns enough arguments that can allow
|
|
||||||
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
|
|
||||||
|
|
||||||
# Get our targets; we ignore path slashes since they identify
|
|
||||||
# our resources
|
|
||||||
results['targets'] = NotifyXMPP.parse_list(results['fullpath'])
|
|
||||||
|
|
||||||
# Over-ride the xep plugins
|
|
||||||
if 'xep' in results['qsd'] and len(results['qsd']['xep']):
|
|
||||||
results['xep'] = \
|
|
||||||
NotifyXMPP.parse_list(results['qsd']['xep'])
|
|
||||||
|
|
||||||
# Over-ride the default (and detected) jid
|
|
||||||
if 'jid' in results['qsd'] and len(results['qsd']['jid']):
|
|
||||||
results['jid'] = NotifyXMPP.unquote(results['qsd']['jid'])
|
|
||||||
|
|
||||||
# Over-ride the default (and detected) jid
|
|
||||||
if 'to' in results['qsd'] and len(results['qsd']['to']):
|
|
||||||
results['targets'] += \
|
|
||||||
NotifyXMPP.parse_list(results['qsd']['to'])
|
|
||||||
|
|
||||||
return results
|
|
|
@ -63,10 +63,11 @@ from ..common import NotifyType
|
||||||
from ..utils import parse_list
|
from ..utils import parse_list
|
||||||
from ..utils import validate_regex
|
from ..utils import validate_regex
|
||||||
from ..utils import is_email
|
from ..utils import is_email
|
||||||
|
from ..utils import remove_suffix
|
||||||
from ..AppriseLocale import gettext_lazy as _
|
from ..AppriseLocale import gettext_lazy as _
|
||||||
|
|
||||||
# A Valid Bot Name
|
# A Valid Bot Name
|
||||||
VALIDATE_BOTNAME = re.compile(r'(?P<name>[A-Z0-9_]{1,32})(-bot)?', re.I)
|
VALIDATE_BOTNAME = re.compile(r'(?P<name>[A-Z0-9_-]{1,32})', re.I)
|
||||||
|
|
||||||
# Organization required as part of the API request
|
# Organization required as part of the API request
|
||||||
VALIDATE_ORG = re.compile(
|
VALIDATE_ORG = re.compile(
|
||||||
|
@ -122,7 +123,7 @@ class NotifyZulip(NotifyBase):
|
||||||
'botname': {
|
'botname': {
|
||||||
'name': _('Bot Name'),
|
'name': _('Bot Name'),
|
||||||
'type': 'string',
|
'type': 'string',
|
||||||
'regex': (r'^[A-Z0-9_]{1,32}(-bot)?$', 'i'),
|
'regex': (r'^[A-Z0-9_-]{1,32}$', 'i'),
|
||||||
},
|
},
|
||||||
'organization': {
|
'organization': {
|
||||||
'name': _('Organization'),
|
'name': _('Organization'),
|
||||||
|
@ -183,7 +184,9 @@ class NotifyZulip(NotifyBase):
|
||||||
raise TypeError
|
raise TypeError
|
||||||
|
|
||||||
# The botname
|
# The botname
|
||||||
self.botname = match.group('name')
|
botname = match.group('name')
|
||||||
|
botname = remove_suffix(botname, '-bot')
|
||||||
|
self.botname = botname
|
||||||
|
|
||||||
except (TypeError, AttributeError):
|
except (TypeError, AttributeError):
|
||||||
msg = 'The Zulip botname specified ({}) is invalid.'\
|
msg = 'The Zulip botname specified ({}) is invalid.'\
|
||||||
|
|
|
@ -24,7 +24,6 @@
|
||||||
# THE SOFTWARE.
|
# THE SOFTWARE.
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import six
|
|
||||||
import re
|
import re
|
||||||
import copy
|
import copy
|
||||||
|
|
||||||
|
@ -33,7 +32,6 @@ from os.path import abspath
|
||||||
|
|
||||||
# Used for testing
|
# Used for testing
|
||||||
from . import NotifyEmail as NotifyEmailBase
|
from . import NotifyEmail as NotifyEmailBase
|
||||||
from .NotifyXMPP import SliXmppAdapter
|
|
||||||
|
|
||||||
# NotifyBase object is passed in as a module not class
|
# NotifyBase object is passed in as a module not class
|
||||||
from . import NotifyBase
|
from . import NotifyBase
|
||||||
|
@ -42,6 +40,7 @@ from ..common import NotifyImageSize
|
||||||
from ..common import NOTIFY_IMAGE_SIZES
|
from ..common import NOTIFY_IMAGE_SIZES
|
||||||
from ..common import NotifyType
|
from ..common import NotifyType
|
||||||
from ..common import NOTIFY_TYPES
|
from ..common import NOTIFY_TYPES
|
||||||
|
from .. import common
|
||||||
from ..utils import parse_list
|
from ..utils import parse_list
|
||||||
from ..utils import cwe312_url
|
from ..utils import cwe312_url
|
||||||
from ..utils import GET_SCHEMA_RE
|
from ..utils import GET_SCHEMA_RE
|
||||||
|
@ -49,9 +48,6 @@ from ..logger import logger
|
||||||
from ..AppriseLocale import gettext_lazy as _
|
from ..AppriseLocale import gettext_lazy as _
|
||||||
from ..AppriseLocale import LazyTranslation
|
from ..AppriseLocale import LazyTranslation
|
||||||
|
|
||||||
# Maintains a mapping of all of the Notification services
|
|
||||||
SCHEMA_MAP = {}
|
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
# Reference
|
# Reference
|
||||||
'NotifyImageSize', 'NOTIFY_IMAGE_SIZES', 'NotifyType', 'NOTIFY_TYPES',
|
'NotifyImageSize', 'NOTIFY_IMAGE_SIZES', 'NotifyType', 'NOTIFY_TYPES',
|
||||||
|
@ -62,15 +58,8 @@ __all__ = [
|
||||||
|
|
||||||
# Tokenizer
|
# Tokenizer
|
||||||
'url_to_dict',
|
'url_to_dict',
|
||||||
|
|
||||||
# slixmpp access points (used for NotifyXMPP Testing)
|
|
||||||
'SliXmppAdapter',
|
|
||||||
]
|
]
|
||||||
|
|
||||||
# we mirror our base purely for the ability to reset everything; this
|
|
||||||
# is generally only used in testing and should not be used by developers
|
|
||||||
MODULE_MAP = {}
|
|
||||||
|
|
||||||
|
|
||||||
# Load our Lookup Matrix
|
# Load our Lookup Matrix
|
||||||
def __load_matrix(path=abspath(dirname(__file__)), name='apprise.plugins'):
|
def __load_matrix(path=abspath(dirname(__file__)), name='apprise.plugins'):
|
||||||
|
@ -113,12 +102,12 @@ def __load_matrix(path=abspath(dirname(__file__)), name='apprise.plugins'):
|
||||||
# Filter out non-notification modules
|
# Filter out non-notification modules
|
||||||
continue
|
continue
|
||||||
|
|
||||||
elif plugin_name in MODULE_MAP:
|
elif plugin_name in common.NOTIFY_MODULE_MAP:
|
||||||
# we're already handling this object
|
# we're already handling this object
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Add our plugin name to our module map
|
# Add our plugin name to our module map
|
||||||
MODULE_MAP[plugin_name] = {
|
common.NOTIFY_MODULE_MAP[plugin_name] = {
|
||||||
'plugin': plugin,
|
'plugin': plugin,
|
||||||
'module': module,
|
'module': module,
|
||||||
}
|
}
|
||||||
|
@ -130,40 +119,20 @@ def __load_matrix(path=abspath(dirname(__file__)), name='apprise.plugins'):
|
||||||
globals()[plugin_name] = plugin
|
globals()[plugin_name] = plugin
|
||||||
|
|
||||||
fn = getattr(plugin, 'schemas', None)
|
fn = getattr(plugin, 'schemas', None)
|
||||||
try:
|
schemas = set([]) if not callable(fn) else fn(plugin)
|
||||||
schemas = set([]) if not callable(fn) else fn(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)
|
|
||||||
|
|
||||||
elif isinstance(schema, (set, list, tuple)):
|
|
||||||
# Support iterables list types
|
|
||||||
for s in schema:
|
|
||||||
if isinstance(s, six.string_types):
|
|
||||||
schemas.add(s)
|
|
||||||
|
|
||||||
# map our schema to our plugin
|
# map our schema to our plugin
|
||||||
for schema in schemas:
|
for schema in schemas:
|
||||||
if schema in SCHEMA_MAP:
|
if schema in common.NOTIFY_SCHEMA_MAP:
|
||||||
logger.error(
|
logger.error(
|
||||||
"Notification schema ({}) mismatch detected - {} to {}"
|
"Notification schema ({}) mismatch detected - {} to {}"
|
||||||
.format(schema, SCHEMA_MAP[schema], plugin))
|
.format(schema, common.NOTIFY_SCHEMA_MAP[schema], plugin))
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Assign plugin
|
# Assign plugin
|
||||||
SCHEMA_MAP[schema] = plugin
|
common.NOTIFY_SCHEMA_MAP[schema] = plugin
|
||||||
|
|
||||||
return SCHEMA_MAP
|
return common.NOTIFY_SCHEMA_MAP
|
||||||
|
|
||||||
|
|
||||||
# Reset our Lookup Matrix
|
# Reset our Lookup Matrix
|
||||||
|
@ -174,10 +143,10 @@ def __reset_matrix():
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# Reset our schema map
|
# Reset our schema map
|
||||||
SCHEMA_MAP.clear()
|
common.NOTIFY_SCHEMA_MAP.clear()
|
||||||
|
|
||||||
# Iterate over our module map so we can clear out our __all__ and globals
|
# Iterate over our module map so we can clear out our __all__ and globals
|
||||||
for plugin_name in MODULE_MAP.keys():
|
for plugin_name in common.NOTIFY_MODULE_MAP.keys():
|
||||||
# Clear out globals
|
# Clear out globals
|
||||||
del globals()[plugin_name]
|
del globals()[plugin_name]
|
||||||
|
|
||||||
|
@ -185,7 +154,7 @@ def __reset_matrix():
|
||||||
__all__.remove(plugin_name)
|
__all__.remove(plugin_name)
|
||||||
|
|
||||||
# Clear out our module map
|
# Clear out our module map
|
||||||
MODULE_MAP.clear()
|
common.NOTIFY_MODULE_MAP.clear()
|
||||||
|
|
||||||
|
|
||||||
# Dynamically build our schema base
|
# Dynamically build our schema base
|
||||||
|
@ -242,7 +211,7 @@ def _sanitize_token(tokens, default_delimiter):
|
||||||
|
|
||||||
if 'regex' in tokens[key]:
|
if 'regex' in tokens[key]:
|
||||||
# Verify that we are a tuple; convert strings to tuples
|
# Verify that we are a tuple; convert strings to tuples
|
||||||
if isinstance(tokens[key]['regex'], six.string_types):
|
if isinstance(tokens[key]['regex'], str):
|
||||||
# Default tuple setup
|
# Default tuple setup
|
||||||
tokens[key]['regex'] = \
|
tokens[key]['regex'] = \
|
||||||
(tokens[key]['regex'], None)
|
(tokens[key]['regex'], None)
|
||||||
|
@ -483,7 +452,7 @@ def requirements(plugin):
|
||||||
|
|
||||||
# Get our required packages
|
# Get our required packages
|
||||||
_req_packages = plugin.requirements.get('packages_required')
|
_req_packages = plugin.requirements.get('packages_required')
|
||||||
if isinstance(_req_packages, six.string_types):
|
if isinstance(_req_packages, str):
|
||||||
# Convert to list
|
# Convert to list
|
||||||
_req_packages = [_req_packages]
|
_req_packages = [_req_packages]
|
||||||
|
|
||||||
|
@ -495,7 +464,7 @@ def requirements(plugin):
|
||||||
|
|
||||||
# Get our recommended packages
|
# Get our recommended packages
|
||||||
_opt_packages = plugin.requirements.get('packages_recommended')
|
_opt_packages = plugin.requirements.get('packages_recommended')
|
||||||
if isinstance(_opt_packages, six.string_types):
|
if isinstance(_opt_packages, str):
|
||||||
# Convert to list
|
# Convert to list
|
||||||
_opt_packages = [_opt_packages]
|
_opt_packages = [_opt_packages]
|
||||||
|
|
||||||
|
@ -554,14 +523,14 @@ def url_to_dict(url, secure_logging=True):
|
||||||
|
|
||||||
# Ensure our schema is always in lower case
|
# Ensure our schema is always in lower case
|
||||||
schema = schema.group('schema').lower()
|
schema = schema.group('schema').lower()
|
||||||
if schema not in SCHEMA_MAP:
|
if schema not in common.NOTIFY_SCHEMA_MAP:
|
||||||
# Give the user the benefit of the doubt that the user may be using
|
# Give the user the benefit of the doubt that the user may be using
|
||||||
# one of the URLs provided to them by their notification service.
|
# one of the URLs provided to them by their notification service.
|
||||||
# Before we fail for good, just scan all the plugins that support the
|
# Before we fail for good, just scan all the plugins that support the
|
||||||
# native_url() parse function
|
# native_url() parse function
|
||||||
results = \
|
results = \
|
||||||
next((r['plugin'].parse_native_url(_url)
|
next((r['plugin'].parse_native_url(_url)
|
||||||
for r in MODULE_MAP.values()
|
for r in common.NOTIFY_MODULE_MAP.values()
|
||||||
if r['plugin'].parse_native_url(_url) is not None),
|
if r['plugin'].parse_native_url(_url) is not None),
|
||||||
None)
|
None)
|
||||||
|
|
||||||
|
@ -576,14 +545,14 @@ def url_to_dict(url, secure_logging=True):
|
||||||
else:
|
else:
|
||||||
# Parse our url details of the server object as dictionary
|
# Parse our url details of the server object as dictionary
|
||||||
# containing all of the information parsed from our URL
|
# containing all of the information parsed from our URL
|
||||||
results = SCHEMA_MAP[schema].parse_url(_url)
|
results = common.NOTIFY_SCHEMA_MAP[schema].parse_url(_url)
|
||||||
if not results:
|
if not results:
|
||||||
logger.error('Unparseable {} URL {}'.format(
|
logger.error('Unparseable {} URL {}'.format(
|
||||||
SCHEMA_MAP[schema].service_name, loggable_url))
|
common.NOTIFY_SCHEMA_MAP[schema].service_name, loggable_url))
|
||||||
return None
|
return None
|
||||||
|
|
||||||
logger.trace('{} URL {} unpacked as:{}{}'.format(
|
logger.trace('{} URL {} unpacked as:{}{}'.format(
|
||||||
SCHEMA_MAP[schema].service_name, url,
|
common.NOTIFY_SCHEMA_MAP[schema].service_name, url,
|
||||||
os.linesep, os.linesep.join(
|
os.linesep, os.linesep.join(
|
||||||
['{}="{}"'.format(k, v) for k, v in results.items()])))
|
['{}="{}"'.format(k, v) for k, v in results.items()])))
|
||||||
|
|
||||||
|
|
|
@ -36,9 +36,7 @@ ASYNCIO_RUN_SUPPORT = \
|
||||||
(sys.version_info.major == 3 and sys.version_info.minor >= 7)
|
(sys.version_info.major == 3 and sys.version_info.minor >= 7)
|
||||||
|
|
||||||
|
|
||||||
# async reference produces a SyntaxError (E999) in Python v2.7
|
async def notify(coroutines):
|
||||||
# For this reason we turn on the noqa flag
|
|
||||||
async def notify(coroutines): # noqa: E999
|
|
||||||
"""
|
"""
|
||||||
An async wrapper to the AsyncNotifyBase.async_notify() calls allowing us
|
An async wrapper to the AsyncNotifyBase.async_notify() calls allowing us
|
||||||
to call gather() and collect the responses
|
to call gather() and collect the responses
|
||||||
|
@ -63,7 +61,20 @@ def tosync(cor, debug=False):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if ASYNCIO_RUN_SUPPORT:
|
if ASYNCIO_RUN_SUPPORT:
|
||||||
return asyncio.run(cor, debug=debug)
|
try:
|
||||||
|
loop = asyncio.get_running_loop()
|
||||||
|
|
||||||
|
except RuntimeError:
|
||||||
|
# There is no existing event loop, so we can start our own.
|
||||||
|
return asyncio.run(cor, debug=debug)
|
||||||
|
|
||||||
|
else:
|
||||||
|
# Enable debug mode
|
||||||
|
loop.set_debug(debug)
|
||||||
|
|
||||||
|
# Run the coroutine and wait for the result.
|
||||||
|
task = loop.create_task(cor)
|
||||||
|
return asyncio.ensure_future(task, loop=loop)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
# The Deprecated Way (<= Python v3.6)
|
# The Deprecated Way (<= Python v3.6)
|
||||||
|
@ -85,7 +96,7 @@ def tosync(cor, debug=False):
|
||||||
return loop.run_until_complete(cor)
|
return loop.run_until_complete(cor)
|
||||||
|
|
||||||
|
|
||||||
async def toasyncwrap(v): # noqa: E999
|
async def toasyncwrapvalue(v):
|
||||||
"""
|
"""
|
||||||
Create a coroutine that, when run, returns the provided value.
|
Create a coroutine that, when run, returns the provided value.
|
||||||
"""
|
"""
|
||||||
|
@ -93,12 +104,20 @@ async def toasyncwrap(v): # noqa: E999
|
||||||
return v
|
return v
|
||||||
|
|
||||||
|
|
||||||
|
async def toasyncwrap(fn):
|
||||||
|
"""
|
||||||
|
Create a coroutine that, when run, executes the provided function.
|
||||||
|
"""
|
||||||
|
|
||||||
|
return fn()
|
||||||
|
|
||||||
|
|
||||||
class AsyncNotifyBase(URLBase):
|
class AsyncNotifyBase(URLBase):
|
||||||
"""
|
"""
|
||||||
asyncio wrapper for the NotifyBase object
|
asyncio wrapper for the NotifyBase object
|
||||||
"""
|
"""
|
||||||
|
|
||||||
async def async_notify(self, *args, **kwargs): # noqa: E999
|
async def async_notify(self, *args, **kwargs):
|
||||||
"""
|
"""
|
||||||
Async Notification Wrapper
|
Async Notification Wrapper
|
||||||
"""
|
"""
|
||||||
|
@ -110,11 +129,11 @@ class AsyncNotifyBase(URLBase):
|
||||||
None, partial(self.notify, *args, **kwargs))
|
None, partial(self.notify, *args, **kwargs))
|
||||||
|
|
||||||
except TypeError:
|
except TypeError:
|
||||||
# These our our internally thrown notifications
|
# These are our internally thrown notifications
|
||||||
pass
|
pass
|
||||||
|
|
||||||
except Exception:
|
except Exception:
|
||||||
# A catch all so we don't have to abort early
|
# A catch-all so we don't have to abort early
|
||||||
# just because one of our plugins has a bug in it.
|
# just because one of our plugins has a bug in it.
|
||||||
logger.exception("Notification Exception")
|
logger.exception("Notification Exception")
|
||||||
|
|
||||||
|
|
|
@ -24,27 +24,55 @@
|
||||||
# THE SOFTWARE.
|
# THE SOFTWARE.
|
||||||
|
|
||||||
import re
|
import re
|
||||||
import six
|
import sys
|
||||||
import json
|
import json
|
||||||
import contextlib
|
import contextlib
|
||||||
import os
|
import os
|
||||||
|
import hashlib
|
||||||
from itertools import chain
|
from itertools import chain
|
||||||
from os.path import expanduser
|
from os.path import expanduser
|
||||||
from functools import reduce
|
from functools import reduce
|
||||||
from .common import MATCH_ALL_TAG
|
from . import common
|
||||||
from .common import MATCH_ALWAYS_TAG
|
from .logger import logger
|
||||||
|
|
||||||
try:
|
from urllib.parse import unquote
|
||||||
# Python 2.7
|
from urllib.parse import quote
|
||||||
from urllib import unquote
|
from urllib.parse import urlparse
|
||||||
from urllib import quote
|
from urllib.parse import urlencode as _urlencode
|
||||||
from urlparse import urlparse
|
|
||||||
|
|
||||||
except ImportError:
|
import importlib.util
|
||||||
# Python 3.x
|
|
||||||
from urllib.parse import unquote
|
|
||||||
from urllib.parse import quote
|
def import_module(path, name):
|
||||||
from urllib.parse import urlparse
|
"""
|
||||||
|
Load our module based on path
|
||||||
|
"""
|
||||||
|
# if path.endswith('test_module_detection0/a/hook.py'):
|
||||||
|
# import pdb
|
||||||
|
# pdb.set_trace()
|
||||||
|
|
||||||
|
spec = importlib.util.spec_from_file_location(name, path)
|
||||||
|
try:
|
||||||
|
module = importlib.util.module_from_spec(spec)
|
||||||
|
sys.modules[name] = module
|
||||||
|
|
||||||
|
spec.loader.exec_module(module)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
# module isn't loadable
|
||||||
|
del sys.modules[name]
|
||||||
|
module = None
|
||||||
|
|
||||||
|
logger.debug(
|
||||||
|
'Custom module exception raised from %s (name=%s) %s',
|
||||||
|
path, name, str(e))
|
||||||
|
|
||||||
|
return module
|
||||||
|
|
||||||
|
|
||||||
|
# Hash of all paths previously scanned so we don't waste effort/overhead doing
|
||||||
|
# it again
|
||||||
|
PATHS_PREVIOUSLY_SCANNED = set()
|
||||||
|
|
||||||
# URL Indexing Table for returns via parse_url()
|
# URL Indexing Table for returns via parse_url()
|
||||||
# The below accepts and scans for:
|
# The below accepts and scans for:
|
||||||
|
@ -107,6 +135,13 @@ NOTIFY_CUSTOM_COLON_TOKENS = re.compile(r'^:(?P<key>.*)\s*')
|
||||||
# Used for attempting to acquire the schema if the URL can't be parsed.
|
# Used for attempting to acquire the schema if the URL can't be parsed.
|
||||||
GET_SCHEMA_RE = re.compile(r'\s*(?P<schema>[a-z0-9]{2,9})://.*$', re.I)
|
GET_SCHEMA_RE = re.compile(r'\s*(?P<schema>[a-z0-9]{2,9})://.*$', re.I)
|
||||||
|
|
||||||
|
# Used for validating that a provided entry is indeed a schema
|
||||||
|
# this is slightly different then the GET_SCHEMA_RE above which
|
||||||
|
# insists the schema is only valid with a :// entry. this one
|
||||||
|
# extrapolates the individual entries
|
||||||
|
URL_DETAILS_RE = re.compile(
|
||||||
|
r'\s*(?P<schema>[a-z0-9]{2,9})(://(?P<base>.*))?$', re.I)
|
||||||
|
|
||||||
# Regular expression based and expanded from:
|
# Regular expression based and expanded from:
|
||||||
# http://www.regular-expressions.info/email.html
|
# http://www.regular-expressions.info/email.html
|
||||||
# Extended to support colon (:) delimiter for parsing names from the URL
|
# Extended to support colon (:) delimiter for parsing names from the URL
|
||||||
|
@ -167,7 +202,7 @@ UUID4_RE = re.compile(
|
||||||
REGEX_VALIDATE_LOOKUP = {}
|
REGEX_VALIDATE_LOOKUP = {}
|
||||||
|
|
||||||
|
|
||||||
class TemplateType(object):
|
class TemplateType:
|
||||||
"""
|
"""
|
||||||
Defines the different template types we can perform parsing on
|
Defines the different template types we can perform parsing on
|
||||||
"""
|
"""
|
||||||
|
@ -294,7 +329,7 @@ def is_uuid(uuid):
|
||||||
return True if match else False
|
return True if match else False
|
||||||
|
|
||||||
|
|
||||||
def is_phone_no(phone, min_len=11):
|
def is_phone_no(phone, min_len=10):
|
||||||
"""Determine if the specified entry is a phone number
|
"""Determine if the specified entry is a phone number
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
|
@ -477,16 +512,14 @@ def tidy_path(path):
|
||||||
# Windows
|
# Windows
|
||||||
path = TIDY_WIN_PATH_RE.sub('\\1', path.strip())
|
path = TIDY_WIN_PATH_RE.sub('\\1', path.strip())
|
||||||
# Linux
|
# Linux
|
||||||
path = TIDY_NUX_PATH_RE.sub('\\1', path.strip())
|
path = TIDY_NUX_PATH_RE.sub('\\1', path)
|
||||||
|
|
||||||
# Linux Based Trim
|
# Windows Based (final) Trim
|
||||||
path = TIDY_NUX_TRIM_RE.sub('\\1', path.strip())
|
path = expanduser(TIDY_WIN_TRIM_RE.sub('\\1', path))
|
||||||
# Windows Based Trim
|
|
||||||
path = expanduser(TIDY_WIN_TRIM_RE.sub('\\1', path.strip()))
|
|
||||||
return path
|
return path
|
||||||
|
|
||||||
|
|
||||||
def parse_qsd(qs):
|
def parse_qsd(qs, simple=False):
|
||||||
"""
|
"""
|
||||||
Query String Dictionary Builder
|
Query String Dictionary Builder
|
||||||
|
|
||||||
|
@ -505,6 +538,9 @@ def parse_qsd(qs):
|
||||||
|
|
||||||
This function returns a result object that fits with the apprise
|
This function returns a result object that fits with the apprise
|
||||||
expected parameters (populating the 'qsd' portion of the dictionary
|
expected parameters (populating the 'qsd' portion of the dictionary
|
||||||
|
|
||||||
|
if simple is set to true, then a ONE dictionary is returned and is not
|
||||||
|
sub-parsed for additional elements
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# Our return result set:
|
# Our return result set:
|
||||||
|
@ -520,7 +556,7 @@ def parse_qsd(qs):
|
||||||
'qsd+': {},
|
'qsd+': {},
|
||||||
'qsd-': {},
|
'qsd-': {},
|
||||||
'qsd:': {},
|
'qsd:': {},
|
||||||
}
|
} if not simple else {'qsd': {}}
|
||||||
|
|
||||||
pairs = [s2 for s1 in qs.split('&') for s2 in s1.split(';')]
|
pairs = [s2 for s1 in qs.split('&') for s2 in s1.split(';')]
|
||||||
for name_value in pairs:
|
for name_value in pairs:
|
||||||
|
@ -547,6 +583,10 @@ def parse_qsd(qs):
|
||||||
# content is always made lowercase for easy indexing
|
# content is always made lowercase for easy indexing
|
||||||
result['qsd'][key.lower().strip()] = val
|
result['qsd'][key.lower().strip()] = val
|
||||||
|
|
||||||
|
if simple:
|
||||||
|
# move along
|
||||||
|
continue
|
||||||
|
|
||||||
# Check for tokens that start with a addition/plus symbol (+)
|
# Check for tokens that start with a addition/plus symbol (+)
|
||||||
k = NOTIFY_CUSTOM_ADD_TOKENS.match(key)
|
k = NOTIFY_CUSTOM_ADD_TOKENS.match(key)
|
||||||
if k is not None:
|
if k is not None:
|
||||||
|
@ -568,7 +608,8 @@ def parse_qsd(qs):
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
def parse_url(url, default_schema='http', verify_host=True, strict_port=False):
|
def parse_url(url, default_schema='http', verify_host=True, strict_port=False,
|
||||||
|
simple=False):
|
||||||
"""A function that greatly simplifies the parsing of a url
|
"""A function that greatly simplifies the parsing of a url
|
||||||
specified by the end user.
|
specified by the end user.
|
||||||
|
|
||||||
|
@ -590,9 +631,42 @@ def parse_url(url, default_schema='http', verify_host=True, strict_port=False):
|
||||||
The function returns a simple dictionary with all of
|
The function returns a simple dictionary with all of
|
||||||
the parsed content within it and returns 'None' if the
|
the parsed content within it and returns 'None' if the
|
||||||
content could not be extracted.
|
content could not be extracted.
|
||||||
|
|
||||||
|
The output of 'http://hostname' would look like:
|
||||||
|
{
|
||||||
|
'schema': 'http',
|
||||||
|
'url': 'http://hostname',
|
||||||
|
'host': 'hostname',
|
||||||
|
|
||||||
|
'user': None,
|
||||||
|
'password': None,
|
||||||
|
'port': None,
|
||||||
|
'fullpath': None,
|
||||||
|
'path': None,
|
||||||
|
'query': None,
|
||||||
|
|
||||||
|
'qsd': {},
|
||||||
|
|
||||||
|
'qsd+': {},
|
||||||
|
'qsd-': {},
|
||||||
|
'qsd:': {}
|
||||||
|
}
|
||||||
|
|
||||||
|
The simple switch cleans the dictionary response to only include the
|
||||||
|
fields that were detected.
|
||||||
|
|
||||||
|
The output of 'http://hostname' with the simple flag set would look like:
|
||||||
|
{
|
||||||
|
'schema': 'http',
|
||||||
|
'url': 'http://hostname',
|
||||||
|
'host': 'hostname',
|
||||||
|
}
|
||||||
|
|
||||||
|
If the URL can't be parsed then None is returned
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if not isinstance(url, six.string_types):
|
if not isinstance(url, str):
|
||||||
# Simple error checking
|
# Simple error checking
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
@ -628,7 +702,7 @@ def parse_url(url, default_schema='http', verify_host=True, strict_port=False):
|
||||||
'qsd+': {},
|
'qsd+': {},
|
||||||
'qsd-': {},
|
'qsd-': {},
|
||||||
'qsd:': {},
|
'qsd:': {},
|
||||||
}
|
} if not simple else {}
|
||||||
|
|
||||||
qsdata = ''
|
qsdata = ''
|
||||||
match = VALID_URL_RE.search(url)
|
match = VALID_URL_RE.search(url)
|
||||||
|
@ -648,7 +722,7 @@ def parse_url(url, default_schema='http', verify_host=True, strict_port=False):
|
||||||
# Parse Query Arugments ?val=key&key=val
|
# Parse Query Arugments ?val=key&key=val
|
||||||
# while ensuring that all keys are lowercase
|
# while ensuring that all keys are lowercase
|
||||||
if qsdata:
|
if qsdata:
|
||||||
result.update(parse_qsd(qsdata))
|
result.update(parse_qsd(qsdata, simple=simple))
|
||||||
|
|
||||||
# Now do a proper extraction of data; http:// is just substitued in place
|
# Now do a proper extraction of data; http:// is just substitued in place
|
||||||
# to allow urlparse() to function as expected, we'll swap this back to the
|
# to allow urlparse() to function as expected, we'll swap this back to the
|
||||||
|
@ -671,8 +745,12 @@ def parse_url(url, default_schema='http', verify_host=True, strict_port=False):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
if not result['fullpath']:
|
if not result['fullpath']:
|
||||||
# Default
|
if not simple:
|
||||||
result['fullpath'] = None
|
# Default
|
||||||
|
result['fullpath'] = None
|
||||||
|
else:
|
||||||
|
# Remove entry
|
||||||
|
del result['fullpath']
|
||||||
|
|
||||||
else:
|
else:
|
||||||
# Using full path, extract query from path
|
# Using full path, extract query from path
|
||||||
|
@ -680,7 +758,11 @@ def parse_url(url, default_schema='http', verify_host=True, strict_port=False):
|
||||||
result['path'] = match.group('path')
|
result['path'] = match.group('path')
|
||||||
result['query'] = match.group('query')
|
result['query'] = match.group('query')
|
||||||
if not result['query']:
|
if not result['query']:
|
||||||
result['query'] = None
|
if not simple:
|
||||||
|
result['query'] = None
|
||||||
|
else:
|
||||||
|
del result['query']
|
||||||
|
|
||||||
try:
|
try:
|
||||||
(result['user'], result['host']) = \
|
(result['user'], result['host']) = \
|
||||||
re.split(r'[@]+', result['host'])[:2]
|
re.split(r'[@]+', result['host'])[:2]
|
||||||
|
@ -690,7 +772,7 @@ def parse_url(url, default_schema='http', verify_host=True, strict_port=False):
|
||||||
# and it's already assigned
|
# and it's already assigned
|
||||||
pass
|
pass
|
||||||
|
|
||||||
if result['user'] is not None:
|
if result.get('user') is not None:
|
||||||
try:
|
try:
|
||||||
(result['user'], result['password']) = \
|
(result['user'], result['password']) = \
|
||||||
re.split(r'[:]+', result['user'])[:2]
|
re.split(r'[:]+', result['user'])[:2]
|
||||||
|
@ -724,6 +806,9 @@ def parse_url(url, default_schema='http', verify_host=True, strict_port=False):
|
||||||
# Invalid Host Specified
|
# Invalid Host Specified
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
# Acquire our port (if defined)
|
||||||
|
_port = result.get('port')
|
||||||
|
|
||||||
if verify_host:
|
if verify_host:
|
||||||
# Verify and Validate our hostname
|
# Verify and Validate our hostname
|
||||||
result['host'] = is_hostname(result['host'])
|
result['host'] = is_hostname(result['host'])
|
||||||
|
@ -733,15 +818,14 @@ def parse_url(url, default_schema='http', verify_host=True, strict_port=False):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# Max port is 65535 and min is 1
|
# Max port is 65535 and min is 1
|
||||||
if isinstance(result['port'], int) and not ((
|
if isinstance(_port, int) and not ((
|
||||||
not strict_port or (
|
not strict_port or (
|
||||||
strict_port and
|
strict_port and _port > 0 and _port <= 65535))):
|
||||||
result['port'] > 0 and result['port'] <= 65535))):
|
|
||||||
|
|
||||||
# An invalid port was specified
|
# An invalid port was specified
|
||||||
return None
|
return None
|
||||||
|
|
||||||
elif pmatch and not isinstance(result['port'], int):
|
elif pmatch and not isinstance(_port, int):
|
||||||
if strict_port:
|
if strict_port:
|
||||||
# Store port
|
# Store port
|
||||||
result['port'] = pmatch.group('port').strip()
|
result['port'] = pmatch.group('port').strip()
|
||||||
|
@ -754,26 +838,34 @@ def parse_url(url, default_schema='http', verify_host=True, strict_port=False):
|
||||||
|
|
||||||
# Re-assemble cleaned up version of the url
|
# Re-assemble cleaned up version of the url
|
||||||
result['url'] = '%s://' % result['schema']
|
result['url'] = '%s://' % result['schema']
|
||||||
if isinstance(result['user'], six.string_types):
|
if isinstance(result.get('user'), str):
|
||||||
result['url'] += result['user']
|
result['url'] += result['user']
|
||||||
|
|
||||||
if isinstance(result['password'], six.string_types):
|
if isinstance(result.get('password'), str):
|
||||||
result['url'] += ':%s@' % result['password']
|
result['url'] += ':%s@' % result['password']
|
||||||
|
|
||||||
else:
|
else:
|
||||||
result['url'] += '@'
|
result['url'] += '@'
|
||||||
result['url'] += result['host']
|
result['url'] += result['host']
|
||||||
|
|
||||||
if result['port'] is not None:
|
if result.get('port') is not None:
|
||||||
try:
|
try:
|
||||||
result['url'] += ':%d' % result['port']
|
result['url'] += ':%d' % result['port']
|
||||||
|
|
||||||
except TypeError:
|
except TypeError:
|
||||||
result['url'] += ':%s' % result['port']
|
result['url'] += ':%s' % result['port']
|
||||||
|
|
||||||
if result['fullpath']:
|
elif 'port' in result and simple:
|
||||||
|
# Eliminate empty fields
|
||||||
|
del result['port']
|
||||||
|
|
||||||
|
if result.get('fullpath'):
|
||||||
result['url'] += result['fullpath']
|
result['url'] += result['fullpath']
|
||||||
|
|
||||||
|
if simple and not result['host']:
|
||||||
|
# simple mode does not carry over empty host names
|
||||||
|
del result['host']
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
@ -784,7 +876,7 @@ def parse_bool(arg, default=False):
|
||||||
If the content could not be parsed, then the default is returned.
|
If the content could not be parsed, then the default is returned.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if isinstance(arg, six.string_types):
|
if isinstance(arg, str):
|
||||||
# no = no - False
|
# no = no - False
|
||||||
# of = short for off - False
|
# of = short for off - False
|
||||||
# 0 = int for False
|
# 0 = int for False
|
||||||
|
@ -814,20 +906,15 @@ def parse_bool(arg, default=False):
|
||||||
return bool(arg)
|
return bool(arg)
|
||||||
|
|
||||||
|
|
||||||
def parse_phone_no(*args, **kwargs):
|
def parse_phone_no(*args, store_unparseable=True, **kwargs):
|
||||||
"""
|
"""
|
||||||
Takes a string containing phone numbers separated by comma's and/or spaces
|
Takes a string containing phone numbers separated by comma's and/or spaces
|
||||||
and returns a list.
|
and returns a list.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# 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)
|
|
||||||
|
|
||||||
result = []
|
result = []
|
||||||
for arg in args:
|
for arg in args:
|
||||||
if isinstance(arg, six.string_types) and arg:
|
if isinstance(arg, str) and arg:
|
||||||
_result = PHONE_NO_DETECTION_RE.findall(arg)
|
_result = PHONE_NO_DETECTION_RE.findall(arg)
|
||||||
if _result:
|
if _result:
|
||||||
result += _result
|
result += _result
|
||||||
|
@ -851,20 +938,15 @@ def parse_phone_no(*args, **kwargs):
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
def parse_call_sign(*args, **kwargs):
|
def parse_call_sign(*args, store_unparseable=True, **kwargs):
|
||||||
"""
|
"""
|
||||||
Takes a string containing ham radio call signs separated by
|
Takes a string containing ham radio call signs separated by
|
||||||
comma and/or spacesand returns a list.
|
comma and/or spacesand returns a list.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# 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)
|
|
||||||
|
|
||||||
result = []
|
result = []
|
||||||
for arg in args:
|
for arg in args:
|
||||||
if isinstance(arg, six.string_types) and arg:
|
if isinstance(arg, str) and arg:
|
||||||
_result = CALL_SIGN_DETECTION_RE.findall(arg)
|
_result = CALL_SIGN_DETECTION_RE.findall(arg)
|
||||||
if _result:
|
if _result:
|
||||||
result += _result
|
result += _result
|
||||||
|
@ -888,20 +970,15 @@ def parse_call_sign(*args, **kwargs):
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
def parse_emails(*args, **kwargs):
|
def parse_emails(*args, store_unparseable=True, **kwargs):
|
||||||
"""
|
"""
|
||||||
Takes a string containing emails separated by comma's and/or spaces and
|
Takes a string containing emails separated by comma's and/or spaces and
|
||||||
returns a list.
|
returns a list.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# 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)
|
|
||||||
|
|
||||||
result = []
|
result = []
|
||||||
for arg in args:
|
for arg in args:
|
||||||
if isinstance(arg, six.string_types) and arg:
|
if isinstance(arg, str) and arg:
|
||||||
_result = EMAIL_DETECTION_RE.findall(arg)
|
_result = EMAIL_DETECTION_RE.findall(arg)
|
||||||
if _result:
|
if _result:
|
||||||
result += _result
|
result += _result
|
||||||
|
@ -924,20 +1001,15 @@ def parse_emails(*args, **kwargs):
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
def parse_urls(*args, **kwargs):
|
def parse_urls(*args, store_unparseable=True, **kwargs):
|
||||||
"""
|
"""
|
||||||
Takes a string containing URLs separated by comma's and/or spaces and
|
Takes a string containing URLs separated by comma's and/or spaces and
|
||||||
returns a list.
|
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 = []
|
result = []
|
||||||
for arg in args:
|
for arg in args:
|
||||||
if isinstance(arg, six.string_types) and arg:
|
if isinstance(arg, str) and arg:
|
||||||
_result = URL_DETECTION_RE.findall(arg)
|
_result = URL_DETECTION_RE.findall(arg)
|
||||||
if _result:
|
if _result:
|
||||||
result += _result
|
result += _result
|
||||||
|
@ -960,6 +1032,75 @@ def parse_urls(*args, **kwargs):
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def url_assembly(**kwargs):
|
||||||
|
"""
|
||||||
|
This function reverses the parse_url() function by taking in the provided
|
||||||
|
result set and re-assembling a URL
|
||||||
|
|
||||||
|
"""
|
||||||
|
# Determine Authentication
|
||||||
|
auth = ''
|
||||||
|
if kwargs.get('user') is not None and \
|
||||||
|
kwargs.get('password') is not None:
|
||||||
|
|
||||||
|
auth = '{user}:{password}@'.format(
|
||||||
|
user=quote(kwargs.get('user'), safe=''),
|
||||||
|
password=quote(kwargs.get('password'), safe=''),
|
||||||
|
)
|
||||||
|
|
||||||
|
elif kwargs.get('user') is not None:
|
||||||
|
auth = '{user}@'.format(
|
||||||
|
user=quote(kwargs.get('user'), safe=''),
|
||||||
|
)
|
||||||
|
|
||||||
|
return '{schema}://{auth}{hostname}{port}{fullpath}{params}'.format(
|
||||||
|
schema='' if not kwargs.get('schema') else kwargs.get('schema'),
|
||||||
|
auth=auth,
|
||||||
|
# never encode hostname since we're expecting it to be a valid one
|
||||||
|
hostname='' if not kwargs.get('host') else kwargs.get('host', ''),
|
||||||
|
port='' if not kwargs.get('port')
|
||||||
|
else ':{}'.format(kwargs.get('port')),
|
||||||
|
fullpath=quote(kwargs.get('fullpath', ''), safe='/'),
|
||||||
|
params='' if not kwargs.get('qsd')
|
||||||
|
else '?{}'.format(urlencode(kwargs.get('qsd'))),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def urlencode(query, doseq=False, safe='', encoding=None, errors=None):
|
||||||
|
"""Convert a mapping object or a sequence of two-element tuples
|
||||||
|
|
||||||
|
Wrapper to Python's unquote while remaining compatible with both
|
||||||
|
Python 2 & 3 since the reference to this function changed between
|
||||||
|
versions.
|
||||||
|
|
||||||
|
The resulting string is a series of key=value pairs separated by '&'
|
||||||
|
characters, where both key and value are quoted using the quote()
|
||||||
|
function.
|
||||||
|
|
||||||
|
Note: If the dictionary entry contains an entry that is set to None
|
||||||
|
it is not included in the final result set. If you want to
|
||||||
|
pass in an empty variable, set it to an empty string.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
query (str): The dictionary to encode
|
||||||
|
doseq (:obj:`bool`, optional): Handle sequences
|
||||||
|
safe (:obj:`str`): non-ascii characters and URI specific ones that
|
||||||
|
you do not wish to escape (if detected). Setting this string
|
||||||
|
to an empty one causes everything to be escaped.
|
||||||
|
encoding (:obj:`str`, optional): encoding type
|
||||||
|
errors (:obj:`str`, errors): how to handle invalid character found
|
||||||
|
in encoded string (defined by encoding)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: The escaped parameters returned as a string
|
||||||
|
"""
|
||||||
|
# Tidy query by eliminating any records set to None
|
||||||
|
_query = {k: v for (k, v) in query.items() if v is not None}
|
||||||
|
return _urlencode(
|
||||||
|
_query, doseq=doseq, safe=safe, encoding=encoding,
|
||||||
|
errors=errors)
|
||||||
|
|
||||||
|
|
||||||
def parse_list(*args):
|
def parse_list(*args):
|
||||||
"""
|
"""
|
||||||
Take a string list and break it into a delimited
|
Take a string list and break it into a delimited
|
||||||
|
@ -983,7 +1124,7 @@ def parse_list(*args):
|
||||||
|
|
||||||
result = []
|
result = []
|
||||||
for arg in args:
|
for arg in args:
|
||||||
if isinstance(arg, six.string_types):
|
if isinstance(arg, str):
|
||||||
result += re.split(STRING_DELIMITERS, arg)
|
result += re.split(STRING_DELIMITERS, arg)
|
||||||
|
|
||||||
elif isinstance(arg, (set, list, tuple)):
|
elif isinstance(arg, (set, list, tuple)):
|
||||||
|
@ -992,14 +1133,15 @@ def parse_list(*args):
|
||||||
#
|
#
|
||||||
# filter() eliminates any empty entries
|
# filter() eliminates any empty entries
|
||||||
#
|
#
|
||||||
# Since Python v3 returns a filter (iterator) where-as Python v2 returned
|
# Since Python v3 returns a filter (iterator) whereas Python v2 returned
|
||||||
# a list, we need to change it into a list object to remain compatible with
|
# a list, we need to change it into a list object to remain compatible with
|
||||||
# both distribution types.
|
# both distribution types.
|
||||||
|
# TODO: Review after dropping support for Python 2.
|
||||||
return sorted([x for x in filter(bool, list(set(result)))])
|
return sorted([x for x in filter(bool, list(set(result)))])
|
||||||
|
|
||||||
|
|
||||||
def is_exclusive_match(logic, data, match_all=MATCH_ALL_TAG,
|
def is_exclusive_match(logic, data, match_all=common.MATCH_ALL_TAG,
|
||||||
match_always=MATCH_ALWAYS_TAG):
|
match_always=common.MATCH_ALWAYS_TAG):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
The data variable should always be a set of strings that the logic can be
|
The data variable should always be a set of strings that the logic can be
|
||||||
|
@ -1020,7 +1162,7 @@ def is_exclusive_match(logic, data, match_all=MATCH_ALL_TAG,
|
||||||
to all specified logic searches.
|
to all specified logic searches.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if isinstance(logic, six.string_types):
|
if isinstance(logic, str):
|
||||||
# Update our logic to support our delimiters
|
# Update our logic to support our delimiters
|
||||||
logic = set(parse_list(logic))
|
logic = set(parse_list(logic))
|
||||||
|
|
||||||
|
@ -1043,7 +1185,7 @@ def is_exclusive_match(logic, data, match_all=MATCH_ALL_TAG,
|
||||||
|
|
||||||
# Every entry here will be or'ed with the next
|
# Every entry here will be or'ed with the next
|
||||||
for entry in logic:
|
for entry in logic:
|
||||||
if not isinstance(entry, (six.string_types, list, tuple, set)):
|
if not isinstance(entry, (str, list, tuple, set)):
|
||||||
# Garbage entry in our logic found
|
# Garbage entry in our logic found
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
@ -1109,7 +1251,7 @@ def validate_regex(value, regex=r'[^\s]+', flags=re.I, strip=True, fmt=None):
|
||||||
'x': re.X,
|
'x': re.X,
|
||||||
}
|
}
|
||||||
|
|
||||||
if isinstance(flags, six.string_types):
|
if isinstance(flags, str):
|
||||||
# Convert a string of regular expression flags into their
|
# Convert a string of regular expression flags into their
|
||||||
# respected integer (expected) Python values and perform
|
# respected integer (expected) Python values and perform
|
||||||
# a bit-wise or on each match found:
|
# a bit-wise or on each match found:
|
||||||
|
@ -1164,7 +1306,7 @@ def cwe312_word(word, force=False, advanced=True, threshold=5):
|
||||||
reached, then content is considered secret
|
reached, then content is considered secret
|
||||||
"""
|
"""
|
||||||
|
|
||||||
class Variance(object):
|
class Variance:
|
||||||
"""
|
"""
|
||||||
A Simple List of Possible Character Variances
|
A Simple List of Possible Character Variances
|
||||||
"""
|
"""
|
||||||
|
@ -1177,7 +1319,7 @@ def cwe312_word(word, force=False, advanced=True, threshold=5):
|
||||||
# A Numerical Character (1234... etc)
|
# A Numerical Character (1234... etc)
|
||||||
NUMERIC = 'n'
|
NUMERIC = 'n'
|
||||||
|
|
||||||
if not (isinstance(word, six.string_types) and word.strip()):
|
if not (isinstance(word, str) and word.strip()):
|
||||||
# not a password if it's not something we even support
|
# not a password if it's not something we even support
|
||||||
return word
|
return word
|
||||||
|
|
||||||
|
@ -1379,3 +1521,161 @@ def apply_template(template, app_mode=TemplateType.RAW, **kwargs):
|
||||||
# to drop the '{{' and '}}' surrounding our match so that we can
|
# to drop the '{{' and '}}' surrounding our match so that we can
|
||||||
# re-index it back into our list
|
# re-index it back into our list
|
||||||
return mask_r.sub(lambda x: fn(kwargs[x.group()[2:-2].strip()]), template)
|
return mask_r.sub(lambda x: fn(kwargs[x.group()[2:-2].strip()]), template)
|
||||||
|
|
||||||
|
|
||||||
|
def remove_suffix(value, suffix):
|
||||||
|
"""
|
||||||
|
Removes a suffix from the end of a string.
|
||||||
|
"""
|
||||||
|
return value[:-len(suffix)] if value.endswith(suffix) else value
|
||||||
|
|
||||||
|
|
||||||
|
def module_detection(paths, cache=True):
|
||||||
|
"""
|
||||||
|
Iterates over a defined path for apprise decorators to load such as
|
||||||
|
@notify.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
# A simple restriction that we don't allow periods in the filename at all
|
||||||
|
# so it can't be hidden (Linux OS's) and it won't conflict with Python
|
||||||
|
# path naming. This also prevents us from loading any python file that
|
||||||
|
# starts with an underscore or dash
|
||||||
|
# We allow __init__.py as well
|
||||||
|
module_re = re.compile(
|
||||||
|
r'^(?P<name>[_a-z0-9][a-z0-9._-]+)?(\.py)?$', re.I)
|
||||||
|
|
||||||
|
if isinstance(paths, str):
|
||||||
|
paths = [paths, ]
|
||||||
|
|
||||||
|
if not paths or not isinstance(paths, (tuple, list)):
|
||||||
|
# We're done
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _import_module(path):
|
||||||
|
# Since our plugin name can conflict (as a module) with another
|
||||||
|
# we want to generate random strings to avoid steping on
|
||||||
|
# another's namespace
|
||||||
|
module_name = hashlib.sha1(path.encode('utf-8')).hexdigest()
|
||||||
|
module_pyname = "{prefix}.{name}".format(
|
||||||
|
prefix='apprise.custom.module', name=module_name)
|
||||||
|
|
||||||
|
if module_pyname in common.NOTIFY_CUSTOM_MODULE_MAP:
|
||||||
|
# First clear out existing entries
|
||||||
|
for schema in common.\
|
||||||
|
NOTIFY_CUSTOM_MODULE_MAP[module_pyname]['notify'] \
|
||||||
|
.keys():
|
||||||
|
# Remove any mapped modules to this file
|
||||||
|
del common.NOTIFY_SCHEMA_MAP[schema]
|
||||||
|
|
||||||
|
# Reset
|
||||||
|
del common.NOTIFY_CUSTOM_MODULE_MAP[module_pyname]
|
||||||
|
|
||||||
|
# Load our module
|
||||||
|
module = import_module(path, module_pyname)
|
||||||
|
if not module:
|
||||||
|
# No problem, we can't use this object
|
||||||
|
logger.warning('Failed to load custom module: %s', _path)
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Print our loaded modules if any
|
||||||
|
if module_pyname in common.NOTIFY_CUSTOM_MODULE_MAP:
|
||||||
|
logger.debug(
|
||||||
|
'Loaded custom module: %s (name=%s)',
|
||||||
|
_path, module_name)
|
||||||
|
|
||||||
|
for schema, meta in common.\
|
||||||
|
NOTIFY_CUSTOM_MODULE_MAP[module_pyname]['notify']\
|
||||||
|
.items():
|
||||||
|
|
||||||
|
logger.info('Loaded custom notification: %s://', schema)
|
||||||
|
else:
|
||||||
|
# The code reaches here if we successfully loaded the Python
|
||||||
|
# module but no hooks/triggers were found. So we can safely
|
||||||
|
# just remove/ignore this entry
|
||||||
|
del sys.modules[module_pyname]
|
||||||
|
return None
|
||||||
|
|
||||||
|
# end of _import_module()
|
||||||
|
return None
|
||||||
|
|
||||||
|
for _path in paths:
|
||||||
|
path = os.path.abspath(os.path.expanduser(_path))
|
||||||
|
if (cache and path in PATHS_PREVIOUSLY_SCANNED) \
|
||||||
|
or not os.path.exists(path):
|
||||||
|
# We're done as we've already scanned this
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Store our path as a way of hashing it has been handled
|
||||||
|
PATHS_PREVIOUSLY_SCANNED.add(path)
|
||||||
|
|
||||||
|
if os.path.isdir(path) and not \
|
||||||
|
os.path.isfile(os.path.join(path, '__init__.py')):
|
||||||
|
|
||||||
|
logger.debug('Scanning for custom plugins in: %s', path)
|
||||||
|
for entry in os.listdir(path):
|
||||||
|
re_match = module_re.match(entry)
|
||||||
|
if not re_match:
|
||||||
|
# keep going
|
||||||
|
logger.trace('Plugin Scan: Ignoring %s', entry)
|
||||||
|
continue
|
||||||
|
|
||||||
|
new_path = os.path.join(path, entry)
|
||||||
|
if os.path.isdir(new_path):
|
||||||
|
# Update our path
|
||||||
|
new_path = os.path.join(path, entry, '__init__.py')
|
||||||
|
if not os.path.isfile(new_path):
|
||||||
|
logger.trace(
|
||||||
|
'Plugin Scan: Ignoring %s',
|
||||||
|
os.path.join(path, entry))
|
||||||
|
continue
|
||||||
|
|
||||||
|
if not cache or \
|
||||||
|
(cache and new_path not in PATHS_PREVIOUSLY_SCANNED):
|
||||||
|
# Load our module
|
||||||
|
_import_module(new_path)
|
||||||
|
|
||||||
|
# Add our subdir path
|
||||||
|
PATHS_PREVIOUSLY_SCANNED.add(new_path)
|
||||||
|
else:
|
||||||
|
if os.path.isdir(path):
|
||||||
|
# This logic is safe to apply because we already validated
|
||||||
|
# the directories state above; update our path
|
||||||
|
path = os.path.join(path, '__init__.py')
|
||||||
|
if cache and path in PATHS_PREVIOUSLY_SCANNED:
|
||||||
|
continue
|
||||||
|
|
||||||
|
PATHS_PREVIOUSLY_SCANNED.add(path)
|
||||||
|
|
||||||
|
# directly load as is
|
||||||
|
re_match = module_re.match(os.path.basename(path))
|
||||||
|
# must be a match and must have a .py extension
|
||||||
|
if not re_match or not re_match.group(1):
|
||||||
|
# keep going
|
||||||
|
logger.trace('Plugin Scan: Ignoring %s', path)
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Load our module
|
||||||
|
_import_module(path)
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def dict_full_update(dict1, dict2):
|
||||||
|
"""
|
||||||
|
Takes 2 dictionaries (dict1 and dict2) that contain sub-dictionaries and
|
||||||
|
gracefully merges them into dict1.
|
||||||
|
|
||||||
|
This is similar to: dict1.update(dict2) except that internal dictionaries
|
||||||
|
are also recursively applied.
|
||||||
|
"""
|
||||||
|
def _merge(dict1, dict2):
|
||||||
|
for k in dict2:
|
||||||
|
if k in dict1 and isinstance(dict1[k], dict) \
|
||||||
|
and isinstance(dict2[k], dict):
|
||||||
|
_merge(dict1[k], dict2[k])
|
||||||
|
else:
|
||||||
|
dict1[k] = dict2[k]
|
||||||
|
|
||||||
|
_merge(dict1, dict2)
|
||||||
|
return
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
# Bazarr dependencies
|
# Bazarr dependencies
|
||||||
aniso8601==9.0.1
|
aniso8601==9.0.1
|
||||||
argparse==1.4.0
|
argparse==1.4.0
|
||||||
apprise==0.9.8.3
|
apprise==1.1.0
|
||||||
apscheduler==3.8.1
|
apscheduler==3.8.1
|
||||||
attrs==22.1.0
|
attrs==22.1.0
|
||||||
charamel==1.0.0
|
charamel==1.0.0
|
||||||
|
|
Loading…
Reference in a new issue