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.
|
||||
|
||||
import os
|
||||
import six
|
||||
from itertools import chain
|
||||
from .common import NotifyType
|
||||
from .common import MATCH_ALL_TAG
|
||||
from .common import MATCH_ALWAYS_TAG
|
||||
from . import common
|
||||
from .conversion import convert_between
|
||||
from .utils import is_exclusive_match
|
||||
from .utils import parse_list
|
||||
from .utils import parse_urls
|
||||
from .utils import cwe312_url
|
||||
from .logger import logger
|
||||
|
||||
from .AppriseAsset import AppriseAsset
|
||||
from .AppriseConfig import AppriseConfig
|
||||
from .AppriseAttachment import AppriseAttachment
|
||||
|
@ -47,13 +43,13 @@ from .plugins.NotifyBase import NotifyBase
|
|||
from . import plugins
|
||||
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
|
||||
# TODO: Review after dropping support for Python 2.
|
||||
from . import py3compat
|
||||
ASYNCIO_SUPPORT = not six.PY2
|
||||
|
||||
|
||||
class Apprise(object):
|
||||
class Apprise:
|
||||
"""
|
||||
Our Notification Manager
|
||||
|
||||
|
@ -127,7 +123,7 @@ class Apprise(object):
|
|||
# Prepare our Asset Object
|
||||
asset = asset if isinstance(asset, AppriseAsset) else AppriseAsset()
|
||||
|
||||
if isinstance(url, six.string_types):
|
||||
if isinstance(url, str):
|
||||
# Acquire our url tokens
|
||||
results = plugins.url_to_dict(
|
||||
url, secure_logging=asset.secure_logging)
|
||||
|
@ -141,7 +137,7 @@ class Apprise(object):
|
|||
# We already have our result set
|
||||
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
|
||||
# we can index into our loaded plugins
|
||||
logger.error('Dictionary does not include a "schema" entry.')
|
||||
|
@ -164,7 +160,7 @@ class Apprise(object):
|
|||
type(url))
|
||||
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)
|
||||
#
|
||||
|
@ -184,12 +180,13 @@ class Apprise(object):
|
|||
try:
|
||||
# Attempt to create an instance of our plugin using the parsed
|
||||
# URL information
|
||||
plugin = plugins.SCHEMA_MAP[results['schema']](**results)
|
||||
plugin = common.NOTIFY_SCHEMA_MAP[results['schema']](**results)
|
||||
|
||||
# Create log entry of loaded URL
|
||||
logger.debug(
|
||||
'Loaded {} URL: {}'.format(
|
||||
plugins.SCHEMA_MAP[results['schema']].service_name,
|
||||
common.
|
||||
NOTIFY_SCHEMA_MAP[results['schema']].service_name,
|
||||
plugin.url(privacy=asset.secure_logging)))
|
||||
|
||||
except Exception:
|
||||
|
@ -200,14 +197,15 @@ class Apprise(object):
|
|||
# the arguments are invalid or can not be used.
|
||||
logger.error(
|
||||
'Could not load {} URL: {}'.format(
|
||||
plugins.SCHEMA_MAP[results['schema']].service_name,
|
||||
common.
|
||||
NOTIFY_SCHEMA_MAP[results['schema']].service_name,
|
||||
loggable_url))
|
||||
return None
|
||||
|
||||
else:
|
||||
# Attempt to create an instance of our plugin using the parsed
|
||||
# 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:
|
||||
#
|
||||
|
@ -248,7 +246,7 @@ class Apprise(object):
|
|||
# prepare default asset
|
||||
asset = self.asset
|
||||
|
||||
if isinstance(servers, six.string_types):
|
||||
if isinstance(servers, str):
|
||||
# build our server list
|
||||
servers = parse_urls(servers)
|
||||
if len(servers) == 0:
|
||||
|
@ -276,7 +274,7 @@ class Apprise(object):
|
|||
self.servers.append(_server)
|
||||
continue
|
||||
|
||||
elif not isinstance(_server, (six.string_types, dict)):
|
||||
elif not isinstance(_server, (str, dict)):
|
||||
logger.error(
|
||||
"An invalid notification (type={}) was specified.".format(
|
||||
type(_server)))
|
||||
|
@ -305,9 +303,9 @@ class Apprise(object):
|
|||
"""
|
||||
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
|
||||
# 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
|
||||
for entry in self.servers:
|
||||
|
@ -338,23 +336,24 @@ class Apprise(object):
|
|||
for server in servers:
|
||||
# Apply our tag matching based on our defined logic
|
||||
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):
|
||||
yield server
|
||||
return
|
||||
|
||||
def notify(self, body, title='', notify_type=NotifyType.INFO,
|
||||
body_format=None, tag=MATCH_ALL_TAG, match_always=True,
|
||||
def notify(self, body, title='', notify_type=common.NotifyType.INFO,
|
||||
body_format=None, tag=common.MATCH_ALL_TAG, match_always=True,
|
||||
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
|
||||
be converted to HTML if the Notification type expects this.
|
||||
|
||||
if the tag is specified (either a string or a set/list/tuple
|
||||
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)
|
||||
|
||||
This function returns True if all notifications were successfully
|
||||
|
@ -363,60 +362,33 @@ class Apprise(object):
|
|||
simply having empty configuration files that were read.
|
||||
|
||||
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
|
||||
|
||||
Set interpret_escapes to True if you want to pre-escape a string
|
||||
such as turning a \n into an actual new line, etc.
|
||||
"""
|
||||
|
||||
if ASYNCIO_SUPPORT:
|
||||
return py3compat.asyncio.tosync(
|
||||
self.async_notify(
|
||||
body, title,
|
||||
notify_type=notify_type, body_format=body_format,
|
||||
tag=tag, match_always=match_always, attach=attach,
|
||||
interpret_escapes=interpret_escapes,
|
||||
),
|
||||
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
|
||||
return py3compat.asyncio.tosync(
|
||||
self.async_notify(
|
||||
body, title,
|
||||
notify_type=notify_type, body_format=body_format,
|
||||
tag=tag, match_always=match_always, attach=attach,
|
||||
interpret_escapes=interpret_escapes,
|
||||
),
|
||||
debug=self.debug
|
||||
)
|
||||
|
||||
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
|
||||
awaited on, even if it is missing the async keyword in its signature.
|
||||
(This is omitted to preserve syntax compatibility with Python 2.)
|
||||
|
||||
The arguments are identical to those of Apprise.notify(). This method
|
||||
is not available in Python 2.
|
||||
"""
|
||||
The arguments are identical to those of Apprise.notify().
|
||||
|
||||
"""
|
||||
try:
|
||||
coroutines = list(
|
||||
self._notifyall(
|
||||
|
@ -424,7 +396,7 @@ class Apprise(object):
|
|||
|
||||
except TypeError:
|
||||
# No notifications sent, and there was an internal error.
|
||||
return py3compat.asyncio.toasyncwrap(False)
|
||||
return py3compat.asyncio.toasyncwrapvalue(False)
|
||||
|
||||
else:
|
||||
if len(coroutines) > 0:
|
||||
|
@ -433,7 +405,7 @@ class Apprise(object):
|
|||
|
||||
else:
|
||||
# No notifications sent.
|
||||
return py3compat.asyncio.toasyncwrap(None)
|
||||
return py3compat.asyncio.toasyncwrapvalue(None)
|
||||
|
||||
@staticmethod
|
||||
def _notifyhandler(server, **kwargs):
|
||||
|
@ -470,13 +442,14 @@ class Apprise(object):
|
|||
# Send the notification immediately, and wrap the result in a
|
||||
# coroutine.
|
||||
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,
|
||||
body_format=None, tag=MATCH_ALL_TAG, match_always=True,
|
||||
attach=None, interpret_escapes=None):
|
||||
def _notifyall(self, handler, body, title='',
|
||||
notify_type=common.NotifyType.INFO, body_format=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
|
||||
and only argument supplied to handler is the server, and the keyword
|
||||
|
@ -485,7 +458,7 @@ class Apprise(object):
|
|||
|
||||
if len(self) == 0:
|
||||
# Nothing to notify
|
||||
msg = "There are service(s) to notify"
|
||||
msg = "There are no service(s) to notify"
|
||||
logger.error(msg)
|
||||
raise TypeError(msg)
|
||||
|
||||
|
@ -495,23 +468,11 @@ class Apprise(object):
|
|||
raise TypeError(msg)
|
||||
|
||||
try:
|
||||
if six.PY2:
|
||||
# Python 2.7 encoding support isn't the greatest, so we try
|
||||
# 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 title and isinstance(title, bytes):
|
||||
title = title.decode(self.asset.encoding)
|
||||
|
||||
if body and isinstance(body, str): # noqa: F821
|
||||
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)
|
||||
if body and isinstance(body, bytes):
|
||||
body = body.decode(self.asset.encoding)
|
||||
|
||||
except UnicodeDecodeError:
|
||||
msg = 'The content passed into Apprise was not of encoding ' \
|
||||
|
@ -579,43 +540,12 @@ class Apprise(object):
|
|||
.encode('ascii', 'backslashreplace')\
|
||||
.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:
|
||||
# Must be of string type
|
||||
msg = 'Failed to escape message body'
|
||||
logger.error(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(
|
||||
server,
|
||||
body=conversion_body_map[server.notify_format],
|
||||
|
@ -641,7 +571,7 @@ class Apprise(object):
|
|||
'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
|
||||
# their status:
|
||||
|
||||
|
@ -650,7 +580,10 @@ class Apprise(object):
|
|||
'service_url': getattr(plugin, 'service_url', None),
|
||||
'setup_url': getattr(plugin, 'setup_url', None),
|
||||
# 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
|
||||
|
@ -665,12 +598,12 @@ class Apprise(object):
|
|||
|
||||
# Standard protocol(s) should be None or a tuple
|
||||
protocols = getattr(plugin, 'protocol', None)
|
||||
if isinstance(protocols, six.string_types):
|
||||
if isinstance(protocols, str):
|
||||
protocols = (protocols, )
|
||||
|
||||
# Secure protocol(s) should be None or a tuple
|
||||
secure_protocols = getattr(plugin, 'secure_protocol', None)
|
||||
if isinstance(secure_protocols, six.string_types):
|
||||
if isinstance(secure_protocols, str):
|
||||
secure_protocols = (secure_protocols, )
|
||||
|
||||
# Add our protocol details to our content
|
||||
|
@ -775,15 +708,8 @@ class Apprise(object):
|
|||
|
||||
def __bool__(self):
|
||||
"""
|
||||
Allows the Apprise object to be wrapped in an Python 3.x based 'if
|
||||
statement'. 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.
|
||||
Allows the Apprise object to be wrapped in an 'if statement'.
|
||||
True is returned if at least one service has been loaded.
|
||||
"""
|
||||
return len(self) > 0
|
||||
|
||||
|
@ -803,7 +729,3 @@ class Apprise(object):
|
|||
"""
|
||||
return sum([1 if not isinstance(s, (ConfigBase, AppriseConfig))
|
||||
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 __getitem__(self, index: int) -> ConfigBase: ...
|
||||
def __bool__(self) -> bool: ...
|
||||
def __nonzero__(self) -> bool: ...
|
||||
def __iter__(self) -> Iterator[ConfigBase]: ...
|
||||
def __len__(self) -> int: ...
|
|
@ -30,15 +30,20 @@ from os.path import dirname
|
|||
from os.path import isfile
|
||||
from os.path import abspath
|
||||
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
|
||||
information and details that can be used by Apprise such as providing
|
||||
an alternate location to where images/icons can be found and the
|
||||
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
|
||||
app_id = 'Apprise'
|
||||
|
@ -102,8 +107,8 @@ class AppriseAsset(object):
|
|||
# - NotifyFormat.HTML
|
||||
# - None
|
||||
#
|
||||
# If no format is specified (hence None), then no special pre-formating
|
||||
# actions will take place during a notificaton. This has been and always
|
||||
# If no format is specified (hence None), then no special pre-formatting
|
||||
# actions will take place during a notification. This has been and always
|
||||
# will be the default.
|
||||
body_format = None
|
||||
|
||||
|
@ -132,6 +137,10 @@ class AppriseAsset(object):
|
|||
# that you leave this option as is otherwise.
|
||||
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 (_)
|
||||
# These can only be initialized using Python libraries and are not picked
|
||||
# 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
|
||||
_uid = str(uuid4())
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
def __init__(self, plugin_paths=None, **kwargs):
|
||||
"""
|
||||
Asset Initialization
|
||||
|
||||
|
@ -160,6 +169,10 @@ class AppriseAsset(object):
|
|||
|
||||
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):
|
||||
"""
|
||||
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
|
||||
# THE SOFTWARE.
|
||||
|
||||
import six
|
||||
|
||||
from . import attachment
|
||||
from . import URLBase
|
||||
from .AppriseAsset import AppriseAsset
|
||||
from .logger import logger
|
||||
from .common import ContentLocation
|
||||
from .common import CONTENT_LOCATIONS
|
||||
from .common import ATTACHMENT_SCHEMA_MAP
|
||||
from .utils import GET_SCHEMA_RE
|
||||
|
||||
|
||||
class AppriseAttachment(object):
|
||||
class AppriseAttachment:
|
||||
"""
|
||||
Our Apprise Attachment File Manager
|
||||
|
||||
|
@ -142,7 +141,7 @@ class AppriseAttachment(object):
|
|||
self.attachments.append(attachments)
|
||||
return True
|
||||
|
||||
elif isinstance(attachments, six.string_types):
|
||||
elif isinstance(attachments, str):
|
||||
# Save our path
|
||||
attachments = (attachments, )
|
||||
|
||||
|
@ -161,7 +160,7 @@ class AppriseAttachment(object):
|
|||
return_status = False
|
||||
continue
|
||||
|
||||
if isinstance(_attachment, six.string_types):
|
||||
if isinstance(_attachment, str):
|
||||
logger.debug("Loading attachment: {}".format(_attachment))
|
||||
# Instantiate ourselves an object, this function throws or
|
||||
# returns None if it fails
|
||||
|
@ -225,13 +224,13 @@ class AppriseAttachment(object):
|
|||
schema = schema.group('schema').lower()
|
||||
|
||||
# Some basic validation
|
||||
if schema not in attachment.SCHEMA_MAP:
|
||||
if schema not in ATTACHMENT_SCHEMA_MAP:
|
||||
logger.warning('Unsupported schema {}.'.format(schema))
|
||||
return None
|
||||
|
||||
# Parse our url details of the server object as dictionary containing
|
||||
# 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:
|
||||
# Failed to parse the server URL
|
||||
|
@ -251,7 +250,7 @@ class AppriseAttachment(object):
|
|||
# Attempt to create an instance of our plugin using the parsed
|
||||
# URL information
|
||||
attach_plugin = \
|
||||
attachment.SCHEMA_MAP[results['schema']](**results)
|
||||
ATTACHMENT_SCHEMA_MAP[results['schema']](**results)
|
||||
|
||||
except Exception:
|
||||
# the arguments are invalid or can not be used.
|
||||
|
@ -261,7 +260,7 @@ class AppriseAttachment(object):
|
|||
else:
|
||||
# Attempt to create an instance of our plugin using the parsed
|
||||
# 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
|
||||
|
||||
|
@ -295,15 +294,8 @@ class AppriseAttachment(object):
|
|||
|
||||
def __bool__(self):
|
||||
"""
|
||||
Allows the Apprise object to be wrapped in an Python 3.x based 'if
|
||||
statement'. 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.
|
||||
Allows the Apprise object to be wrapped in an 'if statement'.
|
||||
True is returned if at least one service has been loaded.
|
||||
"""
|
||||
return True if self.attachments else False
|
||||
|
||||
|
|
|
@ -33,6 +33,5 @@ class AppriseAttachment:
|
|||
def pop(self, index: int = ...) -> AttachBase: ...
|
||||
def __getitem__(self, index: int) -> AttachBase: ...
|
||||
def __bool__(self) -> bool: ...
|
||||
def __nonzero__(self) -> bool: ...
|
||||
def __iter__(self) -> Iterator[AttachBase]: ...
|
||||
def __len__(self) -> int: ...
|
|
@ -23,23 +23,19 @@
|
|||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
# THE SOFTWARE.
|
||||
|
||||
import six
|
||||
|
||||
from . import config
|
||||
from . import ConfigBase
|
||||
from . import CONFIG_FORMATS
|
||||
from . import URLBase
|
||||
from .AppriseAsset import AppriseAsset
|
||||
|
||||
from .common import MATCH_ALL_TAG
|
||||
from .common import MATCH_ALWAYS_TAG
|
||||
from . import common
|
||||
from .utils import GET_SCHEMA_RE
|
||||
from .utils import parse_list
|
||||
from .utils import is_exclusive_match
|
||||
from .logger import logger
|
||||
|
||||
|
||||
class AppriseConfig(object):
|
||||
class AppriseConfig:
|
||||
"""
|
||||
Our Apprise Configuration File Manager
|
||||
|
||||
|
@ -171,7 +167,7 @@ class AppriseConfig(object):
|
|||
self.configs.append(configs)
|
||||
return True
|
||||
|
||||
elif isinstance(configs, six.string_types):
|
||||
elif isinstance(configs, str):
|
||||
# Save our path
|
||||
configs = (configs, )
|
||||
|
||||
|
@ -189,7 +185,7 @@ class AppriseConfig(object):
|
|||
self.configs.append(_config)
|
||||
continue
|
||||
|
||||
elif not isinstance(_config, six.string_types):
|
||||
elif not isinstance(_config, str):
|
||||
logger.warning(
|
||||
"An invalid configuration (type={}) was specified.".format(
|
||||
type(_config)))
|
||||
|
@ -243,7 +239,7 @@ class AppriseConfig(object):
|
|||
# prepare default asset
|
||||
asset = self.asset
|
||||
|
||||
if not isinstance(content, six.string_types):
|
||||
if not isinstance(content, str):
|
||||
logger.warning(
|
||||
"An invalid configuration (type={}) was specified.".format(
|
||||
type(content)))
|
||||
|
@ -267,7 +263,8 @@ class AppriseConfig(object):
|
|||
# Return our status
|
||||
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
|
||||
configuration.
|
||||
|
@ -285,7 +282,7 @@ class AppriseConfig(object):
|
|||
|
||||
# A match_always flag allows us to pick up on our 'any' keyword
|
||||
# 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
|
||||
# - top level entries are treated as an 'or'
|
||||
|
@ -303,7 +300,7 @@ class AppriseConfig(object):
|
|||
|
||||
# Apply our tag matching based on our defined logic
|
||||
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):
|
||||
# Build ourselves a list of services dynamically and return the
|
||||
# as a list
|
||||
|
@ -334,13 +331,13 @@ class AppriseConfig(object):
|
|||
schema = schema.group('schema').lower()
|
||||
|
||||
# Some basic validation
|
||||
if schema not in config.SCHEMA_MAP:
|
||||
if schema not in common.CONFIG_SCHEMA_MAP:
|
||||
logger.warning('Unsupported schema {}.'.format(schema))
|
||||
return None
|
||||
|
||||
# Parse our url details of the server object as dictionary containing
|
||||
# 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:
|
||||
# Failed to parse the server URL
|
||||
|
@ -368,7 +365,8 @@ class AppriseConfig(object):
|
|||
try:
|
||||
# Attempt to create an instance of our plugin using the parsed
|
||||
# URL information
|
||||
cfg_plugin = config.SCHEMA_MAP[results['schema']](**results)
|
||||
cfg_plugin = \
|
||||
common.CONFIG_SCHEMA_MAP[results['schema']](**results)
|
||||
|
||||
except Exception:
|
||||
# the arguments are invalid or can not be used.
|
||||
|
@ -378,7 +376,7 @@ class AppriseConfig(object):
|
|||
else:
|
||||
# Attempt to create an instance of our plugin using the parsed
|
||||
# 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
|
||||
|
||||
|
@ -432,15 +430,8 @@ class AppriseConfig(object):
|
|||
|
||||
def __bool__(self):
|
||||
"""
|
||||
Allows the Apprise object to be wrapped in an Python 3.x based 'if
|
||||
statement'. 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.
|
||||
Allows the Apprise object to be wrapped in an 'if statement'.
|
||||
True is returned if at least one service has been loaded.
|
||||
"""
|
||||
return True if self.configs else False
|
||||
|
||||
|
|
|
@ -44,6 +44,5 @@ class AppriseConfig:
|
|||
def pop(self, index: int = ...) -> ConfigBase: ...
|
||||
def __getitem__(self, index: int) -> ConfigBase: ...
|
||||
def __bool__(self) -> bool: ...
|
||||
def __nonzero__(self) -> bool: ...
|
||||
def __iter__(self) -> Iterator[ConfigBase]: ...
|
||||
def __len__(self) -> int: ...
|
|
@ -23,7 +23,6 @@
|
|||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
# THE SOFTWARE.
|
||||
|
||||
import six
|
||||
import ctypes
|
||||
import locale
|
||||
import contextlib
|
||||
|
@ -52,18 +51,11 @@ try:
|
|||
except ImportError:
|
||||
# gettext isn't available; no problem, just fall back to using
|
||||
# the library features without multi-language support.
|
||||
try:
|
||||
# Python v2.7
|
||||
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
|
||||
import builtins
|
||||
builtins.__dict__['_'] = lambda x: x # pragma: no branch
|
||||
|
||||
|
||||
class LazyTranslation(object):
|
||||
class LazyTranslation:
|
||||
"""
|
||||
Doesn't translate anything until str() or unicode() references
|
||||
are made.
|
||||
|
@ -89,7 +81,7 @@ def gettext_lazy(text):
|
|||
return LazyTranslation(text=text)
|
||||
|
||||
|
||||
class AppriseLocale(object):
|
||||
class AppriseLocale:
|
||||
"""
|
||||
A wrapper class to gettext so that we can manipulate multiple lanaguages
|
||||
on the fly if required.
|
||||
|
@ -186,7 +178,7 @@ class AppriseLocale(object):
|
|||
"""
|
||||
# We want to only use the 2 character version of this language
|
||||
# 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:
|
||||
# no detection enabled; we're done
|
||||
return None
|
||||
|
|
|
@ -24,26 +24,17 @@
|
|||
# THE SOFTWARE.
|
||||
|
||||
import re
|
||||
import six
|
||||
from .logger import logger
|
||||
from time import sleep
|
||||
from datetime import datetime
|
||||
from xml.sax.saxutils import escape as sax_escape
|
||||
|
||||
try:
|
||||
# Python 2.7
|
||||
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 urllib.parse import unquote as _unquote
|
||||
from urllib.parse import quote as _quote
|
||||
|
||||
from .AppriseLocale import gettext_lazy as _
|
||||
from .AppriseAsset import AppriseAsset
|
||||
from .utils import urlencode
|
||||
from .utils import parse_url
|
||||
from .utils import parse_bool
|
||||
from .utils import parse_list
|
||||
|
@ -53,7 +44,7 @@ from .utils import parse_phone_no
|
|||
PATHSPLIT_LIST_DELIM = re.compile(r'[ \t\r\n,\\/]+')
|
||||
|
||||
|
||||
class PrivacyMode(object):
|
||||
class PrivacyMode:
|
||||
# Defines different privacy modes strings can be printed as
|
||||
# Astrisk sets 4 of them: e.g. ****
|
||||
# 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
|
||||
"""
|
||||
|
@ -346,7 +337,7 @@ class URLBase(object):
|
|||
Returns:
|
||||
str: The escaped html
|
||||
"""
|
||||
if not isinstance(html, six.string_types) or not html:
|
||||
if not isinstance(html, str) or not html:
|
||||
return ''
|
||||
|
||||
# Escape HTML
|
||||
|
@ -359,7 +350,7 @@ class URLBase(object):
|
|||
.replace(u' ', u' ')
|
||||
|
||||
if convert_new_lines:
|
||||
return escaped.replace(u'\n', u'<br/>')
|
||||
return escaped.replace(u'\n', u'<br/>')
|
||||
|
||||
return escaped
|
||||
|
||||
|
@ -370,7 +361,7 @@ class URLBase(object):
|
|||
encoding and errors parameters specify how to decode percent-encoded
|
||||
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
|
||||
versions.
|
||||
|
||||
|
@ -389,20 +380,14 @@ class URLBase(object):
|
|||
if not content:
|
||||
return ''
|
||||
|
||||
try:
|
||||
# Python v3.x
|
||||
return _unquote(content, encoding=encoding, errors=errors)
|
||||
|
||||
except TypeError:
|
||||
# Python v2.7
|
||||
return _unquote(content)
|
||||
return _unquote(content, encoding=encoding, errors=errors)
|
||||
|
||||
@staticmethod
|
||||
def quote(content, safe='/', encoding=None, errors=None):
|
||||
""" Replaces single character non-ascii characters and URI specific
|
||||
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
|
||||
versions.
|
||||
|
||||
|
@ -422,13 +407,7 @@ class URLBase(object):
|
|||
if not content:
|
||||
return ''
|
||||
|
||||
try:
|
||||
# Python v3.x
|
||||
return _quote(content, safe=safe, encoding=encoding, errors=errors)
|
||||
|
||||
except TypeError:
|
||||
# Python v2.7
|
||||
return _quote(content, safe=safe)
|
||||
return _quote(content, safe=safe, encoding=encoding, errors=errors)
|
||||
|
||||
@staticmethod
|
||||
def pprint(content, privacy=True, mode=PrivacyMode.Outer,
|
||||
|
@ -457,7 +436,7 @@ class URLBase(object):
|
|||
# Return 4 Asterisks
|
||||
return '****'
|
||||
|
||||
if not isinstance(content, six.string_types) or not content:
|
||||
if not isinstance(content, str) or not content:
|
||||
# Nothing more to do
|
||||
return ''
|
||||
|
||||
|
@ -472,7 +451,7 @@ class URLBase(object):
|
|||
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
|
||||
Wrapper to Python's `urlencode` while remaining compatible with both
|
||||
Python 2 & 3 since the reference to this function changed between
|
||||
versions.
|
||||
|
||||
|
@ -497,17 +476,8 @@ class URLBase(object):
|
|||
Returns:
|
||||
str: The escaped parameters returned as a string
|
||||
"""
|
||||
# Tidy query by eliminating any records set to None
|
||||
_query = {k: v for (k, v) in query.items() if v is not None}
|
||||
try:
|
||||
# Python v3.x
|
||||
return _urlencode(
|
||||
_query, doseq=doseq, safe=safe, encoding=encoding,
|
||||
errors=errors)
|
||||
|
||||
except TypeError:
|
||||
# Python v2.7
|
||||
return _urlencode(_query)
|
||||
return urlencode(
|
||||
query, doseq=doseq, safe=safe, encoding=encoding, errors=errors)
|
||||
|
||||
@staticmethod
|
||||
def split_path(path, unquote=True):
|
||||
|
@ -585,11 +555,6 @@ class URLBase(object):
|
|||
# Nothing further to do
|
||||
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)
|
||||
|
||||
return content
|
||||
|
@ -687,6 +652,9 @@ class URLBase(object):
|
|||
if 'cto' in results['qsd']:
|
||||
results['socket_connect_timeout'] = results['qsd']['cto']
|
||||
|
||||
if 'port' in results['qsd']:
|
||||
results['port'] = results['qsd']['port']
|
||||
|
||||
return results
|
||||
|
||||
@staticmethod
|
||||
|
@ -721,13 +689,13 @@ class URLBase(object):
|
|||
|
||||
for key in ('protocol', 'secure_protocol'):
|
||||
schema = getattr(self, key, None)
|
||||
if isinstance(schema, six.string_types):
|
||||
if isinstance(schema, str):
|
||||
schemas.add(schema)
|
||||
|
||||
elif isinstance(schema, (set, list, tuple)):
|
||||
# Support iterables list types
|
||||
for s in schema:
|
||||
if isinstance(s, six.string_types):
|
||||
if isinstance(s, str):
|
||||
schemas.add(s)
|
||||
|
||||
return schemas
|
||||
|
|
|
@ -24,7 +24,7 @@
|
|||
# THE SOFTWARE.
|
||||
|
||||
__title__ = 'Apprise'
|
||||
__version__ = '0.9.8.3'
|
||||
__version__ = '1.1.0'
|
||||
__author__ = 'Chris Caron'
|
||||
__license__ = 'MIT'
|
||||
__copywrite__ = 'Copyright (C) 2022 Chris Caron <lead2gold@gmail.com>'
|
||||
|
@ -57,6 +57,8 @@ from .AppriseAsset import AppriseAsset
|
|||
from .AppriseConfig import AppriseConfig
|
||||
from .AppriseAttachment import AppriseAttachment
|
||||
|
||||
from . import decorators
|
||||
|
||||
# Inherit our logging with our additional entries added to it
|
||||
from .logger import logging
|
||||
from .logger import logger
|
||||
|
@ -78,6 +80,9 @@ __all__ = [
|
|||
'ContentLocation', 'CONTENT_LOCATIONS',
|
||||
'PrivacyMode',
|
||||
|
||||
# Decorator
|
||||
'decorators',
|
||||
|
||||
# Logging
|
||||
'logging', 'logger', 'LogCapture',
|
||||
]
|
||||
|
|
|
@ -367,14 +367,7 @@ class AttachBase(URLBase):
|
|||
|
||||
def __bool__(self):
|
||||
"""
|
||||
Allows the Apprise object to be wrapped in an Python 3.x based 'if
|
||||
statement'. 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.
|
||||
Allows the Apprise object to be wrapped in an based 'if statement'.
|
||||
True is returned if our content was downloaded correctly.
|
||||
"""
|
||||
return True if self.path else False
|
||||
|
|
|
@ -34,4 +34,3 @@ class AttachBase:
|
|||
) -> Dict[str, Any]: ...
|
||||
def __len__(self) -> int: ...
|
||||
def __bool__(self) -> bool: ...
|
||||
def __nonzero__(self) -> bool: ...
|
|
@ -25,7 +25,6 @@
|
|||
|
||||
import re
|
||||
import os
|
||||
import six
|
||||
import requests
|
||||
from tempfile import NamedTemporaryFile
|
||||
from .AttachBase import AttachBase
|
||||
|
@ -67,7 +66,7 @@ class AttachHTTP(AttachBase):
|
|||
self.schema = 'https' if self.secure else 'http'
|
||||
|
||||
self.fullpath = kwargs.get('fullpath')
|
||||
if not isinstance(self.fullpath, six.string_types):
|
||||
if not isinstance(self.fullpath, str):
|
||||
self.fullpath = '/'
|
||||
|
||||
self.headers = {}
|
||||
|
|
|
@ -23,15 +23,12 @@
|
|||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
# THE SOFTWARE.
|
||||
|
||||
import six
|
||||
import re
|
||||
|
||||
from os import listdir
|
||||
from os.path import dirname
|
||||
from os.path import abspath
|
||||
|
||||
# Maintains a mapping of all of the attachment services
|
||||
SCHEMA_MAP = {}
|
||||
from ..common import ATTACHMENT_SCHEMA_MAP
|
||||
|
||||
__all__ = []
|
||||
|
||||
|
@ -90,29 +87,29 @@ def __load_matrix(path=abspath(dirname(__file__)), name='apprise.attachment'):
|
|||
|
||||
# Load protocol(s) if defined
|
||||
proto = getattr(plugin, 'protocol', None)
|
||||
if isinstance(proto, six.string_types):
|
||||
if proto not in SCHEMA_MAP:
|
||||
SCHEMA_MAP[proto] = plugin
|
||||
if isinstance(proto, str):
|
||||
if proto not in ATTACHMENT_SCHEMA_MAP:
|
||||
ATTACHMENT_SCHEMA_MAP[proto] = plugin
|
||||
|
||||
elif isinstance(proto, (set, list, tuple)):
|
||||
# Support iterables list types
|
||||
for p in proto:
|
||||
if p not in SCHEMA_MAP:
|
||||
SCHEMA_MAP[p] = plugin
|
||||
if p not in ATTACHMENT_SCHEMA_MAP:
|
||||
ATTACHMENT_SCHEMA_MAP[p] = plugin
|
||||
|
||||
# Load secure protocol(s) if defined
|
||||
protos = getattr(plugin, 'secure_protocol', None)
|
||||
if isinstance(protos, six.string_types):
|
||||
if protos not in SCHEMA_MAP:
|
||||
SCHEMA_MAP[protos] = plugin
|
||||
if isinstance(protos, str):
|
||||
if protos not in ATTACHMENT_SCHEMA_MAP:
|
||||
ATTACHMENT_SCHEMA_MAP[protos] = plugin
|
||||
|
||||
if isinstance(protos, (set, list, tuple)):
|
||||
# Support iterables list types
|
||||
for p in protos:
|
||||
if p not in SCHEMA_MAP:
|
||||
SCHEMA_MAP[p] = plugin
|
||||
if p not in ATTACHMENT_SCHEMA_MAP:
|
||||
ATTACHMENT_SCHEMA_MAP[p] = plugin
|
||||
|
||||
return SCHEMA_MAP
|
||||
return ATTACHMENT_SCHEMA_MAP
|
||||
|
||||
|
||||
# Dynamically build our schema base
|
||||
|
|
|
@ -26,12 +26,12 @@
|
|||
import click
|
||||
import logging
|
||||
import platform
|
||||
import six
|
||||
import sys
|
||||
import os
|
||||
import re
|
||||
|
||||
from os.path import isfile
|
||||
from os.path import exists
|
||||
from os.path import expanduser
|
||||
from os.path import expandvars
|
||||
|
||||
|
@ -40,6 +40,7 @@ from . import NotifyFormat
|
|||
from . import Apprise
|
||||
from . import AppriseAsset
|
||||
from . import AppriseConfig
|
||||
|
||||
from .utils import parse_list
|
||||
from .common import NOTIFY_TYPES
|
||||
from .common import NOTIFY_FORMATS
|
||||
|
@ -60,23 +61,42 @@ DEFAULT_RECURSION_DEPTH = 1
|
|||
CONTEXT_SETTINGS = dict(help_option_names=['-h', '--help'])
|
||||
|
||||
# Define our default configuration we use if nothing is otherwise specified
|
||||
DEFAULT_SEARCH_PATHS = (
|
||||
DEFAULT_CONFIG_PATHS = (
|
||||
# Legacy Path Support
|
||||
'~/.apprise',
|
||||
'~/.apprise.yml',
|
||||
'~/.config/apprise',
|
||||
'~/.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
|
||||
if platform.system() == 'Windows':
|
||||
# Default Search Path for Windows Users
|
||||
DEFAULT_SEARCH_PATHS = (
|
||||
# Default Config Search Path for Windows Users
|
||||
DEFAULT_CONFIG_PATHS = (
|
||||
expandvars('%APPDATA%/Apprise/apprise'),
|
||||
expandvars('%APPDATA%/Apprise/apprise.yml'),
|
||||
expandvars('%LOCALAPPDATA%/Apprise/apprise'),
|
||||
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):
|
||||
"""
|
||||
|
@ -107,6 +127,9 @@ def print_version_msg():
|
|||
@click.option('--title', '-t', default=None, type=str,
|
||||
help='Specify the message title. This field is complete '
|
||||
'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,
|
||||
metavar='CONFIG_URL',
|
||||
help='Specify one or more configuration locations.')
|
||||
|
@ -158,7 +181,7 @@ def print_version_msg():
|
|||
metavar='SERVER_URL [SERVER_URL2 [SERVER_URL3]]',)
|
||||
def main(body, title, config, attach, urls, notification_type, theme, tag,
|
||||
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
|
||||
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
|
||||
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
|
||||
asset = AppriseAsset(
|
||||
# Our body format
|
||||
|
@ -243,11 +272,14 @@ def main(body, title, config, attach, urls, notification_type, theme, tag,
|
|||
# Set the theme
|
||||
theme=theme,
|
||||
|
||||
# Async mode is only used for Python v3+ and allows a user to send
|
||||
# all of their notifications asyncronously. This was made an option
|
||||
# incase there are problems in the future where it's better that
|
||||
# everything run sequentially/syncronously instead.
|
||||
# Async mode allows a user to send all of their notifications
|
||||
# asynchronously. This was made an option incase there are problems
|
||||
# in the future where it is better that everything runs sequentially/
|
||||
# synchronously instead.
|
||||
async_mode=disable_async is not True,
|
||||
|
||||
# Load our plugins
|
||||
plugin_paths=plugin_path,
|
||||
)
|
||||
|
||||
# Create our Apprise object
|
||||
|
@ -263,11 +295,11 @@ def main(body, title, config, attach, urls, notification_type, theme, tag,
|
|||
for entry in plugins:
|
||||
protocols = [] if not entry['protocols'] else \
|
||||
[p for p in entry['protocols']
|
||||
if isinstance(p, six.string_types)]
|
||||
if isinstance(p, str)]
|
||||
protocols.extend(
|
||||
[] if not entry['secure_protocols'] else
|
||||
[p for p in entry['secure_protocols']
|
||||
if isinstance(p, six.string_types)])
|
||||
if isinstance(p, str)])
|
||||
|
||||
if len(protocols) == 1:
|
||||
# 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]),
|
||||
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(
|
||||
'{} {:<30} '.format(
|
||||
'+' if entry['enabled'] else '-',
|
||||
str(entry['service_name'])),
|
||||
fg="green" if entry['enabled'] else "red", bold=True),
|
||||
str(entry['service_name'])), fg=fg, bold=True),
|
||||
nl=(not entry['enabled'] or len(protocols) == 1))
|
||||
|
||||
if not entry['enabled']:
|
||||
|
@ -307,8 +346,9 @@ def main(body, title, config, attach, urls, notification_type, theme, tag,
|
|||
click.echo(' - ' + req)
|
||||
|
||||
# new line padding between entries
|
||||
click.echo()
|
||||
continue
|
||||
if entry['category'] == 'native':
|
||||
click.echo()
|
||||
continue
|
||||
|
||||
if len(protocols) > 1:
|
||||
click.echo('| Schema(s): {}'.format(
|
||||
|
@ -324,6 +364,7 @@ def main(body, title, config, attach, urls, notification_type, theme, tag,
|
|||
click.echo()
|
||||
|
||||
sys.exit(0)
|
||||
# end if details()
|
||||
|
||||
# The priorities of what is accepted are parsed in order below:
|
||||
# 1. URLs by command line
|
||||
|
@ -372,13 +413,14 @@ def main(body, title, config, attach, urls, notification_type, theme, tag,
|
|||
a.add(AppriseConfig(
|
||||
paths=os.environ['APPRISE_CONFIG'].strip(),
|
||||
asset=asset, recursion=recursion_depth))
|
||||
|
||||
else:
|
||||
# Load default configuration
|
||||
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))
|
||||
|
||||
if len(a) == 0:
|
||||
if len(a) == 0 and not urls:
|
||||
logger.error(
|
||||
'You must specify at least one server URL or populated '
|
||||
'configuration file.')
|
||||
|
|
|
@ -24,7 +24,52 @@
|
|||
# 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
|
||||
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
|
||||
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
|
||||
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
|
||||
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
|
||||
apprise library.
|
||||
|
@ -130,7 +175,7 @@ CONFIG_FORMATS = (
|
|||
)
|
||||
|
||||
|
||||
class ContentIncludeMode(object):
|
||||
class ContentIncludeMode:
|
||||
"""
|
||||
The different Content inclusion modes. All content based plugins will
|
||||
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
|
||||
to track the source of the attachment itself. We don't want
|
||||
|
|
|
@ -25,22 +25,18 @@
|
|||
|
||||
import os
|
||||
import re
|
||||
import six
|
||||
import yaml
|
||||
import time
|
||||
|
||||
from .. import plugins
|
||||
from .. import common
|
||||
from ..AppriseAsset import AppriseAsset
|
||||
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 parse_list
|
||||
from ..utils import parse_bool
|
||||
from ..utils import parse_urls
|
||||
from ..utils import cwe312_url
|
||||
from . import SCHEMA_MAP
|
||||
|
||||
# Test whether token is valid or not
|
||||
VALID_TOKEN = re.compile(
|
||||
|
@ -57,7 +53,7 @@ class ConfigBase(URLBase):
|
|||
|
||||
# The default expected configuration format unless otherwise
|
||||
# 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 should always initialize itself as None
|
||||
|
@ -70,7 +66,7 @@ class ConfigBase(URLBase):
|
|||
|
||||
# By default all configuration is not includable using the 'include'
|
||||
# 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
|
||||
config_path = os.getcwd()
|
||||
|
@ -138,11 +134,11 @@ class ConfigBase(URLBase):
|
|||
self.encoding = kwargs.get('encoding')
|
||||
|
||||
if 'format' in kwargs \
|
||||
and isinstance(kwargs['format'], six.string_types):
|
||||
and isinstance(kwargs['format'], str):
|
||||
# Store the enforced config format
|
||||
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
|
||||
err = 'An invalid config format ({}) was specified.'.format(
|
||||
self.config_format)
|
||||
|
@ -183,7 +179,7 @@ class ConfigBase(URLBase):
|
|||
# config plugin to load the data source and return unparsed content
|
||||
# None is returned if there was an error or simply no data
|
||||
content = self.read(**kwargs)
|
||||
if not isinstance(content, six.string_types):
|
||||
if not isinstance(content, str):
|
||||
# Set the time our content was cached at
|
||||
self._cached_time = time.time()
|
||||
|
||||
|
@ -230,7 +226,7 @@ class ConfigBase(URLBase):
|
|||
schema = schema.group('schema').lower()
|
||||
|
||||
# Some basic validation
|
||||
if schema not in SCHEMA_MAP:
|
||||
if schema not in common.CONFIG_SCHEMA_MAP:
|
||||
ConfigBase.logger.warning(
|
||||
'Unsupported include schema {}.'.format(schema))
|
||||
continue
|
||||
|
@ -241,7 +237,7 @@ class ConfigBase(URLBase):
|
|||
|
||||
# Parse our url details of the server object as dictionary
|
||||
# containing all of the information parsed from our URL
|
||||
results = SCHEMA_MAP[schema].parse_url(url)
|
||||
results = common.CONFIG_SCHEMA_MAP[schema].parse_url(url)
|
||||
if not results:
|
||||
# Failed to parse the server URL
|
||||
self.logger.warning(
|
||||
|
@ -249,12 +245,13 @@ class ConfigBase(URLBase):
|
|||
continue
|
||||
|
||||
# Handle cross inclusion based on allow_cross_includes rules
|
||||
if (SCHEMA_MAP[schema].allow_cross_includes ==
|
||||
ContentIncludeMode.STRICT
|
||||
if (common.CONFIG_SCHEMA_MAP[schema].allow_cross_includes ==
|
||||
common.ContentIncludeMode.STRICT
|
||||
and schema not in self.schemas()
|
||||
and not self.insecure_includes) or \
|
||||
SCHEMA_MAP[schema].allow_cross_includes == \
|
||||
ContentIncludeMode.NEVER:
|
||||
common.CONFIG_SCHEMA_MAP[schema] \
|
||||
.allow_cross_includes == \
|
||||
common.ContentIncludeMode.NEVER:
|
||||
|
||||
# Prevent the loading if insecure base protocols
|
||||
ConfigBase.logger.warning(
|
||||
|
@ -280,7 +277,8 @@ class ConfigBase(URLBase):
|
|||
try:
|
||||
# Attempt to create an instance of our plugin using the
|
||||
# parsed URL information
|
||||
cfg_plugin = SCHEMA_MAP[results['schema']](**results)
|
||||
cfg_plugin = \
|
||||
common.CONFIG_SCHEMA_MAP[results['schema']](**results)
|
||||
|
||||
except Exception as e:
|
||||
# the arguments are invalid or can not be used.
|
||||
|
@ -379,7 +377,7 @@ class ConfigBase(URLBase):
|
|||
# Allow overriding the default config format
|
||||
if 'format' in results['qsd']:
|
||||
results['format'] = results['qsd'].get('format')
|
||||
if results['format'] not in CONFIG_FORMATS:
|
||||
if results['format'] not in common.CONFIG_FORMATS:
|
||||
URLBase.logger.warning(
|
||||
'Unsupported format specified {}'.format(
|
||||
results['format']))
|
||||
|
@ -457,14 +455,14 @@ class ConfigBase(URLBase):
|
|||
|
||||
# Attempt to detect configuration
|
||||
if result.group('yaml'):
|
||||
config_format = ConfigFormat.YAML
|
||||
config_format = common.ConfigFormat.YAML
|
||||
ConfigBase.logger.debug(
|
||||
'Detected YAML configuration '
|
||||
'based on line {}.'.format(line))
|
||||
break
|
||||
|
||||
elif result.group('text'):
|
||||
config_format = ConfigFormat.TEXT
|
||||
config_format = common.ConfigFormat.TEXT
|
||||
ConfigBase.logger.debug(
|
||||
'Detected TEXT configuration '
|
||||
'based on line {}.'.format(line))
|
||||
|
@ -472,7 +470,7 @@ class ConfigBase(URLBase):
|
|||
|
||||
# If we reach here, we have a comment entry
|
||||
# Adjust default format to TEXT
|
||||
config_format = ConfigFormat.TEXT
|
||||
config_format = common.ConfigFormat.TEXT
|
||||
|
||||
return config_format
|
||||
|
||||
|
@ -493,7 +491,7 @@ class ConfigBase(URLBase):
|
|||
ConfigBase.logger.error('Could not detect configuration')
|
||||
return (list(), list())
|
||||
|
||||
if config_format not in CONFIG_FORMATS:
|
||||
if config_format not in common.CONFIG_FORMATS:
|
||||
# Invalid configuration type specified
|
||||
ConfigBase.logger.error(
|
||||
'An invalid configuration format ({}) was specified'.format(
|
||||
|
@ -618,7 +616,7 @@ class ConfigBase(URLBase):
|
|||
try:
|
||||
# Attempt to create an instance of our plugin using the
|
||||
# parsed URL information
|
||||
plugin = plugins.SCHEMA_MAP[results['schema']](**results)
|
||||
plugin = common.NOTIFY_SCHEMA_MAP[results['schema']](**results)
|
||||
|
||||
# Create log entry of loaded URL
|
||||
ConfigBase.logger.debug(
|
||||
|
@ -705,7 +703,7 @@ class ConfigBase(URLBase):
|
|||
|
||||
if not (hasattr(asset, k) and
|
||||
isinstance(getattr(asset, k),
|
||||
(bool, six.string_types))):
|
||||
(bool, str))):
|
||||
|
||||
# We can't set a function or non-string set value
|
||||
ConfigBase.logger.warning(
|
||||
|
@ -716,7 +714,7 @@ class ConfigBase(URLBase):
|
|||
# Convert to an empty string
|
||||
v = ''
|
||||
|
||||
if (isinstance(v, (bool, six.string_types))
|
||||
if (isinstance(v, (bool, str))
|
||||
and isinstance(getattr(asset, k), bool)):
|
||||
|
||||
# If the object in the Asset is a boolean, then
|
||||
|
@ -724,7 +722,7 @@ class ConfigBase(URLBase):
|
|||
# match that.
|
||||
setattr(asset, k, parse_bool(v))
|
||||
|
||||
elif isinstance(v, six.string_types):
|
||||
elif isinstance(v, str):
|
||||
# Set our asset object with the new value
|
||||
setattr(asset, k, v.strip())
|
||||
|
||||
|
@ -739,7 +737,7 @@ class ConfigBase(URLBase):
|
|||
global_tags = set()
|
||||
|
||||
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
|
||||
global_tags = set(parse_list(tags))
|
||||
|
||||
|
@ -747,7 +745,7 @@ class ConfigBase(URLBase):
|
|||
# include root directive
|
||||
#
|
||||
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
|
||||
# comma and/or space
|
||||
includes = parse_urls(includes)
|
||||
|
@ -759,7 +757,7 @@ class ConfigBase(URLBase):
|
|||
# Iterate over each config URL
|
||||
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
|
||||
# a comma and/or space
|
||||
configs.extend(parse_urls(url))
|
||||
|
@ -787,7 +785,7 @@ class ConfigBase(URLBase):
|
|||
loggable_url = url if not asset.secure_logging \
|
||||
else cwe312_url(url)
|
||||
|
||||
if isinstance(url, six.string_types):
|
||||
if isinstance(url, str):
|
||||
# We're just a simple URL string...
|
||||
schema = GET_SCHEMA_RE.match(url)
|
||||
if schema is None:
|
||||
|
@ -818,10 +816,7 @@ class ConfigBase(URLBase):
|
|||
# can at least tell the end user what entries were ignored
|
||||
# due to errors
|
||||
|
||||
if six.PY2:
|
||||
it = url.iteritems()
|
||||
else: # six.PY3
|
||||
it = iter(url.items())
|
||||
it = iter(url.items())
|
||||
|
||||
# Track the URL to-load
|
||||
_url = None
|
||||
|
@ -871,17 +866,14 @@ class ConfigBase(URLBase):
|
|||
|
||||
# We are a url string with additional unescaped options
|
||||
if isinstance(entries, dict):
|
||||
if six.PY2:
|
||||
_url, tokens = next(url.iteritems())
|
||||
else: # six.PY3
|
||||
_url, tokens = next(iter(url.items()))
|
||||
_url, tokens = next(iter(url.items()))
|
||||
|
||||
# Tags you just can't over-ride
|
||||
if 'schema' in entries:
|
||||
del entries['schema']
|
||||
|
||||
# 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(
|
||||
schema, entries)
|
||||
|
||||
|
@ -893,7 +885,7 @@ class ConfigBase(URLBase):
|
|||
|
||||
elif isinstance(tokens, dict):
|
||||
# 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(
|
||||
schema, tokens)
|
||||
|
||||
|
@ -927,6 +919,14 @@ class ConfigBase(URLBase):
|
|||
# Grab our first item
|
||||
_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.
|
||||
# The below ensures our tags are set correctly
|
||||
if 'tag' in _results:
|
||||
|
@ -958,10 +958,12 @@ class ConfigBase(URLBase):
|
|||
# Prepare our Asset Object
|
||||
_results['asset'] = asset
|
||||
|
||||
# Now we generate our plugin
|
||||
try:
|
||||
# Attempt to create an instance of our plugin using the
|
||||
# parsed URL information
|
||||
plugin = plugins.SCHEMA_MAP[_results['schema']](**_results)
|
||||
plugin = common.\
|
||||
NOTIFY_SCHEMA_MAP[_results['schema']](**_results)
|
||||
|
||||
# Create log entry of loaded URL
|
||||
ConfigBase.logger.debug(
|
||||
|
@ -1014,7 +1016,7 @@ class ConfigBase(URLBase):
|
|||
# Create a copy of our dictionary
|
||||
tokens = tokens.copy()
|
||||
|
||||
for kw, meta in plugins.SCHEMA_MAP[schema]\
|
||||
for kw, meta in common.NOTIFY_SCHEMA_MAP[schema]\
|
||||
.template_kwargs.items():
|
||||
|
||||
# Determine our prefix:
|
||||
|
@ -1059,7 +1061,7 @@ class ConfigBase(URLBase):
|
|||
# This function here allows these mappings to take place within the
|
||||
# YAML file as independant arguments.
|
||||
class_templates = \
|
||||
plugins.details(plugins.SCHEMA_MAP[schema])
|
||||
plugins.details(common.NOTIFY_SCHEMA_MAP[schema])
|
||||
|
||||
for key in list(tokens.keys()):
|
||||
|
||||
|
@ -1088,7 +1090,7 @@ class ConfigBase(URLBase):
|
|||
|
||||
# Detect if we're dealign with a list or not
|
||||
is_list = re.search(
|
||||
r'^(list|choice):.*',
|
||||
r'^list:.*',
|
||||
meta.get('type'),
|
||||
re.IGNORECASE)
|
||||
|
||||
|
@ -1105,7 +1107,7 @@ class ConfigBase(URLBase):
|
|||
r'^(choice:)?string',
|
||||
meta.get('type'),
|
||||
re.IGNORECASE) \
|
||||
and not isinstance(value, six.string_types):
|
||||
and not isinstance(value, str):
|
||||
|
||||
# Ensure our format is as expected
|
||||
value = str(value)
|
||||
|
@ -1158,19 +1160,8 @@ class ConfigBase(URLBase):
|
|||
|
||||
def __bool__(self):
|
||||
"""
|
||||
Allows the Apprise object to be wrapped in an Python 3.x based 'if
|
||||
statement'. 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.
|
||||
Allows the Apprise object to be wrapped in an 'if statement'.
|
||||
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
|
||||
|
|
|
@ -24,7 +24,6 @@
|
|||
# THE SOFTWARE.
|
||||
|
||||
import re
|
||||
import io
|
||||
import os
|
||||
from .ConfigBase import ConfigBase
|
||||
from ..common import ConfigFormat
|
||||
|
@ -119,9 +118,7 @@ class ConfigFile(ConfigBase):
|
|||
self.throttle()
|
||||
|
||||
try:
|
||||
# Python 3 just supports open(), however to remain compatible with
|
||||
# Python 2, we use the io module
|
||||
with io.open(self.path, "rt", encoding=self.encoding) as f:
|
||||
with open(self.path, "rt", encoding=self.encoding) as f:
|
||||
# Store our content for parsing
|
||||
response = f.read()
|
||||
|
||||
|
|
|
@ -24,7 +24,6 @@
|
|||
# THE SOFTWARE.
|
||||
|
||||
import re
|
||||
import six
|
||||
import requests
|
||||
from .ConfigBase import ConfigBase
|
||||
from ..common import ConfigFormat
|
||||
|
@ -81,7 +80,7 @@ class ConfigHTTP(ConfigBase):
|
|||
self.schema = 'https' if self.secure else 'http'
|
||||
|
||||
self.fullpath = kwargs.get('fullpath')
|
||||
if not isinstance(self.fullpath, six.string_types):
|
||||
if not isinstance(self.fullpath, str):
|
||||
self.fullpath = '/'
|
||||
|
||||
self.headers = {}
|
||||
|
|
|
@ -24,14 +24,11 @@
|
|||
# THE SOFTWARE.
|
||||
|
||||
import re
|
||||
import six
|
||||
from os import listdir
|
||||
from os.path import dirname
|
||||
from os.path import abspath
|
||||
from ..logger import logger
|
||||
|
||||
# Maintains a mapping of all of the configuration services
|
||||
SCHEMA_MAP = {}
|
||||
from ..common import CONFIG_SCHEMA_MAP
|
||||
|
||||
__all__ = []
|
||||
|
||||
|
@ -89,40 +86,20 @@ def __load_matrix(path=abspath(dirname(__file__)), name='apprise.config'):
|
|||
globals()[plugin_name] = plugin
|
||||
|
||||
fn = getattr(plugin, 'schemas', None)
|
||||
try:
|
||||
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)
|
||||
schemas = set([]) if not callable(fn) else fn(plugin)
|
||||
|
||||
# map our schema to our plugin
|
||||
for schema in schemas:
|
||||
if schema in SCHEMA_MAP:
|
||||
if schema in CONFIG_SCHEMA_MAP:
|
||||
logger.error(
|
||||
"Config schema ({}) mismatch detected - {} to {}"
|
||||
.format(schema, SCHEMA_MAP[schema], plugin))
|
||||
.format(schema, CONFIG_SCHEMA_MAP[schema], plugin))
|
||||
continue
|
||||
|
||||
# Assign plugin
|
||||
SCHEMA_MAP[schema] = plugin
|
||||
CONFIG_SCHEMA_MAP[schema] = plugin
|
||||
|
||||
return SCHEMA_MAP
|
||||
return CONFIG_SCHEMA_MAP
|
||||
|
||||
|
||||
# Dynamically build our schema base
|
||||
|
|
|
@ -23,18 +23,12 @@
|
|||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
# THE SOFTWARE.
|
||||
|
||||
|
||||
import re
|
||||
import six
|
||||
from markdown import markdown
|
||||
from .common import NotifyFormat
|
||||
from .URLBase import URLBase
|
||||
|
||||
if six.PY2:
|
||||
from HTMLParser import HTMLParser
|
||||
|
||||
else:
|
||||
from html.parser import HTMLParser
|
||||
from html.parser import HTMLParser
|
||||
|
||||
|
||||
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.
|
||||
"""
|
||||
|
||||
return URLBase.escape_html(content)
|
||||
# First eliminate any carriage returns
|
||||
return URLBase.escape_html(content, convert_new_lines=True)
|
||||
|
||||
|
||||
def html_to_text(content):
|
||||
|
@ -79,10 +74,6 @@ def html_to_text(content):
|
|||
"""
|
||||
|
||||
parser = HTMLConverter()
|
||||
if six.PY2:
|
||||
# Python 2.7 requires an additional parsing to un-escape characters
|
||||
content = parser.unescape(content)
|
||||
|
||||
parser.feed(content)
|
||||
parser.close()
|
||||
return parser.converted
|
||||
|
@ -96,7 +87,9 @@ class HTMLConverter(HTMLParser, object):
|
|||
'div', 'td', 'th', 'code', 'pre', 'label', 'li',)
|
||||
|
||||
# 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
|
||||
WS_TRIM = re.compile(r'[\s]+', re.DOTALL | re.MULTILINE)
|
||||
|
@ -122,14 +115,6 @@ class HTMLConverter(HTMLParser, object):
|
|||
string = ''.join(self._finalize(self._result))
|
||||
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):
|
||||
"""
|
||||
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)
|
||||
|
||||
|
||||
class LogCapture(object):
|
||||
class LogCapture:
|
||||
"""
|
||||
A class used to allow one to instantiate loggers that write to
|
||||
memory for temporary purposes. e.g.:
|
||||
|
|
|
@ -24,7 +24,6 @@
|
|||
# THE SOFTWARE.
|
||||
|
||||
import re
|
||||
import six
|
||||
import requests
|
||||
from json import dumps
|
||||
|
||||
|
@ -137,7 +136,7 @@ class NotifyAppriseAPI(NotifyBase):
|
|||
super(NotifyAppriseAPI, self).__init__(**kwargs)
|
||||
|
||||
self.fullpath = kwargs.get('fullpath')
|
||||
if not isinstance(self.fullpath, six.string_types):
|
||||
if not isinstance(self.fullpath, str):
|
||||
self.fullpath = '/'
|
||||
|
||||
self.token = validate_regex(
|
||||
|
@ -339,18 +338,10 @@ class NotifyAppriseAPI(NotifyBase):
|
|||
return results
|
||||
|
||||
# Add our headers that the user can potentially over-ride if they wish
|
||||
# to to our returned result set
|
||||
results['headers'] = results['qsd+']
|
||||
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
|
||||
# to to our returned result set and tidy entries by unquoting them
|
||||
results['headers'] = \
|
||||
{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
|
||||
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.
|
||||
|
||||
import re
|
||||
import six
|
||||
|
||||
from ..URLBase import URLBase
|
||||
from ..common import NotifyType
|
||||
|
@ -37,14 +36,9 @@ from ..AppriseLocale import gettext_lazy as _
|
|||
from ..AppriseAttachment import AppriseAttachment
|
||||
|
||||
|
||||
if six.PY3:
|
||||
# Wrap our base with the asyncio wrapper
|
||||
from ..py3compat.asyncio import AsyncNotifyBase
|
||||
BASE_OBJECT = AsyncNotifyBase
|
||||
|
||||
else:
|
||||
# Python v2.7 (backwards compatibility)
|
||||
BASE_OBJECT = URLBase
|
||||
# Wrap our base with the asyncio wrapper
|
||||
from ..py3compat.asyncio import AsyncNotifyBase
|
||||
BASE_OBJECT = AsyncNotifyBase
|
||||
|
||||
|
||||
class NotifyBase(BASE_OBJECT):
|
||||
|
@ -59,6 +53,15 @@ class NotifyBase(BASE_OBJECT):
|
|||
# enabled.
|
||||
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
|
||||
# already by Apprise.
|
||||
#
|
||||
|
|
|
@ -24,7 +24,6 @@
|
|||
# THE SOFTWARE.
|
||||
|
||||
import re
|
||||
import six
|
||||
import requests
|
||||
import hmac
|
||||
from json import dumps
|
||||
|
@ -181,7 +180,7 @@ class NotifyBoxcar(NotifyBase):
|
|||
self.tags.append(DEFAULT_TAG)
|
||||
targets = []
|
||||
|
||||
elif isinstance(targets, six.string_types):
|
||||
elif isinstance(targets, str):
|
||||
targets = [x for x in filter(bool, TAGS_LIST_DELIM.split(
|
||||
targets,
|
||||
))]
|
||||
|
@ -357,13 +356,8 @@ class NotifyBoxcar(NotifyBase):
|
|||
# by default
|
||||
entries = NotifyBoxcar.split_path(results['fullpath'])
|
||||
|
||||
try:
|
||||
# Now fetch the remaining tokens
|
||||
results['secret'] = entries.pop(0)
|
||||
|
||||
except IndexError:
|
||||
# secret wasn't specified
|
||||
results['secret'] = None
|
||||
# Now fetch the remaining tokens
|
||||
results['secret'] = entries.pop(0) if entries else None
|
||||
|
||||
# Our recipients make up the remaining entries of our array
|
||||
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
|
||||
# account profile area: https://d7networks.com/accounts/profile/
|
||||
|
||||
import six
|
||||
import requests
|
||||
import base64
|
||||
from json import dumps
|
||||
|
@ -54,7 +53,7 @@ D7NETWORKS_HTTP_ERROR_MAP = {
|
|||
|
||||
|
||||
# Priorities
|
||||
class D7SMSPriority(object):
|
||||
class D7SMSPriority:
|
||||
"""
|
||||
D7 Networks SMS Message Priority
|
||||
"""
|
||||
|
@ -192,7 +191,7 @@ class NotifyD7Networks(NotifyBase):
|
|||
|
||||
# Setup our source address (if defined)
|
||||
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):
|
||||
msg = 'A D7 Networks user/pass was not provided.'
|
||||
|
@ -232,10 +231,10 @@ class NotifyD7Networks(NotifyBase):
|
|||
|
||||
auth = '{user}:{password}'.format(
|
||||
user=self.user, password=self.password)
|
||||
if six.PY3:
|
||||
# Python 3's versio of b64encode() expects a byte array and not
|
||||
# a string. To accomodate this, we encode the content here
|
||||
auth = auth.encode('utf-8')
|
||||
|
||||
# Python 3's versio of b64encode() expects a byte array and not
|
||||
# a string. To accommodate this, we encode the content here
|
||||
auth = auth.encode('utf-8')
|
||||
|
||||
# Prepare our headers
|
||||
headers = {
|
||||
|
|
|
@ -60,7 +60,7 @@ try:
|
|||
from dbus.mainloop.glib import DBusGMainLoop
|
||||
LOOP_GLIB = DBusGMainLoop()
|
||||
|
||||
except ImportError:
|
||||
except ImportError: # pragma: no cover
|
||||
# No problem
|
||||
pass
|
||||
|
||||
|
@ -109,18 +109,36 @@ MAINLOOP_MAP = {
|
|||
|
||||
|
||||
# Urgencies
|
||||
class DBusUrgency(object):
|
||||
class DBusUrgency:
|
||||
LOW = 0
|
||||
NORMAL = 1
|
||||
HIGH = 2
|
||||
|
||||
|
||||
# Define our urgency levels
|
||||
DBUS_URGENCIES = (
|
||||
DBusUrgency.LOW,
|
||||
DBusUrgency.NORMAL,
|
||||
DBusUrgency.HIGH,
|
||||
)
|
||||
DBUS_URGENCIES = {
|
||||
# Note: This also acts as a reverse lookup mapping
|
||||
DBusUrgency.LOW: 'low',
|
||||
DBusUrgency.NORMAL: 'normal',
|
||||
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):
|
||||
|
@ -143,10 +161,11 @@ class NotifyDBus(NotifyBase):
|
|||
service_url = 'http://www.freedesktop.org/Software/dbus/'
|
||||
|
||||
# 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
|
||||
# Python v2. So converting the result set back into a list makes us
|
||||
# compatible
|
||||
# TODO: Review after dropping support for Python 2.
|
||||
protocol = list(MAINLOOP_MAP.keys())
|
||||
|
||||
# A URL that takes you to the setup/help of the specific protocol
|
||||
|
@ -182,6 +201,12 @@ class NotifyDBus(NotifyBase):
|
|||
'values': DBUS_URGENCIES,
|
||||
'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': {
|
||||
'name': _('X-Axis'),
|
||||
'type': 'int',
|
||||
|
@ -223,15 +248,29 @@ class NotifyDBus(NotifyBase):
|
|||
raise TypeError(msg)
|
||||
|
||||
# The urgency of the message
|
||||
if urgency not in DBUS_URGENCIES:
|
||||
self.urgency = DBusUrgency.NORMAL
|
||||
|
||||
else:
|
||||
self.urgency = urgency
|
||||
self.urgency = int(
|
||||
NotifyDBus.template_args['urgency']['default']
|
||||
if urgency is None else
|
||||
next((
|
||||
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
|
||||
self.x_axis = x_axis if isinstance(x_axis, int) else None
|
||||
self.y_axis = y_axis if isinstance(y_axis, int) else None
|
||||
if x_axis or y_axis:
|
||||
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
|
||||
# or not.
|
||||
|
@ -343,17 +382,13 @@ class NotifyDBus(NotifyBase):
|
|||
Returns the URL built dynamically based on specified arguments.
|
||||
"""
|
||||
|
||||
_map = {
|
||||
DBusUrgency.LOW: 'low',
|
||||
DBusUrgency.NORMAL: 'normal',
|
||||
DBusUrgency.HIGH: 'high',
|
||||
}
|
||||
|
||||
# Define any URL parameters
|
||||
params = {
|
||||
'image': 'yes' if self.include_image else 'no',
|
||||
'urgency': 'normal' if self.urgency not in _map
|
||||
else _map[self.urgency],
|
||||
'urgency':
|
||||
DBUS_URGENCIES[self.template_args['urgency']['default']]
|
||||
if self.urgency not in DBUS_URGENCIES
|
||||
else DBUS_URGENCIES[self.urgency],
|
||||
}
|
||||
|
||||
# Extend our parameters
|
||||
|
@ -389,38 +424,20 @@ class NotifyDBus(NotifyBase):
|
|||
|
||||
# DBus supports urgency, but we we also support the keyword priority
|
||||
# so that it is consistent with some of the other plugins
|
||||
urgency = results['qsd'].get('urgency', results['qsd'].get('priority'))
|
||||
if urgency and len(urgency):
|
||||
_map = {
|
||||
'0': DBusUrgency.LOW,
|
||||
'l': DBusUrgency.LOW,
|
||||
'n': DBusUrgency.NORMAL,
|
||||
'1': DBusUrgency.NORMAL,
|
||||
'h': DBusUrgency.HIGH,
|
||||
'2': DBusUrgency.HIGH,
|
||||
}
|
||||
if 'priority' in results['qsd'] and len(results['qsd']['priority']):
|
||||
# We intentionally store the priority in the urgency section
|
||||
results['urgency'] = \
|
||||
NotifyDBus.unquote(results['qsd']['priority'])
|
||||
|
||||
try:
|
||||
# Attempt to index/retrieve our urgency
|
||||
results['urgency'] = _map[urgency[0].lower()]
|
||||
|
||||
except KeyError:
|
||||
# No priority was set
|
||||
pass
|
||||
if 'urgency' in results['qsd'] and len(results['qsd']['urgency']):
|
||||
results['urgency'] = \
|
||||
NotifyDBus.unquote(results['qsd']['urgency'])
|
||||
|
||||
# handle x,y coordinates
|
||||
try:
|
||||
results['x_axis'] = int(results['qsd'].get('x'))
|
||||
if 'x' in results['qsd'] and len(results['qsd']['x']):
|
||||
results['x_axis'] = NotifyDBus.unquote(results['qsd'].get('x'))
|
||||
|
||||
except (TypeError, ValueError):
|
||||
# No x was set
|
||||
pass
|
||||
|
||||
try:
|
||||
results['y_axis'] = int(results['qsd'].get('y'))
|
||||
|
||||
except (TypeError, ValueError):
|
||||
# No y was set
|
||||
pass
|
||||
if 'y' in results['qsd'] and len(results['qsd']['y']):
|
||||
results['y_axis'] = NotifyDBus.unquote(results['qsd'].get('y'))
|
||||
|
||||
return results
|
||||
|
|
|
@ -58,15 +58,27 @@ from ..utils import parse_list
|
|||
from ..utils import parse_bool
|
||||
|
||||
|
||||
class DapnetPriority(object):
|
||||
class DapnetPriority:
|
||||
NORMAL = 0
|
||||
EMERGENCY = 1
|
||||
|
||||
|
||||
DAPNET_PRIORITIES = (
|
||||
DapnetPriority.NORMAL,
|
||||
DapnetPriority.EMERGENCY,
|
||||
)
|
||||
DAPNET_PRIORITIES = {
|
||||
DapnetPriority.NORMAL: 'normal',
|
||||
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):
|
||||
|
@ -172,11 +184,14 @@ class NotifyDapnet(NotifyBase):
|
|||
# Parse our targets
|
||||
self.targets = list()
|
||||
|
||||
# get the emergency prio setting
|
||||
if priority not in DAPNET_PRIORITIES:
|
||||
self.priority = self.template_args['priority']['default']
|
||||
else:
|
||||
self.priority = priority
|
||||
# The Priority of the message
|
||||
self.priority = int(
|
||||
NotifyDapnet.template_args['priority']['default']
|
||||
if priority is None else
|
||||
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):
|
||||
msg = 'A Dapnet user/pass was not provided.'
|
||||
|
@ -201,8 +216,7 @@ class NotifyDapnet(NotifyBase):
|
|||
)
|
||||
continue
|
||||
|
||||
# Store callsign without SSID and
|
||||
# ignore duplicates
|
||||
# Store callsign without SSID and ignore duplicates
|
||||
if result['callsign'] not in self.targets:
|
||||
self.targets.append(result['callsign'])
|
||||
|
||||
|
@ -230,10 +244,6 @@ class NotifyDapnet(NotifyBase):
|
|||
# error tracking (used for function return)
|
||||
has_error = False
|
||||
|
||||
# prepare the emergency mode
|
||||
emergency_mode = True \
|
||||
if self.priority == DapnetPriority.EMERGENCY else False
|
||||
|
||||
# Create a copy of the targets list
|
||||
targets = list(self.targets)
|
||||
|
||||
|
@ -244,7 +254,7 @@ class NotifyDapnet(NotifyBase):
|
|||
'text': body,
|
||||
'callSignNames': targets[index:index + batch_size],
|
||||
'transmitterGroupNames': self.txgroups,
|
||||
'emergency': emergency_mode,
|
||||
'emergency': (self.priority == DapnetPriority.EMERGENCY),
|
||||
}
|
||||
|
||||
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.
|
||||
"""
|
||||
|
||||
# Define any URL parameters
|
||||
_map = {
|
||||
DapnetPriority.NORMAL: 'normal',
|
||||
DapnetPriority.EMERGENCY: 'emergency',
|
||||
}
|
||||
|
||||
# Define any URL parameters
|
||||
params = {
|
||||
'priority': 'normal' if self.priority not in _map
|
||||
else _map[self.priority],
|
||||
'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',
|
||||
'txgroups': ','.join(self.txgroups),
|
||||
}
|
||||
|
@ -361,25 +367,10 @@ class NotifyDapnet(NotifyBase):
|
|||
results['targets'] += \
|
||||
NotifyDapnet.parse_list(results['qsd']['to'])
|
||||
|
||||
# Check for priority
|
||||
# Set our priority
|
||||
if 'priority' in results['qsd'] and len(results['qsd']['priority']):
|
||||
_map = {
|
||||
# Letter Assignments
|
||||
'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
|
||||
results['priority'] = \
|
||||
NotifyDapnet.unquote(results['qsd']['priority'])
|
||||
|
||||
# Check for one or multiple transmitter groups (comma separated)
|
||||
# and split them up, when necessary
|
||||
|
|
|
@ -128,6 +128,12 @@ class NotifyDiscord(NotifyBase):
|
|||
'name': _('Avatar URL'),
|
||||
'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': {
|
||||
'name': _('Display Footer'),
|
||||
'type': 'bool',
|
||||
|
@ -153,7 +159,7 @@ class NotifyDiscord(NotifyBase):
|
|||
|
||||
def __init__(self, webhook_id, webhook_token, tts=False, avatar=True,
|
||||
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
|
||||
|
||||
|
@ -194,6 +200,9 @@ class NotifyDiscord(NotifyBase):
|
|||
# Use Fields
|
||||
self.fields = fields
|
||||
|
||||
# Specified Thread ID
|
||||
self.thread_id = thread
|
||||
|
||||
# Avatar URL
|
||||
# This allows a user to provide an over-ride to the otherwise
|
||||
# dynamically generated avatar url images
|
||||
|
@ -274,6 +283,9 @@ class NotifyDiscord(NotifyBase):
|
|||
payload['content'] = \
|
||||
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):
|
||||
payload['avatar_url'] = \
|
||||
self.avatar_url if self.avatar_url else image_url
|
||||
|
@ -447,6 +459,9 @@ class NotifyDiscord(NotifyBase):
|
|||
if self.avatar_url:
|
||||
params['avatar_url'] = self.avatar_url
|
||||
|
||||
if self.thread_id:
|
||||
params['thread'] = self.thread_id
|
||||
|
||||
# Extend our parameters
|
||||
params.update(self.url_parameters(privacy=privacy, *args, **kwargs))
|
||||
|
||||
|
@ -515,6 +530,11 @@ class NotifyDiscord(NotifyBase):
|
|||
results['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
|
||||
|
||||
@staticmethod
|
||||
|
|
|
@ -24,12 +24,11 @@
|
|||
# THE SOFTWARE.
|
||||
|
||||
import re
|
||||
import six
|
||||
import smtplib
|
||||
from email.mime.text import MIMEText
|
||||
from email.mime.application import MIMEApplication
|
||||
from email.mime.multipart import MIMEMultipart
|
||||
from email.utils import formataddr
|
||||
from email.utils import formataddr, make_msgid
|
||||
from email.header import Header
|
||||
from email import charset
|
||||
|
||||
|
@ -38,17 +37,16 @@ from datetime import datetime
|
|||
|
||||
from .NotifyBase import NotifyBase
|
||||
from ..URLBase import PrivacyMode
|
||||
from ..common import NotifyFormat
|
||||
from ..common import NotifyType
|
||||
from ..utils import is_email
|
||||
from ..utils import parse_emails
|
||||
from ..common import NotifyFormat, NotifyType
|
||||
from ..conversion import convert_between
|
||||
from ..utils import is_email, parse_emails
|
||||
from ..AppriseLocale import gettext_lazy as _
|
||||
|
||||
# Globally Default encoding mode set to Quoted Printable.
|
||||
charset.add_charset('utf-8', charset.QP, charset.QP, 'utf-8')
|
||||
|
||||
|
||||
class WebBaseLogin(object):
|
||||
class WebBaseLogin:
|
||||
"""
|
||||
This class is just used in conjunction of the default emailers
|
||||
to best formulate a login to it using the data detected
|
||||
|
@ -61,7 +59,7 @@ class WebBaseLogin(object):
|
|||
|
||||
|
||||
# Secure Email Modes
|
||||
class SecureMailMode(object):
|
||||
class SecureMailMode:
|
||||
SSL = "ssl"
|
||||
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',
|
||||
|
@ -126,7 +109,7 @@ EMAIL_TEMPLATES = (
|
|||
'Microsoft Hotmail',
|
||||
re.compile(
|
||||
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,
|
||||
'smtp_host': 'smtp-mail.outlook.com',
|
||||
|
@ -412,6 +395,11 @@ class NotifyEmail(NotifyBase):
|
|||
'default': SecureMailMode.STARTTLS,
|
||||
'map_to': 'secure_mode',
|
||||
},
|
||||
'reply': {
|
||||
'name': _('Reply To'),
|
||||
'type': 'list:string',
|
||||
'map_to': 'reply_to',
|
||||
},
|
||||
})
|
||||
|
||||
# Define any kwargs we're using
|
||||
|
@ -424,7 +412,7 @@ class NotifyEmail(NotifyBase):
|
|||
|
||||
def __init__(self, smtp_host=None, from_name=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
|
||||
|
||||
|
@ -450,6 +438,9 @@ class NotifyEmail(NotifyBase):
|
|||
# Acquire Blind Carbon Copies
|
||||
self.bcc = set()
|
||||
|
||||
# Acquire Reply To
|
||||
self.reply_to = set()
|
||||
|
||||
# For tracking our email -> name lookups
|
||||
self.names = {}
|
||||
|
||||
|
@ -482,13 +473,17 @@ class NotifyEmail(NotifyBase):
|
|||
# Set our from 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
|
||||
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
|
||||
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()
|
||||
if self.secure_mode not in SECURE_MODES:
|
||||
msg = 'The secure mode specified ({}) is invalid.'\
|
||||
|
@ -548,8 +543,24 @@ class NotifyEmail(NotifyBase):
|
|||
'({}) 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
|
||||
self.NotifyEmailDefaults()
|
||||
self.NotifyEmailDefaults(secure_mode=secure_mode, **kwargs)
|
||||
|
||||
# if there is still no smtp_host then we fall back to the hostname
|
||||
if not self.smtp_host:
|
||||
|
@ -557,7 +568,7 @@ class NotifyEmail(NotifyBase):
|
|||
|
||||
return
|
||||
|
||||
def NotifyEmailDefaults(self):
|
||||
def NotifyEmailDefaults(self, secure_mode=None, port=None, **kwargs):
|
||||
"""
|
||||
A function that prefills defaults based on the email
|
||||
it was provided.
|
||||
|
@ -586,18 +597,23 @@ class NotifyEmail(NotifyBase):
|
|||
'Applying %s Defaults' %
|
||||
EMAIL_TEMPLATES[i][0],
|
||||
)
|
||||
self.port = EMAIL_TEMPLATES[i][2]\
|
||||
.get('port', self.port)
|
||||
# the secure flag can not be altered if defined in the template
|
||||
self.secure = EMAIL_TEMPLATES[i][2]\
|
||||
.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]\
|
||||
.get('smtp_host', self.smtp_host)
|
||||
|
||||
if self.smtp_host is None:
|
||||
# default to our host
|
||||
self.smtp_host = self.host
|
||||
# The following can be over-ridden if defined manually in the
|
||||
# Apprise URL. Otherwise they take on the template value
|
||||
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
|
||||
# was specified, then we default to having them all set (which
|
||||
|
@ -622,6 +638,18 @@ class NotifyEmail(NotifyBase):
|
|||
|
||||
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,
|
||||
**kwargs):
|
||||
"""
|
||||
|
@ -652,26 +680,24 @@ class NotifyEmail(NotifyBase):
|
|||
# Strip target out of bcc list if in To
|
||||
bcc = (self.bcc - set([to_addr]))
|
||||
|
||||
try:
|
||||
# Format our cc addresses to support the Name field
|
||||
cc = [formataddr(
|
||||
# Strip target out of reply_to list if in To
|
||||
reply_to = (self.reply_to - set([to_addr]))
|
||||
|
||||
# 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')
|
||||
for addr in cc]
|
||||
|
||||
# Format our bcc addresses to support the Name field
|
||||
bcc = [formataddr(
|
||||
(self.names.get(addr, False), addr), charset='utf-8')
|
||||
for addr in bcc]
|
||||
|
||||
except TypeError:
|
||||
# Python v2.x Support (no charset keyword)
|
||||
# Format our cc addresses to support the Name field
|
||||
cc = [formataddr( # 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]
|
||||
for addr in reply_to]
|
||||
|
||||
self.logger.debug(
|
||||
'Email From: {} <{}>'.format(from_name, self.from_addr))
|
||||
|
@ -680,45 +706,29 @@ class NotifyEmail(NotifyBase):
|
|||
self.logger.debug('Email Cc: {}'.format(', '.join(cc)))
|
||||
if 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(
|
||||
'Delivery: {}:{}'.format(self.smtp_host, self.port))
|
||||
|
||||
# Prepare Email Message
|
||||
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:
|
||||
content = 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
|
||||
base = MIMEText(body, 'plain', 'utf-8')
|
||||
|
||||
if attach:
|
||||
# First attach our body to our content as the first element
|
||||
base.attach(content)
|
||||
|
||||
mixed = MIMEMultipart("mixed")
|
||||
mixed.attach(base)
|
||||
# Now store our attachments
|
||||
for attachment in attach:
|
||||
if not attachment:
|
||||
|
@ -745,8 +755,28 @@ class NotifyEmail(NotifyBase):
|
|||
'attachment; filename="{}"'.format(
|
||||
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
|
||||
socket = None
|
||||
|
@ -839,6 +869,14 @@ class NotifyEmail(NotifyBase):
|
|||
'' if not e not in self.names
|
||||
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)
|
||||
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
|
||||
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
|
||||
if 'smtp' in results['qsd'] and len(results['qsd']['smtp']):
|
||||
# Extract the smtp server
|
||||
|
@ -940,6 +971,10 @@ class NotifyEmail(NotifyBase):
|
|||
if 'bcc' in results['qsd'] and len(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['smtp_host'] = smtp_host
|
||||
|
||||
|
|
|
@ -677,7 +677,7 @@ class NotifyEmby(NotifyBase):
|
|||
|
||||
def __del__(self):
|
||||
"""
|
||||
Deconstructor
|
||||
Destructor
|
||||
"""
|
||||
try:
|
||||
self.logout()
|
||||
|
@ -694,20 +694,20 @@ class NotifyEmby(NotifyBase):
|
|||
# - https://bugs.python.org/issue29288
|
||||
#
|
||||
# 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
|
||||
pass
|
||||
|
||||
except ImportError: # pragma: no cover
|
||||
# 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
|
||||
|
||||
# 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
|
||||
# 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.
|
||||
|
||||
# https://stackoverflow.com/questions/67218341/\
|
||||
|
@ -719,6 +719,6 @@ class NotifyEmby(NotifyBase):
|
|||
# /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)
|
||||
# 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.
|
||||
pass
|
||||
|
|
|
@ -31,7 +31,6 @@
|
|||
# - https://github.com/E2OpenPlugins/e2openplugin-OpenWebif/wiki/\
|
||||
# OpenWebif-API-documentation#message
|
||||
|
||||
import six
|
||||
import requests
|
||||
from json import loads
|
||||
|
||||
|
@ -41,7 +40,7 @@ from ..common import NotifyType
|
|||
from ..AppriseLocale import gettext_lazy as _
|
||||
|
||||
|
||||
class Enigma2MessageType(object):
|
||||
class Enigma2MessageType:
|
||||
# Defines the Enigma2 notification types Apprise can map to
|
||||
INFO = 1
|
||||
WARNING = 2
|
||||
|
@ -169,7 +168,7 @@ class NotifyEnigma2(NotifyBase):
|
|||
self.timeout = self.template_args['timeout']['default']
|
||||
|
||||
self.fullpath = kwargs.get('fullpath')
|
||||
if not isinstance(self.fullpath, six.string_types):
|
||||
if not isinstance(self.fullpath, str):
|
||||
self.fullpath = '/'
|
||||
|
||||
self.headers = {}
|
||||
|
@ -337,18 +336,10 @@ class NotifyEnigma2(NotifyBase):
|
|||
return results
|
||||
|
||||
# Add our headers that the user can potentially over-ride if they wish
|
||||
# to to our returned result set
|
||||
results['headers'] = results['qsd+']
|
||||
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
|
||||
# to to our returned result set and tidy entries by unquoting them
|
||||
results['headers'] = {
|
||||
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)
|
||||
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
|
||||
# You will need this in order to send an apprise messag
|
||||
import six
|
||||
import requests
|
||||
from json import dumps
|
||||
from ..NotifyBase import NotifyBase
|
||||
|
@ -53,6 +52,7 @@ from ...common import NotifyType
|
|||
from ...utils import validate_regex
|
||||
from ...utils import parse_list
|
||||
from ...utils import parse_bool
|
||||
from ...utils import dict_full_update
|
||||
from ...common import NotifyImageSize
|
||||
from ...AppriseAttachment import AppriseAttachment
|
||||
from ...AppriseLocale import gettext_lazy as _
|
||||
|
@ -73,7 +73,7 @@ except ImportError:
|
|||
# cryptography is the dependency of the .oauth library
|
||||
|
||||
# Create a dummy object for init() call to work
|
||||
class GoogleOAuth(object):
|
||||
class GoogleOAuth:
|
||||
pass
|
||||
|
||||
|
||||
|
@ -227,7 +227,7 @@ class NotifyFCM(NotifyBase):
|
|||
else:
|
||||
# Setup our mode
|
||||
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:
|
||||
msg = 'The FCM mode specified ({}) is invalid.'.format(mode)
|
||||
self.logger.warning(msg)
|
||||
|
@ -450,17 +450,9 @@ class NotifyFCM(NotifyBase):
|
|||
"FCM recipient %s parsed as a device token",
|
||||
recipient)
|
||||
|
||||
#
|
||||
# Apply our priority configuration (if set)
|
||||
#
|
||||
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())
|
||||
# A more advanced dict.update() that recursively includes
|
||||
# sub-dictionaries as well
|
||||
dict_full_update(payload, self.priority.payload())
|
||||
|
||||
self.logger.debug(
|
||||
'FCM %s POST URL: %s (cert_verify=%r)',
|
||||
|
|
|
@ -31,13 +31,12 @@
|
|||
# https://firebase.google.com/docs/reference/fcm/rest/v1/\
|
||||
# projects.messages#androidnotification
|
||||
import re
|
||||
import six
|
||||
from ...utils import parse_bool
|
||||
from ...common import NotifyType
|
||||
from ...AppriseAsset import AppriseAsset
|
||||
|
||||
|
||||
class FCMColorManager(object):
|
||||
class FCMColorManager:
|
||||
"""
|
||||
A Simple object to accept either a boolean value
|
||||
- True: Use colors provided by Apprise
|
||||
|
@ -63,7 +62,7 @@ class FCMColorManager(object):
|
|||
|
||||
# Prepare our color
|
||||
self.color = color
|
||||
if isinstance(color, six.string_types):
|
||||
if isinstance(color, str):
|
||||
self.color = self.__color_rgb.match(color)
|
||||
if self.color:
|
||||
# Store our RGB value as #rrggbb
|
||||
|
@ -112,16 +111,8 @@ class FCMColorManager(object):
|
|||
|
||||
def __bool__(self):
|
||||
"""
|
||||
Allows this object to be wrapped in an Python 3.x based 'if
|
||||
statement'. True is returned if a color was loaded
|
||||
Allows this object to be wrapped in an '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
|
||||
|
||||
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
|
||||
isinstance(self.color, str) else False
|
||||
|
|
|
@ -22,7 +22,7 @@
|
|||
# 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.
|
||||
class FCMMode(object):
|
||||
class FCMMode:
|
||||
"""
|
||||
Define the Firebase Cloud Messaging Modes
|
||||
"""
|
||||
|
|
|
@ -29,7 +29,6 @@
|
|||
# 2. Click Generate New Private Key, then confirm by clicking Generate Key.
|
||||
# 3. Securely store the JSON file containing the key.
|
||||
|
||||
import io
|
||||
import requests
|
||||
import base64
|
||||
import json
|
||||
|
@ -41,26 +40,13 @@ from cryptography.hazmat.primitives import asymmetric
|
|||
from cryptography.exceptions import UnsupportedAlgorithm
|
||||
from datetime import datetime
|
||||
from datetime import timedelta
|
||||
from json.decoder import JSONDecodeError
|
||||
from urllib.parse import urlencode as _urlencode
|
||||
|
||||
from ...logger import logger
|
||||
|
||||
try:
|
||||
# Python 2.7
|
||||
from urllib import urlencode as _urlencode
|
||||
|
||||
except ImportError:
|
||||
# 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):
|
||||
class GoogleOAuth:
|
||||
"""
|
||||
A OAuth simplified implimentation to Google's Firebase Cloud Messaging
|
||||
|
||||
|
@ -127,7 +113,7 @@ class GoogleOAuth(object):
|
|||
self.__access_token_expiry = datetime.utcnow()
|
||||
|
||||
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())
|
||||
|
||||
except (OSError, IOError):
|
||||
|
|
|
@ -33,7 +33,7 @@ from .common import (FCMMode, FCM_MODES)
|
|||
from ...logger import logger
|
||||
|
||||
|
||||
class NotificationPriority(object):
|
||||
class NotificationPriority:
|
||||
"""
|
||||
Defines the Notification Priorities as described on:
|
||||
https://firebase.google.com/docs/reference/fcm/rest/v1/\
|
||||
|
@ -63,7 +63,7 @@ class NotificationPriority(object):
|
|||
HIGH = 'HIGH'
|
||||
|
||||
|
||||
class FCMPriority(object):
|
||||
class FCMPriority:
|
||||
"""
|
||||
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
|
||||
"""
|
||||
|
@ -242,14 +242,7 @@ class FCMPriorityManager(object):
|
|||
|
||||
def __bool__(self):
|
||||
"""
|
||||
Allows this object to be wrapped in an Python 3.x based 'if
|
||||
statement'. 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
|
||||
Allows this object to be wrapped in an 'if statement'.
|
||||
True is returned if a priority was loaded
|
||||
"""
|
||||
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
|
||||
# THE SOFTWARE.
|
||||
|
||||
import six
|
||||
import requests
|
||||
|
||||
from .NotifyBase import NotifyBase
|
||||
|
@ -137,11 +136,11 @@ class NotifyForm(NotifyBase):
|
|||
super(NotifyForm, self).__init__(**kwargs)
|
||||
|
||||
self.fullpath = kwargs.get('fullpath')
|
||||
if not isinstance(self.fullpath, six.string_types):
|
||||
if not isinstance(self.fullpath, str):
|
||||
self.fullpath = ''
|
||||
|
||||
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:
|
||||
msg = 'The method specified ({}) is invalid.'.format(method)
|
||||
|
@ -374,17 +373,9 @@ class NotifyForm(NotifyBase):
|
|||
for x, y in results['qsd:'].items()}
|
||||
|
||||
# Add our headers that the user can potentially over-ride if they wish
|
||||
# to to our returned result set
|
||||
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
|
||||
# to to our returned result set and tidy entries by unquoting them
|
||||
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
|
||||
if 'method' in results['qsd'] and len(results['qsd']['method']):
|
||||
|
|
|
@ -60,17 +60,36 @@ except (ImportError, ValueError, AttributeError):
|
|||
|
||||
|
||||
# Urgencies
|
||||
class GnomeUrgency(object):
|
||||
class GnomeUrgency:
|
||||
LOW = 0
|
||||
NORMAL = 1
|
||||
HIGH = 2
|
||||
|
||||
|
||||
GNOME_URGENCIES = (
|
||||
GnomeUrgency.LOW,
|
||||
GnomeUrgency.NORMAL,
|
||||
GnomeUrgency.HIGH,
|
||||
)
|
||||
GNOME_URGENCIES = {
|
||||
GnomeUrgency.LOW: 'low',
|
||||
GnomeUrgency.NORMAL: 'normal',
|
||||
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):
|
||||
|
@ -126,6 +145,12 @@ class NotifyGnome(NotifyBase):
|
|||
'values': GNOME_URGENCIES,
|
||||
'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': {
|
||||
'name': _('Include Image'),
|
||||
'type': 'bool',
|
||||
|
@ -142,11 +167,13 @@ class NotifyGnome(NotifyBase):
|
|||
super(NotifyGnome, self).__init__(**kwargs)
|
||||
|
||||
# The urgency of the message
|
||||
if urgency not in GNOME_URGENCIES:
|
||||
self.urgency = self.template_args['urgency']['default']
|
||||
|
||||
else:
|
||||
self.urgency = urgency
|
||||
self.urgency = int(
|
||||
NotifyGnome.template_args['urgency']['default']
|
||||
if urgency is None else
|
||||
next((
|
||||
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
|
||||
# or not.
|
||||
|
@ -205,17 +232,13 @@ class NotifyGnome(NotifyBase):
|
|||
Returns the URL built dynamically based on specified arguments.
|
||||
"""
|
||||
|
||||
_map = {
|
||||
GnomeUrgency.LOW: 'low',
|
||||
GnomeUrgency.NORMAL: 'normal',
|
||||
GnomeUrgency.HIGH: 'high',
|
||||
}
|
||||
|
||||
# Define any URL parameters
|
||||
params = {
|
||||
'image': 'yes' if self.include_image else 'no',
|
||||
'urgency': 'normal' if self.urgency not in _map
|
||||
else _map[self.urgency],
|
||||
'urgency':
|
||||
GNOME_URGENCIES[self.template_args['urgency']['default']]
|
||||
if self.urgency not in GNOME_URGENCIES
|
||||
else GNOME_URGENCIES[self.urgency],
|
||||
}
|
||||
|
||||
# Extend our parameters
|
||||
|
@ -243,23 +266,13 @@ class NotifyGnome(NotifyBase):
|
|||
|
||||
# Gnome supports urgency, but we we also support the keyword priority
|
||||
# so that it is consistent with some of the other plugins
|
||||
urgency = results['qsd'].get('urgency', results['qsd'].get('priority'))
|
||||
if urgency and len(urgency):
|
||||
_map = {
|
||||
'0': GnomeUrgency.LOW,
|
||||
'l': GnomeUrgency.LOW,
|
||||
'n': GnomeUrgency.NORMAL,
|
||||
'1': GnomeUrgency.NORMAL,
|
||||
'h': GnomeUrgency.HIGH,
|
||||
'2': GnomeUrgency.HIGH,
|
||||
}
|
||||
if 'priority' in results['qsd'] and len(results['qsd']['priority']):
|
||||
# We intentionally store the priority in the urgency section
|
||||
results['urgency'] = \
|
||||
NotifyGnome.unquote(results['qsd']['priority'])
|
||||
|
||||
try:
|
||||
# Attempt to index/retrieve our urgency
|
||||
results['urgency'] = _map[urgency[0].lower()]
|
||||
|
||||
except KeyError:
|
||||
# No priority was set
|
||||
pass
|
||||
if 'urgency' in results['qsd'] and len(results['qsd']['urgency']):
|
||||
results['urgency'] = \
|
||||
NotifyGnome.unquote(results['qsd']['urgency'])
|
||||
|
||||
return results
|
||||
|
|
|
@ -41,7 +41,7 @@ from ..AppriseLocale import gettext_lazy as _
|
|||
|
||||
|
||||
# Priorities
|
||||
class GotifyPriority(object):
|
||||
class GotifyPriority:
|
||||
LOW = 0
|
||||
MODERATE = 3
|
||||
NORMAL = 5
|
||||
|
@ -49,13 +49,37 @@ class GotifyPriority(object):
|
|||
EMERGENCY = 10
|
||||
|
||||
|
||||
GOTIFY_PRIORITIES = (
|
||||
GotifyPriority.LOW,
|
||||
GotifyPriority.MODERATE,
|
||||
GotifyPriority.NORMAL,
|
||||
GotifyPriority.HIGH,
|
||||
GotifyPriority.EMERGENCY,
|
||||
)
|
||||
GOTIFY_PRIORITIES = {
|
||||
# Note: This also acts as a reverse lookup mapping
|
||||
GotifyPriority.LOW: 'low',
|
||||
GotifyPriority.MODERATE: 'moderate',
|
||||
GotifyPriority.NORMAL: 'normal',
|
||||
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):
|
||||
|
@ -144,11 +168,14 @@ class NotifyGotify(NotifyBase):
|
|||
# prepare our fullpath
|
||||
self.fullpath = kwargs.get('fullpath', '/')
|
||||
|
||||
if priority not in GOTIFY_PRIORITIES:
|
||||
self.priority = GotifyPriority.NORMAL
|
||||
|
||||
else:
|
||||
self.priority = priority
|
||||
# The Priority of the message
|
||||
self.priority = int(
|
||||
NotifyGotify.template_args['priority']['default']
|
||||
if priority is None else
|
||||
next((
|
||||
v for k, v in GOTIFY_PRIORITY_MAP.items()
|
||||
if str(priority).lower().startswith(k)),
|
||||
NotifyGotify.template_args['priority']['default']))
|
||||
|
||||
if self.secure:
|
||||
self.schema = 'https'
|
||||
|
@ -246,7 +273,10 @@ class NotifyGotify(NotifyBase):
|
|||
|
||||
# Define any URL parameters
|
||||
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
|
||||
|
@ -294,20 +324,9 @@ class NotifyGotify(NotifyBase):
|
|||
results['fullpath'] = \
|
||||
'/' if not entries else '/{}/'.format('/'.join(entries))
|
||||
|
||||
# Set our priority
|
||||
if 'priority' in results['qsd'] and len(results['qsd']['priority']):
|
||||
_map = {
|
||||
'l': GotifyPriority.LOW,
|
||||
'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
|
||||
results['priority'] = \
|
||||
NotifyGotify.unquote(results['qsd']['priority'])
|
||||
|
||||
return results
|
||||
|
|
|
@ -46,7 +46,7 @@ except ImportError:
|
|||
|
||||
|
||||
# Priorities
|
||||
class GrowlPriority(object):
|
||||
class GrowlPriority:
|
||||
LOW = -2
|
||||
MODERATE = -1
|
||||
NORMAL = 0
|
||||
|
@ -54,13 +54,34 @@ class GrowlPriority(object):
|
|||
EMERGENCY = 2
|
||||
|
||||
|
||||
GROWL_PRIORITIES = (
|
||||
GrowlPriority.LOW,
|
||||
GrowlPriority.MODERATE,
|
||||
GrowlPriority.NORMAL,
|
||||
GrowlPriority.HIGH,
|
||||
GrowlPriority.EMERGENCY,
|
||||
)
|
||||
GROWL_PRIORITIES = {
|
||||
# Note: This also acts as a reverse lookup mapping
|
||||
GrowlPriority.LOW: 'low',
|
||||
GrowlPriority.MODERATE: 'moderate',
|
||||
GrowlPriority.NORMAL: 'normal',
|
||||
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):
|
||||
|
@ -172,11 +193,12 @@ class NotifyGrowl(NotifyBase):
|
|||
self.port = self.default_port
|
||||
|
||||
# The Priority of the message
|
||||
if priority not in GROWL_PRIORITIES:
|
||||
self.priority = GrowlPriority.NORMAL
|
||||
|
||||
else:
|
||||
self.priority = priority
|
||||
self.priority = NotifyGrowl.template_args['priority']['default'] \
|
||||
if not priority else \
|
||||
next((
|
||||
v for k, v in GROWL_PRIORITY_MAP.items()
|
||||
if str(priority).lower().startswith(k)),
|
||||
NotifyGrowl.template_args['priority']['default'])
|
||||
|
||||
# Our Registered object
|
||||
self.growl = None
|
||||
|
@ -318,21 +340,14 @@ class NotifyGrowl(NotifyBase):
|
|||
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
|
||||
params = {
|
||||
'image': 'yes' if self.include_image else 'no',
|
||||
'sticky': 'yes' if self.sticky else 'no',
|
||||
'priority':
|
||||
_map[GrowlPriority.NORMAL] if self.priority not in _map
|
||||
else _map[self.priority],
|
||||
GROWL_PRIORITIES[self.template_args['priority']['default']]
|
||||
if self.priority not in GROWL_PRIORITIES
|
||||
else GROWL_PRIORITIES[self.priority],
|
||||
'version': self.version,
|
||||
}
|
||||
|
||||
|
@ -384,33 +399,10 @@ class NotifyGrowl(NotifyBase):
|
|||
)
|
||||
pass
|
||||
|
||||
# Set our priority
|
||||
if 'priority' in results['qsd'] and len(results['qsd']['priority']):
|
||||
_map = {
|
||||
# Letter Assignments
|
||||
'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
|
||||
results['priority'] = \
|
||||
NotifyGrowl.unquote(results['qsd']['priority'])
|
||||
|
||||
# Because of the URL formatting, the password is actually where the
|
||||
# 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
|
||||
'name': _('Notification ID'),
|
||||
'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
|
||||
# THE SOFTWARE.
|
||||
|
||||
import six
|
||||
import requests
|
||||
import base64
|
||||
from json import dumps
|
||||
|
@ -139,11 +138,11 @@ class NotifyJSON(NotifyBase):
|
|||
super(NotifyJSON, self).__init__(**kwargs)
|
||||
|
||||
self.fullpath = kwargs.get('fullpath')
|
||||
if not isinstance(self.fullpath, six.string_types):
|
||||
if not isinstance(self.fullpath, str):
|
||||
self.fullpath = ''
|
||||
|
||||
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:
|
||||
msg = 'The method specified ({}) is invalid.'.format(method)
|
||||
|
@ -361,17 +360,9 @@ class NotifyJSON(NotifyBase):
|
|||
for x, y in results['qsd:'].items()}
|
||||
|
||||
# Add our headers that the user can potentially over-ride if they wish
|
||||
# to to our returned result set
|
||||
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
|
||||
# to to our returned result set and tidy entries by unquoting them
|
||||
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
|
||||
if 'method' in results['qsd'] and len(results['qsd']['method']):
|
||||
|
|
|
@ -63,7 +63,7 @@ JOIN_IMAGE_XY = NotifyImageSize.XY_72
|
|||
|
||||
|
||||
# Priorities
|
||||
class JoinPriority(object):
|
||||
class JoinPriority:
|
||||
LOW = -2
|
||||
MODERATE = -1
|
||||
NORMAL = 0
|
||||
|
@ -71,13 +71,34 @@ class JoinPriority(object):
|
|||
EMERGENCY = 2
|
||||
|
||||
|
||||
JOIN_PRIORITIES = (
|
||||
JoinPriority.LOW,
|
||||
JoinPriority.MODERATE,
|
||||
JoinPriority.NORMAL,
|
||||
JoinPriority.HIGH,
|
||||
JoinPriority.EMERGENCY,
|
||||
)
|
||||
JOIN_PRIORITIES = {
|
||||
# Note: This also acts as a reverse lookup mapping
|
||||
JoinPriority.LOW: 'low',
|
||||
JoinPriority.MODERATE: 'moderate',
|
||||
JoinPriority.NORMAL: 'normal',
|
||||
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):
|
||||
|
@ -189,11 +210,13 @@ class NotifyJoin(NotifyBase):
|
|||
raise TypeError(msg)
|
||||
|
||||
# The Priority of the message
|
||||
if priority not in JOIN_PRIORITIES:
|
||||
self.priority = self.template_args['priority']['default']
|
||||
|
||||
else:
|
||||
self.priority = priority
|
||||
self.priority = int(
|
||||
NotifyJoin.template_args['priority']['default']
|
||||
if priority is None else
|
||||
next((
|
||||
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
|
||||
self.targets = list()
|
||||
|
@ -324,19 +347,12 @@ class NotifyJoin(NotifyBase):
|
|||
"""
|
||||
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
|
||||
params = {
|
||||
'priority':
|
||||
_map[self.template_args['priority']['default']]
|
||||
if self.priority not in _map else _map[self.priority],
|
||||
JOIN_PRIORITIES[self.template_args['priority']['default']]
|
||||
if self.priority not in JOIN_PRIORITIES
|
||||
else JOIN_PRIORITIES[self.priority],
|
||||
'image': 'yes' if self.include_image else 'no',
|
||||
}
|
||||
|
||||
|
@ -371,20 +387,8 @@ class NotifyJoin(NotifyBase):
|
|||
|
||||
# Set our priority
|
||||
if 'priority' in results['qsd'] and len(results['qsd']['priority']):
|
||||
_map = {
|
||||
'l': JoinPriority.LOW,
|
||||
'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
|
||||
results['priority'] = \
|
||||
NotifyJoin.unquote(results['qsd']['priority'])
|
||||
|
||||
# Our Devices
|
||||
results['targets'] = list()
|
||||
|
|
|
@ -85,7 +85,6 @@
|
|||
|
||||
|
||||
import re
|
||||
import six
|
||||
import requests
|
||||
from json import dumps
|
||||
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)
|
||||
|
||||
|
||||
class LametricMode(object):
|
||||
class LametricMode:
|
||||
"""
|
||||
Define Lametric Notification Modes
|
||||
"""
|
||||
|
@ -121,7 +120,7 @@ LAMETRIC_MODES = (
|
|||
)
|
||||
|
||||
|
||||
class LametricPriority(object):
|
||||
class LametricPriority:
|
||||
"""
|
||||
Priority of the message
|
||||
"""
|
||||
|
@ -158,7 +157,7 @@ LAMETRIC_PRIORITIES = (
|
|||
)
|
||||
|
||||
|
||||
class LametricIconType(object):
|
||||
class LametricIconType:
|
||||
"""
|
||||
Represents the nature of notification.
|
||||
"""
|
||||
|
@ -184,7 +183,7 @@ LAMETRIC_ICON_TYPES = (
|
|||
)
|
||||
|
||||
|
||||
class LametricSoundCategory(object):
|
||||
class LametricSoundCategory:
|
||||
"""
|
||||
Define Sound Categories
|
||||
"""
|
||||
|
@ -192,7 +191,7 @@ class LametricSoundCategory(object):
|
|||
ALARMS = "alarms"
|
||||
|
||||
|
||||
class LametricSound(object):
|
||||
class LametricSound:
|
||||
"""
|
||||
There are 2 categories of sounds, to make things simple we just lump them
|
||||
all togther in one class object.
|
||||
|
@ -471,7 +470,7 @@ class NotifyLametric(NotifyBase):
|
|||
super(NotifyLametric, self).__init__(**kwargs)
|
||||
|
||||
self.mode = mode.strip().lower() \
|
||||
if isinstance(mode, six.string_types) \
|
||||
if isinstance(mode, str) \
|
||||
else self.template_args['mode']['default']
|
||||
|
||||
# Default Cloud Argument
|
||||
|
@ -543,7 +542,7 @@ class NotifyLametric(NotifyBase):
|
|||
# assign our icon (if it was defined); we also eliminate
|
||||
# any hashtag (#) entries that might be present
|
||||
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:
|
||||
self.icon_type = self.template_args['icon_type']['default']
|
||||
|
@ -557,7 +556,7 @@ class NotifyLametric(NotifyBase):
|
|||
cycles > self.template_args['cycles']['min']) else cycles
|
||||
|
||||
self.sound = None
|
||||
if isinstance(sound, six.string_types):
|
||||
if isinstance(sound, str):
|
||||
# If sound is set, get it's match
|
||||
self.sound = self.sound_lookup(sound.strip().lower())
|
||||
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
|
||||
import ssl
|
||||
import re
|
||||
import six
|
||||
from time import sleep
|
||||
from datetime import datetime
|
||||
from os.path import isfile
|
||||
|
@ -46,11 +45,6 @@ from ..AppriseLocale import gettext_lazy as _
|
|||
# Default our global support flag
|
||||
NOTIFY_MQTT_SUPPORT_ENABLED = False
|
||||
|
||||
if six.PY2:
|
||||
# handle Python v2.7 suport
|
||||
class ConnectionError(Exception):
|
||||
pass
|
||||
|
||||
try:
|
||||
# 3rd party modules
|
||||
import paho.mqtt.client as mqtt
|
||||
|
|
|
@ -41,7 +41,7 @@ from ..utils import validate_regex
|
|||
from ..AppriseLocale import gettext_lazy as _
|
||||
|
||||
|
||||
class MSG91Route(object):
|
||||
class MSG91Route:
|
||||
"""
|
||||
Transactional SMS Routes
|
||||
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
|
||||
"""
|
||||
|
|
|
@ -76,6 +76,7 @@
|
|||
import re
|
||||
import requests
|
||||
import json
|
||||
from json.decoder import JSONDecodeError
|
||||
|
||||
from .NotifyBase import NotifyBase
|
||||
from ..common import NotifyImageSize
|
||||
|
@ -88,13 +89,6 @@ from ..utils import TemplateType
|
|||
from ..AppriseAttachment import AppriseAttachment
|
||||
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):
|
||||
"""
|
||||
|
|
|
@ -74,7 +74,7 @@ MAILGUN_HTTP_ERROR_MAP = {
|
|||
|
||||
|
||||
# Priorities
|
||||
class MailgunRegion(object):
|
||||
class MailgunRegion:
|
||||
US = 'us'
|
||||
EU = 'eu'
|
||||
|
||||
|
@ -383,17 +383,9 @@ class NotifyMailgun(NotifyBase):
|
|||
|
||||
return False
|
||||
|
||||
try:
|
||||
reply_to = formataddr(
|
||||
(self.from_name if self.from_name else False,
|
||||
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))
|
||||
reply_to = formataddr(
|
||||
(self.from_name if self.from_name else False,
|
||||
self.from_addr), charset='utf-8')
|
||||
|
||||
# Prepare our payload
|
||||
payload = {
|
||||
|
@ -461,33 +453,17 @@ class NotifyMailgun(NotifyBase):
|
|||
# Strip target out of bcc list if in To
|
||||
bcc = (bcc - set([to_addr[1]]))
|
||||
|
||||
try:
|
||||
# Prepare our to
|
||||
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`
|
||||
to.append(formataddr(to_addr, charset='utf-8'))
|
||||
|
||||
# Prepare our To
|
||||
payload['to'] = ','.join(to)
|
||||
|
||||
if cc:
|
||||
try:
|
||||
# Format our cc addresses to support the Name field
|
||||
payload['cc'] = ','.join([formataddr(
|
||||
(self.names.get(addr, False), addr), charset='utf-8')
|
||||
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 cc addresses to support the Name field
|
||||
payload['cc'] = ','.join([formataddr(
|
||||
(self.names.get(addr, False), addr), charset='utf-8')
|
||||
for addr in cc])
|
||||
|
||||
# Format our bcc addresses to support the Name field
|
||||
if bcc:
|
||||
|
|
|
@ -28,7 +28,6 @@
|
|||
# - https://github.com/matrix-org/synapse/blob/master/docs/reverse_proxy.rst
|
||||
#
|
||||
import re
|
||||
import six
|
||||
import requests
|
||||
from markdown import markdown
|
||||
from json import dumps
|
||||
|
@ -67,7 +66,7 @@ IS_ROOM_ID = re.compile(
|
|||
r'(?P<home_server>[a-z0-9.-]+))?\s*$', re.I)
|
||||
|
||||
|
||||
class MatrixMessageType(object):
|
||||
class MatrixMessageType:
|
||||
"""
|
||||
The Matrix Message types
|
||||
"""
|
||||
|
@ -82,7 +81,7 @@ MATRIX_MESSAGE_TYPES = (
|
|||
)
|
||||
|
||||
|
||||
class MatrixWebhookMode(object):
|
||||
class MatrixWebhookMode:
|
||||
# Webhook Mode is disabled
|
||||
DISABLED = "off"
|
||||
|
||||
|
@ -263,7 +262,7 @@ class NotifyMatrix(NotifyBase):
|
|||
|
||||
# Setup our mode
|
||||
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:
|
||||
msg = 'The mode specified ({}) is invalid.'.format(mode)
|
||||
self.logger.warning(msg)
|
||||
|
@ -271,7 +270,7 @@ class NotifyMatrix(NotifyBase):
|
|||
|
||||
# Setup our message type
|
||||
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:
|
||||
msg = 'The msgtype specified ({}) is invalid.'.format(msgtype)
|
||||
self.logger.warning(msg)
|
||||
|
@ -411,7 +410,7 @@ class NotifyMatrix(NotifyBase):
|
|||
"""
|
||||
|
||||
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 = {
|
||||
# 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
|
||||
return None
|
||||
|
||||
if not isinstance(room, six.string_types):
|
||||
if not isinstance(room, str):
|
||||
# Not a supported string
|
||||
return None
|
||||
|
||||
|
@ -850,7 +849,7 @@ class NotifyMatrix(NotifyBase):
|
|||
# We can't create a room if we're not logged in
|
||||
return None
|
||||
|
||||
if not isinstance(room, six.string_types):
|
||||
if not isinstance(room, str):
|
||||
# Not a supported string
|
||||
return None
|
||||
|
||||
|
@ -930,7 +929,7 @@ class NotifyMatrix(NotifyBase):
|
|||
# We can't get a room id if we're not logged in
|
||||
return None
|
||||
|
||||
if not isinstance(room, six.string_types):
|
||||
if not isinstance(room, str):
|
||||
# Not a supported string
|
||||
return None
|
||||
|
||||
|
@ -1109,20 +1108,20 @@ class NotifyMatrix(NotifyBase):
|
|||
# - https://bugs.python.org/issue29288
|
||||
#
|
||||
# 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
|
||||
pass
|
||||
|
||||
except ImportError: # pragma: no cover
|
||||
# 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
|
||||
|
||||
# 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
|
||||
# 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.
|
||||
|
||||
# https://stackoverflow.com/questions/67218341/\
|
||||
|
@ -1134,7 +1133,7 @@ class NotifyMatrix(NotifyBase):
|
|||
# /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)
|
||||
# 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.
|
||||
pass
|
||||
|
||||
|
|
|
@ -33,7 +33,6 @@
|
|||
# - swap http with mmost
|
||||
# - drop /hooks/ reference
|
||||
|
||||
import six
|
||||
import requests
|
||||
from json import dumps
|
||||
|
||||
|
@ -156,7 +155,7 @@ class NotifyMattermost(NotifyBase):
|
|||
|
||||
# our full path
|
||||
self.fullpath = '' if not isinstance(
|
||||
fullpath, six.string_types) else fullpath.strip()
|
||||
fullpath, str) else fullpath.strip()
|
||||
|
||||
# Authorization Token (associated with project)
|
||||
self.token = validate_regex(token)
|
||||
|
|
|
@ -331,13 +331,10 @@ class NotifyNextcloud(NotifyBase):
|
|||
results['version'] = \
|
||||
NotifyNextcloud.unquote(results['qsd']['version'])
|
||||
|
||||
# Add our headers that the user can potentially over-ride if they
|
||||
# wish to to our returned result set
|
||||
results['headers'] = results['qsd+']
|
||||
if results['qsd-']:
|
||||
results['headers'].update(results['qsd-'])
|
||||
NotifyBase.logger.deprecate(
|
||||
"minus (-) based Nextcloud header tokens are being "
|
||||
" removed; use the plus (+) symbol instead.")
|
||||
# Add our headers that the user can potentially over-ride if they wish
|
||||
# to to our returned result set and tidy entries by unquoting them
|
||||
results['headers'] = {
|
||||
NotifyNextcloud.unquote(x): NotifyNextcloud.unquote(y)
|
||||
for x, y in results['qsd+'].items()}
|
||||
|
||||
return results
|
||||
|
|
|
@ -269,13 +269,10 @@ class NotifyNextcloudTalk(NotifyBase):
|
|||
results['targets'] = \
|
||||
NotifyNextcloudTalk.split_path(results['fullpath'])
|
||||
|
||||
# Add our headers that the user can potentially over-ride if they
|
||||
# wish to to our returned result set
|
||||
results['headers'] = results['qsd+']
|
||||
if results['qsd-']:
|
||||
results['headers'].update(results['qsd-'])
|
||||
NotifyBase.logger.deprecate(
|
||||
"minus (-) based Nextcloud Talk header tokens are being "
|
||||
" removed; use the plus (+) symbol instead.")
|
||||
# Add our headers that the user can potentially over-ride if they wish
|
||||
# to to our returned result set and tidy entries by unquoting them
|
||||
results['headers'] = {
|
||||
NotifyNextcloudTalk.unquote(x): NotifyNextcloudTalk.unquote(y)
|
||||
for x, y in results['qsd+'].items()}
|
||||
|
||||
return results
|
||||
|
|
|
@ -37,7 +37,6 @@
|
|||
# notica://abc123
|
||||
#
|
||||
import re
|
||||
import six
|
||||
import requests
|
||||
|
||||
from .NotifyBase import NotifyBase
|
||||
|
@ -47,7 +46,7 @@ from ..utils import validate_regex
|
|||
from ..AppriseLocale import gettext_lazy as _
|
||||
|
||||
|
||||
class NoticaMode(object):
|
||||
class NoticaMode:
|
||||
"""
|
||||
Tracks if we're accessing the notica upstream server or a locally hosted
|
||||
one.
|
||||
|
@ -176,7 +175,7 @@ class NotifyNotica(NotifyBase):
|
|||
|
||||
# prepare our fullpath
|
||||
self.fullpath = kwargs.get('fullpath')
|
||||
if not isinstance(self.fullpath, six.string_types):
|
||||
if not isinstance(self.fullpath, str):
|
||||
self.fullpath = '/'
|
||||
|
||||
self.headers = {}
|
||||
|
@ -364,13 +363,11 @@ class NotifyNotica(NotifyBase):
|
|||
'/' if not entries else '/{}/'.format('/'.join(entries))
|
||||
|
||||
# Add our headers that the user can potentially over-ride if they
|
||||
# wish to to our returned result set
|
||||
results['headers'] = results['qsd+']
|
||||
if results['qsd-']:
|
||||
results['headers'].update(results['qsd-'])
|
||||
NotifyBase.logger.deprecate(
|
||||
"minus (-) based Notica header tokens are being "
|
||||
" removed; use the plus (+) symbol instead.")
|
||||
# wish to to our returned result set and tidy entries by unquoting
|
||||
# them
|
||||
results['headers'] = {
|
||||
NotifyNotica.unquote(x): NotifyNotica.unquote(y)
|
||||
for x, y in results['qsd+'].items()}
|
||||
|
||||
return results
|
||||
|
||||
|
|
|
@ -48,8 +48,8 @@ from ..utils import validate_regex
|
|||
from ..AppriseLocale import gettext_lazy as _
|
||||
|
||||
|
||||
class NotificoFormat(object):
|
||||
# Resets all formating
|
||||
class NotificoFormat:
|
||||
# Resets all formatting
|
||||
Reset = '\x0F'
|
||||
|
||||
# Formatting
|
||||
|
@ -59,7 +59,7 @@ class NotificoFormat(object):
|
|||
BGSwap = '\x16'
|
||||
|
||||
|
||||
class NotificoColor(object):
|
||||
class NotificoColor:
|
||||
# Resets Color
|
||||
Reset = '\x03'
|
||||
|
||||
|
@ -248,13 +248,13 @@ class NotifyNotifico(NotifyBase):
|
|||
if self.color:
|
||||
# Colors were specified, make sure we capture and correctly
|
||||
# allow them to exist inline in the message
|
||||
# \g<1> is less ambigious than \1
|
||||
body = re.sub(r'\\x03(\d{0,2})', '\x03\g<1>', body)
|
||||
# \g<1> is less ambiguous than \1
|
||||
body = re.sub(r'\\x03(\d{0,2})', r'\\x03\g<1>', body)
|
||||
|
||||
else:
|
||||
# no colors specified, make sure we strip out any colors found
|
||||
# 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
|
||||
payload = {
|
||||
|
|
|
@ -1,24 +1,31 @@
|
|||
# MIT License
|
||||
|
||||
# Copyright (c) 2022 Joey Espinosa <@particledecay>
|
||||
|
||||
# -*- 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
|
||||
# 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
|
||||
# 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:
|
||||
|
||||
# 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
|
||||
# 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.
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
# 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:
|
||||
# ntfys://my-topic
|
||||
|
@ -27,7 +34,6 @@
|
|||
# ntfy://ntfy.local.domain/?priority=max
|
||||
import re
|
||||
import requests
|
||||
import six
|
||||
from json import loads
|
||||
from json import dumps
|
||||
from os.path import basename
|
||||
|
@ -43,7 +49,7 @@ from ..URLBase import PrivacyMode
|
|||
from ..attachment.AttachBase import AttachBase
|
||||
|
||||
|
||||
class NtfyMode(object):
|
||||
class NtfyMode:
|
||||
"""
|
||||
Define ntfy Notification Modes
|
||||
"""
|
||||
|
@ -60,7 +66,7 @@ NTFY_MODES = (
|
|||
)
|
||||
|
||||
|
||||
class NtfyPriority(object):
|
||||
class NtfyPriority:
|
||||
"""
|
||||
Ntfy Priority Definitions
|
||||
"""
|
||||
|
@ -79,6 +85,39 @@ NTFY_PRIORITIES = (
|
|||
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):
|
||||
"""
|
||||
|
@ -207,7 +246,7 @@ class NotifyNtfy(NotifyBase):
|
|||
|
||||
# Prepare our mode
|
||||
self.mode = mode.strip().lower() \
|
||||
if isinstance(mode, six.string_types) \
|
||||
if isinstance(mode, str) \
|
||||
else self.template_args['mode']['default']
|
||||
|
||||
if self.mode not in NTFY_MODES:
|
||||
|
@ -230,18 +269,13 @@ class NotifyNtfy(NotifyBase):
|
|||
# An email to forward notifications to
|
||||
self.email = email
|
||||
|
||||
# The priority of the message
|
||||
|
||||
if priority is None:
|
||||
self.priority = self.template_args['priority']['default']
|
||||
else:
|
||||
self.priority = priority
|
||||
|
||||
if self.priority not in NTFY_PRIORITIES:
|
||||
msg = 'An invalid ntfy Priority ({}) was specified.'.format(
|
||||
priority)
|
||||
self.logger.warning(msg)
|
||||
raise TypeError(msg)
|
||||
# The Priority of the message
|
||||
self.priority = NotifyNtfy.template_args['priority']['default'] \
|
||||
if not priority else \
|
||||
next((
|
||||
v for k, v in NTFY_PRIORITY_MAP.items()
|
||||
if str(priority).lower().startswith(k)),
|
||||
NotifyNtfy.template_args['priority']['default'])
|
||||
|
||||
# Any optional tags to attach to the notification
|
||||
self.__tags = parse_list(tags)
|
||||
|
@ -274,7 +308,7 @@ class NotifyNtfy(NotifyBase):
|
|||
self.logger.warning('There are no ntfy topics to notify')
|
||||
return False
|
||||
|
||||
# Create a copy of the subreddits list
|
||||
# Create a copy of the topics
|
||||
topics = list(self.topics)
|
||||
while len(topics) > 0:
|
||||
# Retrieve our topic
|
||||
|
@ -558,31 +592,10 @@ class NotifyNtfy(NotifyBase):
|
|||
# We're done early as we couldn't load the results
|
||||
return results
|
||||
|
||||
# Set our priority
|
||||
if 'priority' in results['qsd'] and len(results['qsd']['priority']):
|
||||
_map = {
|
||||
# Supported lookups
|
||||
'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
|
||||
results['priority'] = \
|
||||
NotifyNtfy.unquote(results['qsd']['priority'])
|
||||
|
||||
if 'attach' in results['qsd'] and len(results['qsd']['attach']):
|
||||
results['attach'] = NotifyNtfy.unquote(results['qsd']['attach'])
|
||||
|
|
|
@ -74,7 +74,7 @@ OPSGENIE_CATEGORIES = (
|
|||
|
||||
|
||||
# Regions
|
||||
class OpsgenieRegion(object):
|
||||
class OpsgenieRegion:
|
||||
US = 'us'
|
||||
EU = 'eu'
|
||||
|
||||
|
@ -93,7 +93,7 @@ OPSGENIE_REGIONS = (
|
|||
|
||||
|
||||
# Priorities
|
||||
class OpsgeniePriority(object):
|
||||
class OpsgeniePriority:
|
||||
LOW = 1
|
||||
MODERATE = 2
|
||||
NORMAL = 3
|
||||
|
@ -101,13 +101,40 @@ class OpsgeniePriority(object):
|
|||
EMERGENCY = 5
|
||||
|
||||
|
||||
OPSGENIE_PRIORITIES = (
|
||||
OpsgeniePriority.LOW,
|
||||
OpsgeniePriority.MODERATE,
|
||||
OpsgeniePriority.NORMAL,
|
||||
OpsgeniePriority.HIGH,
|
||||
OpsgeniePriority.EMERGENCY,
|
||||
)
|
||||
OPSGENIE_PRIORITIES = {
|
||||
# Note: This also acts as a reverse lookup mapping
|
||||
OpsgeniePriority.LOW: 'low',
|
||||
OpsgeniePriority.MODERATE: 'moderate',
|
||||
OpsgeniePriority.NORMAL: 'normal',
|
||||
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):
|
||||
|
@ -246,11 +273,12 @@ class NotifyOpsgenie(NotifyBase):
|
|||
raise TypeError(msg)
|
||||
|
||||
# The Priority of the message
|
||||
if priority not in OPSGENIE_PRIORITIES:
|
||||
self.priority = OpsgeniePriority.NORMAL
|
||||
|
||||
else:
|
||||
self.priority = priority
|
||||
self.priority = NotifyOpsgenie.template_args['priority']['default'] \
|
||||
if not priority else \
|
||||
next((
|
||||
v for k, v in OPSGENIE_PRIORITY_MAP.items()
|
||||
if str(priority).lower().startswith(k)),
|
||||
NotifyOpsgenie.template_args['priority']['default'])
|
||||
|
||||
# Store our region
|
||||
try:
|
||||
|
@ -353,8 +381,8 @@ class NotifyOpsgenie(NotifyBase):
|
|||
# Initialize our has_error flag
|
||||
has_error = False
|
||||
|
||||
# We want to manually set the title onto the body if specified
|
||||
title_body = body if not title else '{}: {}'.format(title, body)
|
||||
# Use body if title not set
|
||||
title_body = body if not title else body
|
||||
|
||||
# Create a copy ouf our details object
|
||||
details = self.details.copy()
|
||||
|
@ -374,7 +402,7 @@ class NotifyOpsgenie(NotifyBase):
|
|||
# limitation
|
||||
if len(payload['message']) > self.opsgenie_body_minlen:
|
||||
payload['message'] = '{}...'.format(
|
||||
body[:self.opsgenie_body_minlen - 3])
|
||||
title_body[:self.opsgenie_body_minlen - 3])
|
||||
|
||||
if self.__tags:
|
||||
payload['tags'] = self.__tags
|
||||
|
@ -450,20 +478,13 @@ class NotifyOpsgenie(NotifyBase):
|
|||
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
|
||||
params = {
|
||||
'region': self.region_name,
|
||||
'priority':
|
||||
_map[OpsgeniePriority.NORMAL] if self.priority not in _map
|
||||
else _map[self.priority],
|
||||
OPSGENIE_PRIORITIES[self.template_args['priority']['default']]
|
||||
if self.priority not in OPSGENIE_PRIORITIES
|
||||
else OPSGENIE_PRIORITIES[self.priority],
|
||||
'batch': 'yes' if self.batch_size > 1 else 'no',
|
||||
}
|
||||
|
||||
|
@ -530,38 +551,10 @@ class NotifyOpsgenie(NotifyBase):
|
|||
results['details'] = {NotifyBase.unquote(x): NotifyBase.unquote(y)
|
||||
for x, y in results['qsd+'].items()}
|
||||
|
||||
# Set our priority
|
||||
if 'priority' in results['qsd'] and len(results['qsd']['priority']):
|
||||
_map = {
|
||||
# Letter Assignnments
|
||||
'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
|
||||
results['priority'] = \
|
||||
NotifyOpsgenie.unquote(results['qsd']['priority'])
|
||||
|
||||
# Get Batch Boolean (if set)
|
||||
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
|
||||
|
||||
import re
|
||||
import six
|
||||
import requests
|
||||
from json import dumps
|
||||
|
||||
|
@ -40,7 +39,7 @@ TARGET_LIST_DELIM = re.compile(r'[ \t\r\n,\\/]+')
|
|||
|
||||
|
||||
# Priorities
|
||||
class ParsePlatformDevice(object):
|
||||
class ParsePlatformDevice:
|
||||
# All Devices
|
||||
ALL = 'all'
|
||||
|
||||
|
@ -134,7 +133,7 @@ class NotifyParsePlatform(NotifyBase):
|
|||
super(NotifyParsePlatform, self).__init__(**kwargs)
|
||||
|
||||
self.fullpath = kwargs.get('fullpath')
|
||||
if not isinstance(self.fullpath, six.string_types):
|
||||
if not isinstance(self.fullpath, str):
|
||||
self.fullpath = '/'
|
||||
|
||||
# Application ID
|
||||
|
|
|
@ -32,7 +32,7 @@ from ..AppriseLocale import gettext_lazy as _
|
|||
|
||||
|
||||
# Priorities
|
||||
class ProwlPriority(object):
|
||||
class ProwlPriority:
|
||||
LOW = -2
|
||||
MODERATE = -1
|
||||
NORMAL = 0
|
||||
|
@ -40,13 +40,34 @@ class ProwlPriority(object):
|
|||
EMERGENCY = 2
|
||||
|
||||
|
||||
PROWL_PRIORITIES = (
|
||||
ProwlPriority.LOW,
|
||||
ProwlPriority.MODERATE,
|
||||
ProwlPriority.NORMAL,
|
||||
ProwlPriority.HIGH,
|
||||
ProwlPriority.EMERGENCY,
|
||||
)
|
||||
PROWL_PRIORITIES = {
|
||||
# Note: This also acts as a reverse lookup mapping
|
||||
ProwlPriority.LOW: 'low',
|
||||
ProwlPriority.MODERATE: 'moderate',
|
||||
ProwlPriority.NORMAL: 'normal',
|
||||
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:
|
||||
PROWL_HTTP_ERROR_MAP = {
|
||||
|
@ -124,11 +145,13 @@ class NotifyProwl(NotifyBase):
|
|||
"""
|
||||
super(NotifyProwl, self).__init__(**kwargs)
|
||||
|
||||
if priority not in PROWL_PRIORITIES:
|
||||
self.priority = self.template_args['priority']['default']
|
||||
|
||||
else:
|
||||
self.priority = priority
|
||||
# The Priority of the message
|
||||
self.priority = NotifyProwl.template_args['priority']['default'] \
|
||||
if not priority else \
|
||||
next((
|
||||
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)
|
||||
self.apikey = validate_regex(
|
||||
|
@ -229,18 +252,12 @@ class NotifyProwl(NotifyBase):
|
|||
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
|
||||
params = {
|
||||
'priority': 'normal' if self.priority not in _map
|
||||
else _map[self.priority],
|
||||
'priority':
|
||||
PROWL_PRIORITIES[self.template_args['priority']['default']]
|
||||
if self.priority not in PROWL_PRIORITIES
|
||||
else PROWL_PRIORITIES[self.priority],
|
||||
}
|
||||
|
||||
# Extend our parameters
|
||||
|
@ -276,32 +293,9 @@ class NotifyProwl(NotifyBase):
|
|||
except IndexError:
|
||||
pass
|
||||
|
||||
# Set our priority
|
||||
if 'priority' in results['qsd'] and len(results['qsd']['priority']):
|
||||
_map = {
|
||||
# Letter Assignments
|
||||
'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
|
||||
results['priority'] = \
|
||||
NotifyProwl.unquote(results['qsd']['priority'])
|
||||
|
||||
return results
|
||||
|
|
|
@ -23,8 +23,6 @@
|
|||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
# THE SOFTWARE.
|
||||
|
||||
# We use io because it allows us to test the open() call
|
||||
import io
|
||||
import base64
|
||||
import requests
|
||||
from json import loads
|
||||
|
@ -36,7 +34,7 @@ from ..utils import validate_regex
|
|||
from ..AppriseLocale import gettext_lazy as _
|
||||
|
||||
|
||||
class PushSaferSound(object):
|
||||
class PushSaferSound:
|
||||
"""
|
||||
Defines all of the supported PushSafe sounds
|
||||
"""
|
||||
|
@ -248,7 +246,7 @@ PUSHSAFER_SOUND_MAP = {
|
|||
|
||||
|
||||
# Priorities
|
||||
class PushSaferPriority(object):
|
||||
class PushSaferPriority:
|
||||
LOW = -2
|
||||
MODERATE = -1
|
||||
NORMAL = 0
|
||||
|
@ -282,7 +280,7 @@ DEFAULT_PRIORITY = "normal"
|
|||
|
||||
|
||||
# Vibrations
|
||||
class PushSaferVibration(object):
|
||||
class PushSaferVibration:
|
||||
"""
|
||||
Defines the acceptable vibration settings for notification
|
||||
"""
|
||||
|
@ -565,7 +563,7 @@ class NotifyPushSafer(NotifyBase):
|
|||
attachment.url(privacy=True)))
|
||||
|
||||
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
|
||||
# PushSafer calls it):
|
||||
attachment = (
|
||||
|
|
|
@ -24,12 +24,12 @@
|
|||
# THE SOFTWARE.
|
||||
|
||||
import re
|
||||
import six
|
||||
import requests
|
||||
|
||||
from .NotifyBase import NotifyBase
|
||||
from ..common import NotifyType
|
||||
from ..common import NotifyFormat
|
||||
from ..conversion import convert_between
|
||||
from ..utils import parse_list
|
||||
from ..utils import validate_regex
|
||||
from ..AppriseLocale import gettext_lazy as _
|
||||
|
@ -43,7 +43,7 @@ VALIDATE_DEVICE = re.compile(r'^[a-z0-9_]{1,25}$', re.I)
|
|||
|
||||
|
||||
# Priorities
|
||||
class PushoverPriority(object):
|
||||
class PushoverPriority:
|
||||
LOW = -2
|
||||
MODERATE = -1
|
||||
NORMAL = 0
|
||||
|
@ -52,7 +52,7 @@ class PushoverPriority(object):
|
|||
|
||||
|
||||
# Sounds
|
||||
class PushoverSound(object):
|
||||
class PushoverSound:
|
||||
PUSHOVER = 'pushover'
|
||||
BIKE = 'bike'
|
||||
BUGLE = 'bugle'
|
||||
|
@ -102,13 +102,34 @@ PUSHOVER_SOUNDS = (
|
|||
PushoverSound.NONE,
|
||||
)
|
||||
|
||||
PUSHOVER_PRIORITIES = (
|
||||
PushoverPriority.LOW,
|
||||
PushoverPriority.MODERATE,
|
||||
PushoverPriority.NORMAL,
|
||||
PushoverPriority.HIGH,
|
||||
PushoverPriority.EMERGENCY,
|
||||
)
|
||||
PUSHOVER_PRIORITIES = {
|
||||
# Note: This also acts as a reverse lookup mapping
|
||||
PushoverPriority.LOW: 'low',
|
||||
PushoverPriority.MODERATE: 'moderate',
|
||||
PushoverPriority.NORMAL: 'normal',
|
||||
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
|
||||
PUSHOVER_HTTP_ERROR_MAP = {
|
||||
|
@ -258,18 +279,20 @@ class NotifyPushover(NotifyBase):
|
|||
|
||||
# Setup our 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:
|
||||
msg = 'The sound specified ({}) is invalid.'.format(sound)
|
||||
self.logger.warning(msg)
|
||||
raise TypeError(msg)
|
||||
|
||||
# The Priority of the message
|
||||
if priority not in PUSHOVER_PRIORITIES:
|
||||
self.priority = self.template_args['priority']['default']
|
||||
|
||||
else:
|
||||
self.priority = priority
|
||||
self.priority = int(
|
||||
NotifyPushover.template_args['priority']['default']
|
||||
if priority is None else
|
||||
next((
|
||||
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
|
||||
if self.priority == PushoverPriority.EMERGENCY:
|
||||
|
@ -344,6 +367,10 @@ class NotifyPushover(NotifyBase):
|
|||
if self.notify_format == NotifyFormat.HTML:
|
||||
# https://pushover.net/api#html
|
||||
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:
|
||||
payload.update({'retry': self.retry, 'expire': self.expire})
|
||||
|
@ -404,24 +431,25 @@ class NotifyPushover(NotifyBase):
|
|||
attach.mimetype,
|
||||
attach.url(privacy=True)))
|
||||
|
||||
return True
|
||||
attach = None
|
||||
|
||||
# If we get here, we're dealing with a supported image.
|
||||
# Verify that the filesize is okay though.
|
||||
file_size = len(attach)
|
||||
if not (file_size > 0
|
||||
and file_size <= self.attach_max_size_bytes):
|
||||
else:
|
||||
# If we get here, we're dealing with a supported image.
|
||||
# Verify that the filesize is okay though.
|
||||
file_size = len(attach)
|
||||
if not (file_size > 0
|
||||
and file_size <= self.attach_max_size_bytes):
|
||||
|
||||
# File size is no good
|
||||
self.logger.warning(
|
||||
'Pushover attachment size ({}B) exceeds limit: {}'
|
||||
.format(file_size, attach.url(privacy=True)))
|
||||
# File size is no good
|
||||
self.logger.warning(
|
||||
'Pushover attachment size ({}B) exceeds limit: {}'
|
||||
.format(file_size, attach.url(privacy=True)))
|
||||
|
||||
return False
|
||||
return False
|
||||
|
||||
self.logger.debug(
|
||||
'Posting Pushover attachment {}'.format(
|
||||
attach.url(privacy=True)))
|
||||
self.logger.debug(
|
||||
'Posting Pushover attachment {}'.format(
|
||||
attach.url(privacy=True)))
|
||||
|
||||
# Default Header
|
||||
headers = {
|
||||
|
@ -510,19 +538,12 @@ class NotifyPushover(NotifyBase):
|
|||
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
|
||||
params = {
|
||||
'priority':
|
||||
_map[self.template_args['priority']['default']]
|
||||
if self.priority not in _map else _map[self.priority],
|
||||
PUSHOVER_PRIORITIES[self.template_args['priority']['default']]
|
||||
if self.priority not in PUSHOVER_PRIORITIES
|
||||
else PUSHOVER_PRIORITIES[self.priority],
|
||||
}
|
||||
|
||||
# Only add expire and retry for emergency messages,
|
||||
|
@ -563,20 +584,8 @@ class NotifyPushover(NotifyBase):
|
|||
|
||||
# Set our priority
|
||||
if 'priority' in results['qsd'] and len(results['qsd']['priority']):
|
||||
_map = {
|
||||
'l': PushoverPriority.LOW,
|
||||
'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
|
||||
results['priority'] = \
|
||||
NotifyPushover.unquote(results['qsd']['priority'])
|
||||
|
||||
# Retrieve all of our targets
|
||||
results['targets'] = NotifyPushover.split_path(results['fullpath'])
|
||||
|
|
|
@ -44,7 +44,6 @@
|
|||
# - https://www.reddit.com/dev/api/
|
||||
# - https://www.reddit.com/dev/api/#POST_api_submit
|
||||
# - https://github.com/reddit-archive/reddit/wiki/API
|
||||
import six
|
||||
import requests
|
||||
from json import loads
|
||||
from datetime import timedelta
|
||||
|
@ -66,7 +65,7 @@ REDDIT_HTTP_ERROR_MAP = {
|
|||
}
|
||||
|
||||
|
||||
class RedditMessageKind(object):
|
||||
class RedditMessageKind:
|
||||
"""
|
||||
Define the kinds of messages supported
|
||||
"""
|
||||
|
@ -271,7 +270,7 @@ class NotifyReddit(NotifyBase):
|
|||
self.__access_token_expiry = datetime.utcnow()
|
||||
|
||||
self.kind = kind.strip().lower() \
|
||||
if isinstance(kind, six.string_types) \
|
||||
if isinstance(kind, str) \
|
||||
else self.template_args['kind']['default']
|
||||
|
||||
if self.kind not in REDDIT_MESSAGE_KINDS:
|
||||
|
|
|
@ -24,7 +24,6 @@
|
|||
# THE SOFTWARE.
|
||||
|
||||
import re
|
||||
import six
|
||||
import requests
|
||||
from json import loads
|
||||
from json import dumps
|
||||
|
@ -54,7 +53,7 @@ RC_HTTP_ERROR_MAP = {
|
|||
LIST_DELIM = re.compile(r'[ \t\r\n,\\/]+')
|
||||
|
||||
|
||||
class RocketChatAuthMode(object):
|
||||
class RocketChatAuthMode:
|
||||
"""
|
||||
The Chat Authentication mode is detected
|
||||
"""
|
||||
|
@ -218,7 +217,7 @@ class NotifyRocketChat(NotifyBase):
|
|||
|
||||
# Authentication mode
|
||||
self.mode = None \
|
||||
if not isinstance(mode, six.string_types) \
|
||||
if not isinstance(mode, str) \
|
||||
else mode.lower()
|
||||
|
||||
if self.mode and self.mode not in ROCKETCHAT_AUTH_MODES:
|
||||
|
|
|
@ -32,7 +32,6 @@
|
|||
# These are important <---^----------------------------------------^
|
||||
#
|
||||
import re
|
||||
import six
|
||||
import requests
|
||||
from json import dumps
|
||||
|
||||
|
@ -44,7 +43,7 @@ from ..utils import validate_regex
|
|||
from ..AppriseLocale import gettext_lazy as _
|
||||
|
||||
|
||||
class RyverWebhookMode(object):
|
||||
class RyverWebhookMode:
|
||||
"""
|
||||
Ryver supports to webhook modes
|
||||
"""
|
||||
|
@ -152,7 +151,7 @@ class NotifyRyver(NotifyBase):
|
|||
|
||||
# Store our webhook mode
|
||||
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:
|
||||
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.utils import formataddr
|
||||
from email.header import Header
|
||||
try:
|
||||
# Python v3.x
|
||||
from urllib.parse import quote
|
||||
|
||||
except ImportError:
|
||||
# Python v2.x
|
||||
from urllib import quote
|
||||
from urllib.parse import quote
|
||||
|
||||
from .NotifyBase import NotifyBase
|
||||
from ..URLBase import PrivacyMode
|
||||
|
@ -395,26 +389,15 @@ class NotifySES(NotifyBase):
|
|||
# Strip target out of bcc list if in To
|
||||
bcc = (self.bcc - set([to_addr]))
|
||||
|
||||
try:
|
||||
# Format our cc addresses to support the Name field
|
||||
cc = [formataddr(
|
||||
(self.names.get(addr, False), addr), charset='utf-8')
|
||||
for addr in cc]
|
||||
# Format our cc addresses to support the Name field
|
||||
cc = [formataddr(
|
||||
(self.names.get(addr, False), addr), charset='utf-8')
|
||||
for addr in cc]
|
||||
|
||||
# Format our bcc addresses to support the Name field
|
||||
bcc = [formataddr(
|
||||
(self.names.get(addr, False), addr), charset='utf-8')
|
||||
for addr in bcc]
|
||||
|
||||
except TypeError:
|
||||
# Python v2.x Support (no charset keyword)
|
||||
# Format our cc addresses to support the Name field
|
||||
cc = [formataddr( # 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]
|
||||
# Format our bcc addresses to support the Name field
|
||||
bcc = [formataddr(
|
||||
(self.names.get(addr, False), addr), charset='utf-8')
|
||||
for addr in bcc]
|
||||
|
||||
self.logger.debug('Email From: {} <{}>'.format(
|
||||
quote(reply_to[0], ' '),
|
||||
|
@ -436,23 +419,14 @@ class NotifySES(NotifyBase):
|
|||
# Create a Multipart container if there is an attachment
|
||||
base = MIMEMultipart() if attach else content
|
||||
|
||||
# TODO: Deduplicate with `NotifyEmail`?
|
||||
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')
|
||||
if reply_to[1] != self.from_addr:
|
||||
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['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')
|
||||
if reply_to[1] != self.from_addr:
|
||||
base['Reply-To'] = formataddr(reply_to, charset='utf-8')
|
||||
base['Cc'] = ','.join(cc)
|
||||
base['Date'] = \
|
||||
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))
|
||||
return False
|
||||
|
||||
try:
|
||||
sender = formataddr(
|
||||
(self.from_name if self.from_name else False,
|
||||
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))
|
||||
sender = formataddr(
|
||||
(self.from_name if self.from_name else False,
|
||||
self.from_addr), charset='utf-8')
|
||||
|
||||
# Prepare our payload
|
||||
payload = {
|
||||
|
@ -369,33 +361,17 @@ class NotifySMTP2Go(NotifyBase):
|
|||
# Strip target out of bcc list if in To
|
||||
bcc = (bcc - set([to_addr[1]]))
|
||||
|
||||
try:
|
||||
# Prepare our to
|
||||
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`
|
||||
to.append(formataddr(to_addr, charset='utf-8'))
|
||||
|
||||
# Prepare our To
|
||||
payload['to'] = to
|
||||
|
||||
if cc:
|
||||
try:
|
||||
# Format our cc addresses to support the Name field
|
||||
payload['cc'] = [formataddr(
|
||||
(self.names.get(addr, False), addr), charset='utf-8')
|
||||
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 cc addresses to support the Name field
|
||||
payload['cc'] = [formataddr(
|
||||
(self.names.get(addr, False), addr), charset='utf-8')
|
||||
for addr in cc]
|
||||
|
||||
# Format our bcc addresses to support the Name field
|
||||
if bcc:
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2020 Chris Caron <lead2gold@gmail.com>
|
||||
# Copyright (C) 2022 Chris Caron <lead2gold@gmail.com>
|
||||
# All rights reserved.
|
||||
#
|
||||
# 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
|
||||
# THE SOFTWARE.
|
||||
|
||||
import re
|
||||
import requests
|
||||
from json import dumps
|
||||
import base64
|
||||
|
||||
from .NotifyBase import NotifyBase
|
||||
from ..common import NotifyType
|
||||
|
@ -35,6 +37,10 @@ from ..URLBase import PrivacyMode
|
|||
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):
|
||||
"""
|
||||
A wrapper for SignalAPI Notifications
|
||||
|
@ -113,6 +119,13 @@ class NotifySignalAPI(NotifyBase):
|
|||
'regex': (r'^[0-9\s)(+-]+$', 'i'),
|
||||
'map_to': 'targets',
|
||||
},
|
||||
'target_channel': {
|
||||
'name': _('Target Group ID'),
|
||||
'type': 'string',
|
||||
'prefix': '@',
|
||||
'regex': (r'^[a-z0-9_=-]+$', 'i'),
|
||||
'map_to': 'targets',
|
||||
},
|
||||
'targets': {
|
||||
'name': _('Targets'),
|
||||
'type': 'list:string',
|
||||
|
@ -173,23 +186,33 @@ class NotifySignalAPI(NotifyBase):
|
|||
for target in parse_phone_no(targets):
|
||||
# Validate targets and drop bad ones:
|
||||
result = is_phone_no(target)
|
||||
if not result:
|
||||
self.logger.warning(
|
||||
'Dropped invalid phone # '
|
||||
'({}) specified.'.format(target),
|
||||
)
|
||||
self.invalid_targets.append(target)
|
||||
if result:
|
||||
# store valid phone number
|
||||
self.targets.append('+{}'.format(result['full']))
|
||||
continue
|
||||
|
||||
# store valid phone number
|
||||
self.targets.append('+{}'.format(result['full']))
|
||||
result = GROUP_REGEX.match(target)
|
||||
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:
|
||||
# Send a message to ourselves
|
||||
self.targets.append(self.source)
|
||||
|
||||
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
|
||||
"""
|
||||
|
@ -203,12 +226,50 @@ class NotifySignalAPI(NotifyBase):
|
|||
# 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
|
||||
|
||||
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
|
||||
headers = {
|
||||
'User-Agent': self.app_id,
|
||||
'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
|
||||
payload = {
|
||||
'message': "{}{}".format(
|
||||
|
@ -218,6 +279,10 @@ class NotifySignalAPI(NotifyBase):
|
|||
"recipients": []
|
||||
}
|
||||
|
||||
if attachments:
|
||||
# Store our attachments
|
||||
payload['base64_attachments'] = attachments
|
||||
|
||||
# Determine Authentication
|
||||
auth = None
|
||||
if self.user:
|
||||
|
@ -255,7 +320,8 @@ class NotifySignalAPI(NotifyBase):
|
|||
verify=self.verify_certificate,
|
||||
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
|
||||
status_str = \
|
||||
NotifySignalAPI.http_response_code_lookup(
|
||||
|
@ -339,7 +405,11 @@ class NotifySignalAPI(NotifyBase):
|
|||
targets = self.invalid_targets
|
||||
|
||||
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(
|
||||
schema=self.secure_protocol if self.secure else self.protocol,
|
||||
|
@ -350,7 +420,7 @@ class NotifySignalAPI(NotifyBase):
|
|||
else ':{}'.format(self.port),
|
||||
src=self.source,
|
||||
dst='/'.join(
|
||||
[NotifySignalAPI.quote(x, safe='') for x in targets]),
|
||||
[NotifySignalAPI.quote(x, safe='@+') for x in targets]),
|
||||
params=NotifySignalAPI.urlencode(params),
|
||||
)
|
||||
|
||||
|
|
|
@ -33,7 +33,6 @@
|
|||
# from). Activated phone numbers can be found on your dashboard here:
|
||||
# - https://dashboard.sinch.com/numbers/your-numbers/numbers
|
||||
#
|
||||
import six
|
||||
import requests
|
||||
import json
|
||||
|
||||
|
@ -46,7 +45,7 @@ from ..utils import validate_regex
|
|||
from ..AppriseLocale import gettext_lazy as _
|
||||
|
||||
|
||||
class SinchRegion(object):
|
||||
class SinchRegion:
|
||||
"""
|
||||
Defines the Sinch Server Regions
|
||||
"""
|
||||
|
@ -192,7 +191,7 @@ class NotifySinch(NotifyBase):
|
|||
|
||||
# Setup our region
|
||||
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:
|
||||
msg = 'The region specified ({}) is invalid.'.format(region)
|
||||
self.logger.warning(msg)
|
||||
|
|
|
@ -94,7 +94,7 @@ SLACK_HTTP_ERROR_MAP = {
|
|||
CHANNEL_LIST_DELIM = re.compile(r'[ \t\r\n,#\\/]+')
|
||||
|
||||
|
||||
class SlackMode(object):
|
||||
class SlackMode:
|
||||
"""
|
||||
Tracks the mode of which we're using Slack
|
||||
"""
|
||||
|
|
|
@ -80,7 +80,7 @@ SPARKPOST_HTTP_ERROR_MAP = {
|
|||
|
||||
|
||||
# Priorities
|
||||
class SparkPostRegion(object):
|
||||
class SparkPostRegion:
|
||||
US = 'us'
|
||||
EU = 'eu'
|
||||
|
||||
|
@ -503,14 +503,9 @@ class NotifySparkPost(NotifyBase):
|
|||
# Send in batches if identified to do so
|
||||
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,
|
||||
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))
|
||||
reply_to = formataddr((self.from_name if self.from_name else False,
|
||||
self.from_addr), charset='utf-8')
|
||||
|
||||
payload = {
|
||||
"options": {
|
||||
# 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
|
||||
# Finally the channel identifier is detected
|
||||
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)
|
||||
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2021 <example@example.com>
|
||||
# Copyright (C) 2021 Chris Caron <lead2gold@gmail.com>
|
||||
# All rights reserved.
|
||||
#
|
||||
# This code is licensed under the MIT License.
|
||||
|
@ -42,7 +42,7 @@ from ..AppriseLocale import gettext_lazy as _
|
|||
|
||||
|
||||
# calls
|
||||
class StrmlabsCall(object):
|
||||
class StrmlabsCall:
|
||||
ALERT = 'ALERTS'
|
||||
DONATION = 'DONATIONS'
|
||||
|
||||
|
@ -55,7 +55,7 @@ STRMLABS_CALLS = (
|
|||
|
||||
|
||||
# alerts
|
||||
class StrmlabsAlert(object):
|
||||
class StrmlabsAlert:
|
||||
FOLLOW = 'follow'
|
||||
SUBSCRIPTION = 'subscription'
|
||||
DONATION = 'donation'
|
||||
|
|
|
@ -23,7 +23,6 @@
|
|||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
# THE SOFTWARE.
|
||||
import os
|
||||
import six
|
||||
import syslog
|
||||
import socket
|
||||
|
||||
|
@ -101,7 +100,7 @@ SYSLOG_FACILITY_RMAP = {
|
|||
}
|
||||
|
||||
|
||||
class SyslogMode(object):
|
||||
class SyslogMode:
|
||||
# A local query
|
||||
LOCAL = "local"
|
||||
|
||||
|
@ -217,7 +216,7 @@ class NotifySyslog(NotifyBase):
|
|||
self.template_tokens['facility']['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:
|
||||
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
|
||||
# search the body for these sequences and convert them to the
|
||||
# output the user expected
|
||||
__telegram_escape_html_dict = {
|
||||
# New Lines
|
||||
re.compile(r'<\s*/?br\s*/?>\r*\n?', re.I): '\r\n',
|
||||
re.compile(r'<\s*/(br|p|div|li)[^>]*>\r*\n?', re.I): '\r\n',
|
||||
|
||||
# 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>',
|
||||
__telegram_escape_html_entries = (
|
||||
# Comments
|
||||
(re.compile(
|
||||
r'\s*<!.+?-->\s*',
|
||||
(re.I | re.M | re.S)), '', {}),
|
||||
|
||||
# the following tags are not supported
|
||||
re.compile(
|
||||
r'<\s*(br|p|div|span|body|script|meta|html|font'
|
||||
r'|label|iframe|li|ol|ul|source|script)[^>]*>', re.I): '',
|
||||
(re.compile(
|
||||
r'\s*<\s*(!?DOCTYPE|p|div|span|body|script|link|'
|
||||
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(
|
||||
r'<\s*/(span|body|script|meta|html|font'
|
||||
r'|label|iframe|ol|ul|source|script)[^>]*>', re.I): '',
|
||||
|
||||
# Italic
|
||||
re.compile(r'<\s*(caption|em)[^>]*>', re.I): '<i>',
|
||||
re.compile(r'<\s*/(caption|em)[^>]*>', re.I): '</i>',
|
||||
# All closing tags to be removed are put here
|
||||
(re.compile(
|
||||
r'\s*<\s*/(span|body|script|meta|html|font|head|'
|
||||
r'label|form|input|textarea|select|ol|ul|link|'
|
||||
r'iframe|source|script)([^a-z0-9>][^>]*)?>\s*',
|
||||
(re.I | re.M | re.S)), '', {}),
|
||||
|
||||
# Bold
|
||||
re.compile(r'<\s*(h[1-6]|title|strong)[^>]*>', re.I): '<b>',
|
||||
re.compile(r'<\s*/(h[1-6]|title|strong)[^>]*>', re.I): '</b>',
|
||||
(re.compile(
|
||||
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
|
||||
# See https://core.telegram.org/bots/api#html-style
|
||||
re.compile(r'\ ?', re.I): ' ',
|
||||
(re.compile(r'\ ?', re.I), ' ', {}),
|
||||
|
||||
# Tabs become 3 spaces
|
||||
re.compile(r'\ ?', re.I): ' ',
|
||||
(re.compile(r'\ ?', re.I), ' ', {}),
|
||||
|
||||
# Some characters get re-escaped by the Telegram upstream
|
||||
# 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
|
||||
template_tokens = dict(NotifyBase.template_tokens, **{
|
||||
|
@ -597,38 +638,19 @@ class NotifyTelegram(NotifyBase):
|
|||
|
||||
# Use Telegram's HTML mode
|
||||
payload['parse_mode'] = 'HTML'
|
||||
for r, v in self.__telegram_escape_html_dict.items():
|
||||
body = r.sub(v, body, re.I)
|
||||
for r, v, m in self.__telegram_escape_html_entries:
|
||||
|
||||
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
|
||||
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
|
||||
targets = list(self.targets)
|
||||
while len(targets):
|
||||
|
|
|
@ -785,7 +785,7 @@ class NotifyTwist(NotifyBase):
|
|||
|
||||
def __del__(self):
|
||||
"""
|
||||
Deconstructor
|
||||
Destructor
|
||||
"""
|
||||
try:
|
||||
self.logout()
|
||||
|
@ -808,14 +808,14 @@ class NotifyTwist(NotifyBase):
|
|||
|
||||
except ImportError: # pragma: no cover
|
||||
# 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
|
||||
|
||||
# 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
|
||||
# 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.
|
||||
|
||||
# https://stackoverflow.com/questions/67218341/\
|
||||
|
@ -827,6 +827,6 @@ class NotifyTwist(NotifyBase):
|
|||
# /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)
|
||||
# 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.
|
||||
pass
|
||||
|
|
|
@ -26,7 +26,6 @@
|
|||
# See https://developer.twitter.com/en/docs/direct-messages/\
|
||||
# sending-and-receiving/api-reference/new-event.html
|
||||
import re
|
||||
import six
|
||||
import requests
|
||||
from copy import deepcopy
|
||||
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)
|
||||
|
||||
|
||||
class TwitterMessageMode(object):
|
||||
class TwitterMessageMode:
|
||||
"""
|
||||
Twitter Message Mode
|
||||
"""
|
||||
|
@ -223,7 +222,7 @@ class NotifyTwitter(NotifyBase):
|
|||
|
||||
# Store our webhook mode
|
||||
self.mode = None \
|
||||
if not isinstance(mode, six.string_types) else mode.lower()
|
||||
if not isinstance(mode, str) else mode.lower()
|
||||
|
||||
# Set Cache Flag
|
||||
self.cache = cache
|
||||
|
|
|
@ -39,24 +39,24 @@ from ..utils import validate_regex
|
|||
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
|
||||
service_name = 'Nexmo'
|
||||
service_name = 'Vonage'
|
||||
|
||||
# The services URL
|
||||
service_url = 'https://dashboard.nexmo.com/'
|
||||
|
||||
# The default protocol
|
||||
secure_protocol = 'nexmo'
|
||||
# The default protocol (nexmo kept for backwards compatibility)
|
||||
secure_protocol = ('vonage', 'nexmo')
|
||||
|
||||
# A URL that takes you to the setup/help of the specific protocol
|
||||
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'
|
||||
|
||||
# The maximum length of the body
|
||||
|
@ -124,7 +124,7 @@ class NotifyNexmo(NotifyBase):
|
|||
},
|
||||
|
||||
# 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
|
||||
# hours. We recommend this value should be kept at its default or at
|
||||
# least 30 minutes.
|
||||
|
@ -140,15 +140,15 @@ class NotifyNexmo(NotifyBase):
|
|||
def __init__(self, apikey, secret, source, targets=None, ttl=None,
|
||||
**kwargs):
|
||||
"""
|
||||
Initialize Nexmo Object
|
||||
Initialize Vonage Object
|
||||
"""
|
||||
super(NotifyNexmo, self).__init__(**kwargs)
|
||||
super(NotifyVonage, self).__init__(**kwargs)
|
||||
|
||||
# API Key (associated with project)
|
||||
self.apikey = validate_regex(
|
||||
apikey, *self.template_tokens['apikey']['regex'])
|
||||
if not self.apikey:
|
||||
msg = 'An invalid Nexmo API Key ' \
|
||||
msg = 'An invalid Vonage API Key ' \
|
||||
'({}) was specified.'.format(apikey)
|
||||
self.logger.warning(msg)
|
||||
raise TypeError(msg)
|
||||
|
@ -157,7 +157,7 @@ class NotifyNexmo(NotifyBase):
|
|||
self.secret = validate_regex(
|
||||
secret, *self.template_tokens['secret']['regex'])
|
||||
if not self.secret:
|
||||
msg = 'An invalid Nexmo API Secret ' \
|
||||
msg = 'An invalid Vonage API Secret ' \
|
||||
'({}) was specified.'.format(secret)
|
||||
self.logger.warning(msg)
|
||||
raise TypeError(msg)
|
||||
|
@ -173,7 +173,7 @@ class NotifyNexmo(NotifyBase):
|
|||
|
||||
if self.ttl < self.template_args['ttl']['min'] or \
|
||||
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)
|
||||
self.logger.warning(msg)
|
||||
raise TypeError(msg)
|
||||
|
@ -211,7 +211,7 @@ class NotifyNexmo(NotifyBase):
|
|||
|
||||
def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs):
|
||||
"""
|
||||
Perform Nexmo Notification
|
||||
Perform Vonage Notification
|
||||
"""
|
||||
|
||||
# error tracking (used for function return)
|
||||
|
@ -250,9 +250,9 @@ class NotifyNexmo(NotifyBase):
|
|||
payload['to'] = target
|
||||
|
||||
# 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.logger.debug('Nexmo Payload: {}' .format(payload))
|
||||
self.logger.debug('Vonage Payload: {}' .format(payload))
|
||||
|
||||
# Always call throttle before any remote server i/o is made
|
||||
self.throttle()
|
||||
|
@ -269,11 +269,11 @@ class NotifyNexmo(NotifyBase):
|
|||
if r.status_code != requests.codes.ok:
|
||||
# We had a problem
|
||||
status_str = \
|
||||
NotifyNexmo.http_response_code_lookup(
|
||||
NotifyVonage.http_response_code_lookup(
|
||||
r.status_code)
|
||||
|
||||
self.logger.warning(
|
||||
'Failed to send Nexmo notification to {}: '
|
||||
'Failed to send Vonage notification to {}: '
|
||||
'{}{}error={}.'.format(
|
||||
target,
|
||||
status_str,
|
||||
|
@ -288,11 +288,12 @@ class NotifyNexmo(NotifyBase):
|
|||
continue
|
||||
|
||||
else:
|
||||
self.logger.info('Sent Nexmo notification to %s.' % target)
|
||||
self.logger.info(
|
||||
'Sent Vonage notification to %s.' % target)
|
||||
|
||||
except requests.RequestException as e:
|
||||
self.logger.warning(
|
||||
'A Connection error occurred sending Nexmo:%s '
|
||||
'A Connection error occurred sending Vonage:%s '
|
||||
'notification.' % target
|
||||
)
|
||||
self.logger.debug('Socket Exception: %s' % str(e))
|
||||
|
@ -317,14 +318,14 @@ class NotifyNexmo(NotifyBase):
|
|||
params.update(self.url_parameters(privacy=privacy, *args, **kwargs))
|
||||
|
||||
return '{schema}://{key}:{secret}@{source}/{targets}/?{params}'.format(
|
||||
schema=self.secure_protocol,
|
||||
schema=self.secure_protocol[0],
|
||||
key=self.pprint(self.apikey, privacy, safe=''),
|
||||
secret=self.pprint(
|
||||
self.secret, privacy, mode=PrivacyMode.Secret, safe=''),
|
||||
source=NotifyNexmo.quote(self.source, safe=''),
|
||||
source=NotifyVonage.quote(self.source, safe=''),
|
||||
targets='/'.join(
|
||||
[NotifyNexmo.quote(x, safe='') for x in self.targets]),
|
||||
params=NotifyNexmo.urlencode(params))
|
||||
[NotifyVonage.quote(x, safe='') for x in self.targets]),
|
||||
params=NotifyVonage.urlencode(params))
|
||||
|
||||
@staticmethod
|
||||
def parse_url(url):
|
||||
|
@ -340,46 +341,46 @@ class NotifyNexmo(NotifyBase):
|
|||
|
||||
# Get our entries; split_path() looks after unquoting content for us
|
||||
# by default
|
||||
results['targets'] = NotifyNexmo.split_path(results['fullpath'])
|
||||
results['targets'] = NotifyVonage.split_path(results['fullpath'])
|
||||
|
||||
# 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
|
||||
results['apikey'] = NotifyNexmo.unquote(results['user'])
|
||||
results['secret'] = NotifyNexmo.unquote(results['password'])
|
||||
results['apikey'] = NotifyVonage.unquote(results['user'])
|
||||
results['secret'] = NotifyVonage.unquote(results['password'])
|
||||
|
||||
# API Key
|
||||
if 'key' in results['qsd'] and len(results['qsd']['key']):
|
||||
# Extract the API Key from an argument
|
||||
results['apikey'] = \
|
||||
NotifyNexmo.unquote(results['qsd']['key'])
|
||||
NotifyVonage.unquote(results['qsd']['key'])
|
||||
|
||||
# API Secret
|
||||
if 'secret' in results['qsd'] and len(results['qsd']['secret']):
|
||||
# Extract the API Secret from an argument
|
||||
results['secret'] = \
|
||||
NotifyNexmo.unquote(results['qsd']['secret'])
|
||||
NotifyVonage.unquote(results['qsd']['secret'])
|
||||
|
||||
# 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'] = \
|
||||
NotifyNexmo.unquote(results['qsd']['from'])
|
||||
NotifyVonage.unquote(results['qsd']['from'])
|
||||
if 'source' in results['qsd'] and len(results['qsd']['source']):
|
||||
results['source'] = \
|
||||
NotifyNexmo.unquote(results['qsd']['source'])
|
||||
NotifyVonage.unquote(results['qsd']['source'])
|
||||
|
||||
# Support the 'ttl' variable
|
||||
if 'ttl' in results['qsd'] and len(results['qsd']['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
|
||||
# The 'to' makes it easier to use yaml configuration
|
||||
if 'to' in results['qsd'] and len(results['qsd']['to']):
|
||||
results['targets'] += \
|
||||
NotifyNexmo.parse_phone_no(results['qsd']['to'])
|
||||
NotifyVonage.parse_phone_no(results['qsd']['to'])
|
||||
|
||||
return results
|
|
@ -203,9 +203,9 @@ class NotifyWindows(NotifyBase):
|
|||
|
||||
self.logger.info('Sent Windows notification.')
|
||||
|
||||
except Exception:
|
||||
except Exception as e:
|
||||
self.logger.warning('Failed to send Windows notification.')
|
||||
self.logger.exception('Windows Exception')
|
||||
self.logger.debug('Windows Exception: {}', str(e))
|
||||
return False
|
||||
|
||||
return True
|
||||
|
|
|
@ -24,7 +24,6 @@
|
|||
# THE SOFTWARE.
|
||||
|
||||
import re
|
||||
import six
|
||||
import requests
|
||||
import base64
|
||||
|
||||
|
@ -157,11 +156,11 @@ class NotifyXML(NotifyBase):
|
|||
</soapenv:Envelope>"""
|
||||
|
||||
self.fullpath = kwargs.get('fullpath')
|
||||
if not isinstance(self.fullpath, six.string_types):
|
||||
if not isinstance(self.fullpath, str):
|
||||
self.fullpath = ''
|
||||
|
||||
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:
|
||||
msg = 'The method specified ({}) is invalid.'.format(method)
|
||||
|
@ -284,8 +283,7 @@ class NotifyXML(NotifyBase):
|
|||
|
||||
try:
|
||||
with open(attachment.path, 'rb') as f:
|
||||
# Output must be in a DataURL format (that's what
|
||||
# PushSafer calls it):
|
||||
# Prepare our Attachment in Base64
|
||||
entry = \
|
||||
'<Attachment filename="{}" mimetype="{}">'.format(
|
||||
NotifyXML.escape_html(
|
||||
|
@ -415,17 +413,9 @@ class NotifyXML(NotifyBase):
|
|||
for x, y in results['qsd:'].items()}
|
||||
|
||||
# Add our headers that the user can potentially over-ride if they wish
|
||||
# to to our returned result set
|
||||
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
|
||||
# to to our returned result set and tidy entries by unquoting them
|
||||
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
|
||||
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 validate_regex
|
||||
from ..utils import is_email
|
||||
from ..utils import remove_suffix
|
||||
from ..AppriseLocale import gettext_lazy as _
|
||||
|
||||
# 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
|
||||
VALIDATE_ORG = re.compile(
|
||||
|
@ -122,7 +123,7 @@ class NotifyZulip(NotifyBase):
|
|||
'botname': {
|
||||
'name': _('Bot Name'),
|
||||
'type': 'string',
|
||||
'regex': (r'^[A-Z0-9_]{1,32}(-bot)?$', 'i'),
|
||||
'regex': (r'^[A-Z0-9_-]{1,32}$', 'i'),
|
||||
},
|
||||
'organization': {
|
||||
'name': _('Organization'),
|
||||
|
@ -183,7 +184,9 @@ class NotifyZulip(NotifyBase):
|
|||
raise TypeError
|
||||
|
||||
# The botname
|
||||
self.botname = match.group('name')
|
||||
botname = match.group('name')
|
||||
botname = remove_suffix(botname, '-bot')
|
||||
self.botname = botname
|
||||
|
||||
except (TypeError, AttributeError):
|
||||
msg = 'The Zulip botname specified ({}) is invalid.'\
|
||||
|
|
|
@ -24,7 +24,6 @@
|
|||
# THE SOFTWARE.
|
||||
|
||||
import os
|
||||
import six
|
||||
import re
|
||||
import copy
|
||||
|
||||
|
@ -33,7 +32,6 @@ from os.path import abspath
|
|||
|
||||
# Used for testing
|
||||
from . import NotifyEmail as NotifyEmailBase
|
||||
from .NotifyXMPP import SliXmppAdapter
|
||||
|
||||
# NotifyBase object is passed in as a module not class
|
||||
from . import NotifyBase
|
||||
|
@ -42,6 +40,7 @@ from ..common import NotifyImageSize
|
|||
from ..common import NOTIFY_IMAGE_SIZES
|
||||
from ..common import NotifyType
|
||||
from ..common import NOTIFY_TYPES
|
||||
from .. import common
|
||||
from ..utils import parse_list
|
||||
from ..utils import cwe312_url
|
||||
from ..utils import GET_SCHEMA_RE
|
||||
|
@ -49,9 +48,6 @@ from ..logger import logger
|
|||
from ..AppriseLocale import gettext_lazy as _
|
||||
from ..AppriseLocale import LazyTranslation
|
||||
|
||||
# Maintains a mapping of all of the Notification services
|
||||
SCHEMA_MAP = {}
|
||||
|
||||
__all__ = [
|
||||
# Reference
|
||||
'NotifyImageSize', 'NOTIFY_IMAGE_SIZES', 'NotifyType', 'NOTIFY_TYPES',
|
||||
|
@ -62,15 +58,8 @@ __all__ = [
|
|||
|
||||
# Tokenizer
|
||||
'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
|
||||
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
|
||||
continue
|
||||
|
||||
elif plugin_name in MODULE_MAP:
|
||||
elif plugin_name in common.NOTIFY_MODULE_MAP:
|
||||
# we're already handling this object
|
||||
continue
|
||||
|
||||
# Add our plugin name to our module map
|
||||
MODULE_MAP[plugin_name] = {
|
||||
common.NOTIFY_MODULE_MAP[plugin_name] = {
|
||||
'plugin': plugin,
|
||||
'module': module,
|
||||
}
|
||||
|
@ -130,40 +119,20 @@ def __load_matrix(path=abspath(dirname(__file__)), name='apprise.plugins'):
|
|||
globals()[plugin_name] = plugin
|
||||
|
||||
fn = getattr(plugin, 'schemas', None)
|
||||
try:
|
||||
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)
|
||||
schemas = set([]) if not callable(fn) else fn(plugin)
|
||||
|
||||
# map our schema to our plugin
|
||||
for schema in schemas:
|
||||
if schema in SCHEMA_MAP:
|
||||
if schema in common.NOTIFY_SCHEMA_MAP:
|
||||
logger.error(
|
||||
"Notification schema ({}) mismatch detected - {} to {}"
|
||||
.format(schema, SCHEMA_MAP[schema], plugin))
|
||||
.format(schema, common.NOTIFY_SCHEMA_MAP[schema], plugin))
|
||||
continue
|
||||
|
||||
# Assign plugin
|
||||
SCHEMA_MAP[schema] = plugin
|
||||
common.NOTIFY_SCHEMA_MAP[schema] = plugin
|
||||
|
||||
return SCHEMA_MAP
|
||||
return common.NOTIFY_SCHEMA_MAP
|
||||
|
||||
|
||||
# Reset our Lookup Matrix
|
||||
|
@ -174,10 +143,10 @@ def __reset_matrix():
|
|||
"""
|
||||
|
||||
# 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
|
||||
for plugin_name in MODULE_MAP.keys():
|
||||
for plugin_name in common.NOTIFY_MODULE_MAP.keys():
|
||||
# Clear out globals
|
||||
del globals()[plugin_name]
|
||||
|
||||
|
@ -185,7 +154,7 @@ def __reset_matrix():
|
|||
__all__.remove(plugin_name)
|
||||
|
||||
# Clear out our module map
|
||||
MODULE_MAP.clear()
|
||||
common.NOTIFY_MODULE_MAP.clear()
|
||||
|
||||
|
||||
# Dynamically build our schema base
|
||||
|
@ -242,7 +211,7 @@ def _sanitize_token(tokens, default_delimiter):
|
|||
|
||||
if 'regex' in tokens[key]:
|
||||
# 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
|
||||
tokens[key]['regex'] = \
|
||||
(tokens[key]['regex'], None)
|
||||
|
@ -483,7 +452,7 @@ def requirements(plugin):
|
|||
|
||||
# Get our required packages
|
||||
_req_packages = plugin.requirements.get('packages_required')
|
||||
if isinstance(_req_packages, six.string_types):
|
||||
if isinstance(_req_packages, str):
|
||||
# Convert to list
|
||||
_req_packages = [_req_packages]
|
||||
|
||||
|
@ -495,7 +464,7 @@ def requirements(plugin):
|
|||
|
||||
# Get our recommended packages
|
||||
_opt_packages = plugin.requirements.get('packages_recommended')
|
||||
if isinstance(_opt_packages, six.string_types):
|
||||
if isinstance(_opt_packages, str):
|
||||
# Convert to list
|
||||
_opt_packages = [_opt_packages]
|
||||
|
||||
|
@ -554,14 +523,14 @@ def url_to_dict(url, secure_logging=True):
|
|||
|
||||
# Ensure our schema is always in lower case
|
||||
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
|
||||
# one of the URLs provided to them by their notification service.
|
||||
# Before we fail for good, just scan all the plugins that support the
|
||||
# native_url() parse function
|
||||
results = \
|
||||
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),
|
||||
None)
|
||||
|
||||
|
@ -576,14 +545,14 @@ def url_to_dict(url, secure_logging=True):
|
|||
else:
|
||||
# Parse our url details of the server object as dictionary
|
||||
# containing all of the information parsed from our URL
|
||||
results = SCHEMA_MAP[schema].parse_url(_url)
|
||||
results = common.NOTIFY_SCHEMA_MAP[schema].parse_url(_url)
|
||||
if not results:
|
||||
logger.error('Unparseable {} URL {}'.format(
|
||||
SCHEMA_MAP[schema].service_name, loggable_url))
|
||||
common.NOTIFY_SCHEMA_MAP[schema].service_name, loggable_url))
|
||||
return None
|
||||
|
||||
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(
|
||||
['{}="{}"'.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)
|
||||
|
||||
|
||||
# async reference produces a SyntaxError (E999) in Python v2.7
|
||||
# For this reason we turn on the noqa flag
|
||||
async def notify(coroutines): # noqa: E999
|
||||
async def notify(coroutines):
|
||||
"""
|
||||
An async wrapper to the AsyncNotifyBase.async_notify() calls allowing us
|
||||
to call gather() and collect the responses
|
||||
|
@ -63,7 +61,20 @@ def tosync(cor, debug=False):
|
|||
"""
|
||||
|
||||
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:
|
||||
# The Deprecated Way (<= Python v3.6)
|
||||
|
@ -85,7 +96,7 @@ def tosync(cor, debug=False):
|
|||
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.
|
||||
"""
|
||||
|
@ -93,12 +104,20 @@ async def toasyncwrap(v): # noqa: E999
|
|||
return v
|
||||
|
||||
|
||||
async def toasyncwrap(fn):
|
||||
"""
|
||||
Create a coroutine that, when run, executes the provided function.
|
||||
"""
|
||||
|
||||
return fn()
|
||||
|
||||
|
||||
class AsyncNotifyBase(URLBase):
|
||||
"""
|
||||
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
|
||||
"""
|
||||
|
@ -110,11 +129,11 @@ class AsyncNotifyBase(URLBase):
|
|||
None, partial(self.notify, *args, **kwargs))
|
||||
|
||||
except TypeError:
|
||||
# These our our internally thrown notifications
|
||||
# These are our internally thrown notifications
|
||||
pass
|
||||
|
||||
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.
|
||||
logger.exception("Notification Exception")
|
||||
|
||||
|
|
|
@ -24,27 +24,55 @@
|
|||
# THE SOFTWARE.
|
||||
|
||||
import re
|
||||
import six
|
||||
import sys
|
||||
import json
|
||||
import contextlib
|
||||
import os
|
||||
import hashlib
|
||||
from itertools import chain
|
||||
from os.path import expanduser
|
||||
from functools import reduce
|
||||
from .common import MATCH_ALL_TAG
|
||||
from .common import MATCH_ALWAYS_TAG
|
||||
from . import common
|
||||
from .logger import logger
|
||||
|
||||
try:
|
||||
# Python 2.7
|
||||
from urllib import unquote
|
||||
from urllib import quote
|
||||
from urlparse import urlparse
|
||||
from urllib.parse import unquote
|
||||
from urllib.parse import quote
|
||||
from urllib.parse import urlparse
|
||||
from urllib.parse import urlencode as _urlencode
|
||||
|
||||
except ImportError:
|
||||
# Python 3.x
|
||||
from urllib.parse import unquote
|
||||
from urllib.parse import quote
|
||||
from urllib.parse import urlparse
|
||||
import importlib.util
|
||||
|
||||
|
||||
def import_module(path, name):
|
||||
"""
|
||||
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()
|
||||
# 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.
|
||||
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:
|
||||
# http://www.regular-expressions.info/email.html
|
||||
# Extended to support colon (:) delimiter for parsing names from the URL
|
||||
|
@ -167,7 +202,7 @@ UUID4_RE = re.compile(
|
|||
REGEX_VALIDATE_LOOKUP = {}
|
||||
|
||||
|
||||
class TemplateType(object):
|
||||
class TemplateType:
|
||||
"""
|
||||
Defines the different template types we can perform parsing on
|
||||
"""
|
||||
|
@ -294,7 +329,7 @@ def is_uuid(uuid):
|
|||
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
|
||||
|
||||
Args:
|
||||
|
@ -477,16 +512,14 @@ def tidy_path(path):
|
|||
# Windows
|
||||
path = TIDY_WIN_PATH_RE.sub('\\1', path.strip())
|
||||
# Linux
|
||||
path = TIDY_NUX_PATH_RE.sub('\\1', path.strip())
|
||||
path = TIDY_NUX_PATH_RE.sub('\\1', path)
|
||||
|
||||
# Linux Based Trim
|
||||
path = TIDY_NUX_TRIM_RE.sub('\\1', path.strip())
|
||||
# Windows Based Trim
|
||||
path = expanduser(TIDY_WIN_TRIM_RE.sub('\\1', path.strip()))
|
||||
# Windows Based (final) Trim
|
||||
path = expanduser(TIDY_WIN_TRIM_RE.sub('\\1', path))
|
||||
return path
|
||||
|
||||
|
||||
def parse_qsd(qs):
|
||||
def parse_qsd(qs, simple=False):
|
||||
"""
|
||||
Query String Dictionary Builder
|
||||
|
||||
|
@ -505,6 +538,9 @@ def parse_qsd(qs):
|
|||
|
||||
This function returns a result object that fits with the apprise
|
||||
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:
|
||||
|
@ -520,7 +556,7 @@ def parse_qsd(qs):
|
|||
'qsd+': {},
|
||||
'qsd-': {},
|
||||
'qsd:': {},
|
||||
}
|
||||
} if not simple else {'qsd': {}}
|
||||
|
||||
pairs = [s2 for s1 in qs.split('&') for s2 in s1.split(';')]
|
||||
for name_value in pairs:
|
||||
|
@ -547,6 +583,10 @@ def parse_qsd(qs):
|
|||
# content is always made lowercase for easy indexing
|
||||
result['qsd'][key.lower().strip()] = val
|
||||
|
||||
if simple:
|
||||
# move along
|
||||
continue
|
||||
|
||||
# Check for tokens that start with a addition/plus symbol (+)
|
||||
k = NOTIFY_CUSTOM_ADD_TOKENS.match(key)
|
||||
if k is not None:
|
||||
|
@ -568,7 +608,8 @@ def parse_qsd(qs):
|
|||
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
|
||||
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 parsed content within it and returns 'None' if the
|
||||
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
|
||||
return None
|
||||
|
||||
|
@ -628,7 +702,7 @@ def parse_url(url, default_schema='http', verify_host=True, strict_port=False):
|
|||
'qsd+': {},
|
||||
'qsd-': {},
|
||||
'qsd:': {},
|
||||
}
|
||||
} if not simple else {}
|
||||
|
||||
qsdata = ''
|
||||
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
|
||||
# while ensuring that all keys are lowercase
|
||||
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
|
||||
# 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
|
||||
|
||||
if not result['fullpath']:
|
||||
# Default
|
||||
result['fullpath'] = None
|
||||
if not simple:
|
||||
# Default
|
||||
result['fullpath'] = None
|
||||
else:
|
||||
# Remove entry
|
||||
del result['fullpath']
|
||||
|
||||
else:
|
||||
# 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['query'] = match.group('query')
|
||||
if not result['query']:
|
||||
result['query'] = None
|
||||
if not simple:
|
||||
result['query'] = None
|
||||
else:
|
||||
del result['query']
|
||||
|
||||
try:
|
||||
(result['user'], result['host']) = \
|
||||
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
|
||||
pass
|
||||
|
||||
if result['user'] is not None:
|
||||
if result.get('user') is not None:
|
||||
try:
|
||||
(result['user'], result['password']) = \
|
||||
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
|
||||
return None
|
||||
|
||||
# Acquire our port (if defined)
|
||||
_port = result.get('port')
|
||||
|
||||
if verify_host:
|
||||
# Verify and Validate our hostname
|
||||
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
|
||||
|
||||
# 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 (
|
||||
strict_port and
|
||||
result['port'] > 0 and result['port'] <= 65535))):
|
||||
strict_port and _port > 0 and _port <= 65535))):
|
||||
|
||||
# An invalid port was specified
|
||||
return None
|
||||
|
||||
elif pmatch and not isinstance(result['port'], int):
|
||||
elif pmatch and not isinstance(_port, int):
|
||||
if strict_port:
|
||||
# Store port
|
||||
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
|
||||
result['url'] = '%s://' % result['schema']
|
||||
if isinstance(result['user'], six.string_types):
|
||||
if isinstance(result.get('user'), str):
|
||||
result['url'] += result['user']
|
||||
|
||||
if isinstance(result['password'], six.string_types):
|
||||
if isinstance(result.get('password'), str):
|
||||
result['url'] += ':%s@' % result['password']
|
||||
|
||||
else:
|
||||
result['url'] += '@'
|
||||
result['url'] += result['host']
|
||||
|
||||
if result['port'] is not None:
|
||||
if result.get('port') is not None:
|
||||
try:
|
||||
result['url'] += ':%d' % result['port']
|
||||
|
||||
except TypeError:
|
||||
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']
|
||||
|
||||
if simple and not result['host']:
|
||||
# simple mode does not carry over empty host names
|
||||
del result['host']
|
||||
|
||||
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 isinstance(arg, six.string_types):
|
||||
if isinstance(arg, str):
|
||||
# no = no - False
|
||||
# of = short for off - False
|
||||
# 0 = int for False
|
||||
|
@ -814,20 +906,15 @@ def parse_bool(arg, default=False):
|
|||
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
|
||||
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 = []
|
||||
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)
|
||||
if _result:
|
||||
result += _result
|
||||
|
@ -851,20 +938,15 @@ def parse_phone_no(*args, **kwargs):
|
|||
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
|
||||
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 = []
|
||||
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)
|
||||
if _result:
|
||||
result += _result
|
||||
|
@ -888,20 +970,15 @@ def parse_call_sign(*args, **kwargs):
|
|||
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
|
||||
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 = []
|
||||
for arg in args:
|
||||
if isinstance(arg, six.string_types) and arg:
|
||||
if isinstance(arg, str) and arg:
|
||||
_result = EMAIL_DETECTION_RE.findall(arg)
|
||||
if _result:
|
||||
result += _result
|
||||
|
@ -924,20 +1001,15 @@ def parse_emails(*args, **kwargs):
|
|||
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
|
||||
returns a list.
|
||||
"""
|
||||
|
||||
# for Python 2.7 support, store_unparsable is not in the url above
|
||||
# as just parse_urls(*args, store_unparseable=True) since it is
|
||||
# an invalid syntax. This is the workaround to be backards compatible:
|
||||
store_unparseable = kwargs.get('store_unparseable', True)
|
||||
|
||||
result = []
|
||||
for arg in args:
|
||||
if isinstance(arg, six.string_types) and arg:
|
||||
if isinstance(arg, str) and arg:
|
||||
_result = URL_DETECTION_RE.findall(arg)
|
||||
if _result:
|
||||
result += _result
|
||||
|
@ -960,6 +1032,75 @@ def parse_urls(*args, **kwargs):
|
|||
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):
|
||||
"""
|
||||
Take a string list and break it into a delimited
|
||||
|
@ -983,7 +1124,7 @@ def parse_list(*args):
|
|||
|
||||
result = []
|
||||
for arg in args:
|
||||
if isinstance(arg, six.string_types):
|
||||
if isinstance(arg, str):
|
||||
result += re.split(STRING_DELIMITERS, arg)
|
||||
|
||||
elif isinstance(arg, (set, list, tuple)):
|
||||
|
@ -992,14 +1133,15 @@ def parse_list(*args):
|
|||
#
|
||||
# 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
|
||||
# both distribution types.
|
||||
# TODO: Review after dropping support for Python 2.
|
||||
return sorted([x for x in filter(bool, list(set(result)))])
|
||||
|
||||
|
||||
def is_exclusive_match(logic, data, match_all=MATCH_ALL_TAG,
|
||||
match_always=MATCH_ALWAYS_TAG):
|
||||
def is_exclusive_match(logic, data, match_all=common.MATCH_ALL_TAG,
|
||||
match_always=common.MATCH_ALWAYS_TAG):
|
||||
"""
|
||||
|
||||
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.
|
||||
"""
|
||||
|
||||
if isinstance(logic, six.string_types):
|
||||
if isinstance(logic, str):
|
||||
# Update our logic to support our delimiters
|
||||
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
|
||||
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
|
||||
return False
|
||||
|
||||
|
@ -1109,7 +1251,7 @@ def validate_regex(value, regex=r'[^\s]+', flags=re.I, strip=True, fmt=None):
|
|||
'x': re.X,
|
||||
}
|
||||
|
||||
if isinstance(flags, six.string_types):
|
||||
if isinstance(flags, str):
|
||||
# Convert a string of regular expression flags into their
|
||||
# respected integer (expected) Python values and perform
|
||||
# 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
|
||||
"""
|
||||
|
||||
class Variance(object):
|
||||
class Variance:
|
||||
"""
|
||||
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)
|
||||
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
|
||||
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
|
||||
# re-index it back into our list
|
||||
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
|
||||
aniso8601==9.0.1
|
||||
argparse==1.4.0
|
||||
apprise==0.9.8.3
|
||||
apprise==1.1.0
|
||||
apscheduler==3.8.1
|
||||
attrs==22.1.0
|
||||
charamel==1.0.0
|
||||
|
|
Loading…
Reference in a new issue