mirror of
https://github.com/SickGear/SickGear.git
synced 2024-12-22 10:43:38 +00:00
Merge branch 'feature/UpdateApprise' into dev
This commit is contained in:
commit
f69e31b295
129 changed files with 5607 additions and 2321 deletions
|
@ -1,5 +1,6 @@
|
|||
### 3.31.0 (2023-1x-xx xx:xx:00 UTC)
|
||||
|
||||
* Update Apprise 1.3.0 (6458ab0) to 1.6.0 (0c0d5da)
|
||||
* Update attr 22.2.0 (683d056) to 23.1.0 (67e4ff2)
|
||||
* Update Beautiful Soup 4.12.2 to 4.12.2 (30c58a1)
|
||||
* Update diskcache 5.6.1 (4d30686) to 5.6.3 (323787f)
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# BSD 3-Clause License
|
||||
# BSD 2-Clause License
|
||||
#
|
||||
# Apprise - Push Notification Library.
|
||||
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
|
||||
|
@ -14,10 +14,6 @@
|
|||
# this list of conditions and the following disclaimer in the documentation
|
||||
# and/or other materials provided with the distribution.
|
||||
#
|
||||
# 3. Neither the name of the copyright holder nor the names of its
|
||||
# contributors may be used to endorse or promote products derived from
|
||||
# this software without specific prior written permission.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
|
@ -31,8 +27,8 @@
|
|||
# POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
import asyncio
|
||||
import concurrent.futures as cf
|
||||
import os
|
||||
from functools import partial
|
||||
from itertools import chain
|
||||
from . import common
|
||||
from .conversion import convert_between
|
||||
|
@ -376,7 +372,7 @@ class Apprise:
|
|||
try:
|
||||
# Process arguments and build synchronous and asynchronous calls
|
||||
# (this step can throw internal errors).
|
||||
sync_partials, async_cors = self._create_notify_calls(
|
||||
sequential_calls, parallel_calls = self._create_notify_calls(
|
||||
body, title,
|
||||
notify_type=notify_type, body_format=body_format,
|
||||
tag=tag, match_always=match_always, attach=attach,
|
||||
|
@ -387,49 +383,13 @@ class Apprise:
|
|||
# No notifications sent, and there was an internal error.
|
||||
return False
|
||||
|
||||
if not sync_partials and not async_cors:
|
||||
if not sequential_calls and not parallel_calls:
|
||||
# Nothing to send
|
||||
return None
|
||||
|
||||
sync_result = Apprise._notify_all(*sync_partials)
|
||||
|
||||
if async_cors:
|
||||
# A single coroutine sends all asynchronous notifications in
|
||||
# parallel.
|
||||
all_cor = Apprise._async_notify_all(*async_cors)
|
||||
|
||||
try:
|
||||
# Python <3.7 automatically starts an event loop if there isn't
|
||||
# already one for the main thread.
|
||||
loop = asyncio.get_event_loop()
|
||||
|
||||
except RuntimeError:
|
||||
# Python >=3.7 raises this exception if there isn't already an
|
||||
# event loop. So, we can spin up our own.
|
||||
loop = asyncio.new_event_loop()
|
||||
asyncio.set_event_loop(loop)
|
||||
loop.set_debug(self.debug)
|
||||
|
||||
# Run the coroutine and wait for the result.
|
||||
async_result = loop.run_until_complete(all_cor)
|
||||
|
||||
# Clean up the loop.
|
||||
loop.close()
|
||||
asyncio.set_event_loop(None)
|
||||
|
||||
else:
|
||||
old_debug = loop.get_debug()
|
||||
loop.set_debug(self.debug)
|
||||
|
||||
# Run the coroutine and wait for the result.
|
||||
async_result = loop.run_until_complete(all_cor)
|
||||
|
||||
loop.set_debug(old_debug)
|
||||
|
||||
else:
|
||||
async_result = True
|
||||
|
||||
return sync_result and async_result
|
||||
sequential_result = Apprise._notify_sequential(*sequential_calls)
|
||||
parallel_result = Apprise._notify_parallel_threadpool(*parallel_calls)
|
||||
return sequential_result and parallel_result
|
||||
|
||||
async def async_notify(self, *args, **kwargs):
|
||||
"""
|
||||
|
@ -442,41 +402,42 @@ class Apprise:
|
|||
try:
|
||||
# Process arguments and build synchronous and asynchronous calls
|
||||
# (this step can throw internal errors).
|
||||
sync_partials, async_cors = self._create_notify_calls(
|
||||
sequential_calls, parallel_calls = self._create_notify_calls(
|
||||
*args, **kwargs)
|
||||
|
||||
except TypeError:
|
||||
# No notifications sent, and there was an internal error.
|
||||
return False
|
||||
|
||||
if not sync_partials and not async_cors:
|
||||
if not sequential_calls and not parallel_calls:
|
||||
# Nothing to send
|
||||
return None
|
||||
|
||||
sync_result = Apprise._notify_all(*sync_partials)
|
||||
async_result = await Apprise._async_notify_all(*async_cors)
|
||||
return sync_result and async_result
|
||||
sequential_result = Apprise._notify_sequential(*sequential_calls)
|
||||
parallel_result = \
|
||||
await Apprise._notify_parallel_asyncio(*parallel_calls)
|
||||
return sequential_result and parallel_result
|
||||
|
||||
def _create_notify_calls(self, *args, **kwargs):
|
||||
"""
|
||||
Creates notifications for all the plugins loaded.
|
||||
|
||||
Returns a list of synchronous calls (partial functions with no
|
||||
arguments required) for plugins with async disabled and a list of
|
||||
asynchronous calls (coroutines) for plugins with async enabled.
|
||||
Returns a list of (server, notify() kwargs) tuples for plugins with
|
||||
parallelism disabled and another list for plugins with parallelism
|
||||
enabled.
|
||||
"""
|
||||
|
||||
all_calls = list(self._create_notify_gen(*args, **kwargs))
|
||||
|
||||
# Split into synchronous partials and asynchronous coroutines.
|
||||
sync_partials, async_cors = [], []
|
||||
for notify in all_calls:
|
||||
if asyncio.iscoroutine(notify):
|
||||
async_cors.append(notify)
|
||||
# Split into sequential and parallel notify() calls.
|
||||
sequential, parallel = [], []
|
||||
for (server, notify_kwargs) in all_calls:
|
||||
if server.asset.async_mode:
|
||||
parallel.append((server, notify_kwargs))
|
||||
else:
|
||||
sync_partials.append(notify)
|
||||
sequential.append((server, notify_kwargs))
|
||||
|
||||
return sync_partials, async_cors
|
||||
return sequential, parallel
|
||||
|
||||
def _create_notify_gen(self, body, title='',
|
||||
notify_type=common.NotifyType.INFO,
|
||||
|
@ -493,7 +454,7 @@ class Apprise:
|
|||
logger.error(msg)
|
||||
raise TypeError(msg)
|
||||
|
||||
if not (title or body):
|
||||
if not (title or body or attach):
|
||||
msg = "No message content specified to deliver"
|
||||
logger.error(msg)
|
||||
raise TypeError(msg)
|
||||
|
@ -533,25 +494,29 @@ class Apprise:
|
|||
# If our code reaches here, we either did not define a tag (it
|
||||
# was set to None), or we did define a tag and the logic above
|
||||
# determined we need to notify the service it's associated with
|
||||
if server.notify_format not in conversion_body_map:
|
||||
# Perform Conversion
|
||||
conversion_body_map[server.notify_format] = \
|
||||
convert_between(
|
||||
body_format, server.notify_format, content=body)
|
||||
|
||||
# First we need to generate a key we will use to determine if we
|
||||
# need to build our data out. Entries without are merged with
|
||||
# the body at this stage.
|
||||
key = server.notify_format if server.title_maxlen > 0\
|
||||
else f'_{server.notify_format}'
|
||||
|
||||
if key not in conversion_title_map:
|
||||
|
||||
# Prepare our title
|
||||
conversion_title_map[server.notify_format] = \
|
||||
'' if not title else title
|
||||
conversion_title_map[key] = '' if not title else title
|
||||
|
||||
# Tidy Title IF required (hence it will become part of the
|
||||
# body)
|
||||
if server.title_maxlen <= 0 and \
|
||||
conversion_title_map[server.notify_format]:
|
||||
# Conversion of title only occurs for services where the title
|
||||
# is blended with the body (title_maxlen <= 0)
|
||||
if conversion_title_map[key] and server.title_maxlen <= 0:
|
||||
conversion_title_map[key] = convert_between(
|
||||
body_format, server.notify_format,
|
||||
content=conversion_title_map[key])
|
||||
|
||||
conversion_title_map[server.notify_format] = \
|
||||
convert_between(
|
||||
body_format, server.notify_format,
|
||||
content=conversion_title_map[server.notify_format])
|
||||
# Our body is always converted no matter what
|
||||
conversion_body_map[key] = \
|
||||
convert_between(
|
||||
body_format, server.notify_format, content=body)
|
||||
|
||||
if interpret_escapes:
|
||||
#
|
||||
|
@ -561,13 +526,13 @@ class Apprise:
|
|||
try:
|
||||
# Added overhead required due to Python 3 Encoding Bug
|
||||
# identified here: https://bugs.python.org/issue21331
|
||||
conversion_body_map[server.notify_format] = \
|
||||
conversion_body_map[server.notify_format]\
|
||||
conversion_body_map[key] = \
|
||||
conversion_body_map[key]\
|
||||
.encode('ascii', 'backslashreplace')\
|
||||
.decode('unicode-escape')
|
||||
|
||||
conversion_title_map[server.notify_format] = \
|
||||
conversion_title_map[server.notify_format]\
|
||||
conversion_title_map[key] = \
|
||||
conversion_title_map[key]\
|
||||
.encode('ascii', 'backslashreplace')\
|
||||
.decode('unicode-escape')
|
||||
|
||||
|
@ -578,29 +543,26 @@ class Apprise:
|
|||
raise TypeError(msg)
|
||||
|
||||
kwargs = dict(
|
||||
body=conversion_body_map[server.notify_format],
|
||||
title=conversion_title_map[server.notify_format],
|
||||
body=conversion_body_map[key],
|
||||
title=conversion_title_map[key],
|
||||
notify_type=notify_type,
|
||||
attach=attach,
|
||||
body_format=body_format
|
||||
)
|
||||
if server.asset.async_mode:
|
||||
yield server.async_notify(**kwargs)
|
||||
else:
|
||||
yield partial(server.notify, **kwargs)
|
||||
yield (server, kwargs)
|
||||
|
||||
@staticmethod
|
||||
def _notify_all(*partials):
|
||||
def _notify_sequential(*servers_kwargs):
|
||||
"""
|
||||
Process a list of synchronous notify() calls.
|
||||
Process a list of notify() calls sequentially and synchronously.
|
||||
"""
|
||||
|
||||
success = True
|
||||
|
||||
for notify in partials:
|
||||
for (server, kwargs) in servers_kwargs:
|
||||
try:
|
||||
# Send notification
|
||||
result = notify()
|
||||
result = server.notify(**kwargs)
|
||||
success = success and result
|
||||
|
||||
except TypeError:
|
||||
|
@ -616,14 +578,71 @@ class Apprise:
|
|||
return success
|
||||
|
||||
@staticmethod
|
||||
async def _async_notify_all(*cors):
|
||||
def _notify_parallel_threadpool(*servers_kwargs):
|
||||
"""
|
||||
Process a list of asynchronous async_notify() calls.
|
||||
Process a list of notify() calls in parallel and synchronously.
|
||||
"""
|
||||
|
||||
n_calls = len(servers_kwargs)
|
||||
|
||||
# 0-length case
|
||||
if n_calls == 0:
|
||||
return True
|
||||
|
||||
# There's no need to use a thread pool for just a single notification
|
||||
if n_calls == 1:
|
||||
return Apprise._notify_sequential(servers_kwargs[0])
|
||||
|
||||
# Create log entry
|
||||
logger.info('Notifying %d service(s) asynchronously.', len(cors))
|
||||
logger.info(
|
||||
'Notifying %d service(s) with threads.', len(servers_kwargs))
|
||||
|
||||
with cf.ThreadPoolExecutor() as executor:
|
||||
success = True
|
||||
futures = [executor.submit(server.notify, **kwargs)
|
||||
for (server, kwargs) in servers_kwargs]
|
||||
|
||||
for future in cf.as_completed(futures):
|
||||
try:
|
||||
result = future.result()
|
||||
success = success and result
|
||||
|
||||
except TypeError:
|
||||
# These are our internally thrown notifications.
|
||||
success = False
|
||||
|
||||
except Exception:
|
||||
# A catch all so we don't have to abort early
|
||||
# just because one of our plugins has a bug in it.
|
||||
logger.exception("Unhandled Notification Exception")
|
||||
success = False
|
||||
|
||||
return success
|
||||
|
||||
@staticmethod
|
||||
async def _notify_parallel_asyncio(*servers_kwargs):
|
||||
"""
|
||||
Process a list of async_notify() calls in parallel and asynchronously.
|
||||
"""
|
||||
|
||||
n_calls = len(servers_kwargs)
|
||||
|
||||
# 0-length case
|
||||
if n_calls == 0:
|
||||
return True
|
||||
|
||||
# (Unlike with the thread pool, we don't optimize for the single-
|
||||
# notification case because asyncio can do useful work while waiting
|
||||
# for that thread to complete)
|
||||
|
||||
# Create log entry
|
||||
logger.info(
|
||||
'Notifying %d service(s) asynchronously.', len(servers_kwargs))
|
||||
|
||||
async def do_call(server, kwargs):
|
||||
return await server.async_notify(**kwargs)
|
||||
|
||||
cors = (do_call(server, kwargs) for (server, kwargs) in servers_kwargs)
|
||||
results = await asyncio.gather(*cors, return_exceptions=True)
|
||||
|
||||
if any(isinstance(status, Exception)
|
||||
|
@ -665,6 +684,12 @@ class Apprise:
|
|||
'setup_url': getattr(plugin, 'setup_url', None),
|
||||
# Placeholder - populated below
|
||||
'details': None,
|
||||
|
||||
# Let upstream service know of the plugins that support
|
||||
# attachments
|
||||
'attachment_support': getattr(
|
||||
plugin, 'attachment_support', False),
|
||||
|
||||
# Differentiat between what is a custom loaded plugin and
|
||||
# which is native.
|
||||
'category': getattr(plugin, 'category', None)
|
||||
|
@ -790,6 +815,36 @@ class Apprise:
|
|||
# If we reach here, then we indexed out of range
|
||||
raise IndexError('list index out of range')
|
||||
|
||||
def __getstate__(self):
|
||||
"""
|
||||
Pickle Support dumps()
|
||||
"""
|
||||
attributes = {
|
||||
'asset': self.asset,
|
||||
# Prepare our URL list as we need to extract the associated tags
|
||||
# and asset details associated with it
|
||||
'urls': [{
|
||||
'url': server.url(privacy=False),
|
||||
'tag': server.tags if server.tags else None,
|
||||
'asset': server.asset} for server in self.servers],
|
||||
'locale': self.locale,
|
||||
'debug': self.debug,
|
||||
'location': self.location,
|
||||
}
|
||||
|
||||
return attributes
|
||||
|
||||
def __setstate__(self, state):
|
||||
"""
|
||||
Pickle Support loads()
|
||||
"""
|
||||
self.servers = list()
|
||||
self.asset = state['asset']
|
||||
self.locale = state['locale']
|
||||
self.location = state['location']
|
||||
for entry in state['urls']:
|
||||
self.add(entry['url'], asset=entry['asset'], tag=entry['tag'])
|
||||
|
||||
def __bool__(self):
|
||||
"""
|
||||
Allows the Apprise object to be wrapped in an 'if statement'.
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# BSD 3-Clause License
|
||||
# BSD 2-Clause License
|
||||
#
|
||||
# Apprise - Push Notification Library.
|
||||
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
|
||||
|
@ -14,10 +14,6 @@
|
|||
# this list of conditions and the following disclaimer in the documentation
|
||||
# and/or other materials provided with the distribution.
|
||||
#
|
||||
# 3. Neither the name of the copyright holder nor the names of its
|
||||
# contributors may be used to endorse or promote products derived from
|
||||
# this software without specific prior written permission.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# BSD 3-Clause License
|
||||
# BSD 2-Clause License
|
||||
#
|
||||
# Apprise - Push Notification Library.
|
||||
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
|
||||
|
@ -14,10 +14,6 @@
|
|||
# this list of conditions and the following disclaimer in the documentation
|
||||
# and/or other materials provided with the distribution.
|
||||
#
|
||||
# 3. Neither the name of the copyright holder nor the names of its
|
||||
# contributors may be used to endorse or promote products derived from
|
||||
# this software without specific prior written permission.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# BSD 3-Clause License
|
||||
# BSD 2-Clause License
|
||||
#
|
||||
# Apprise - Push Notification Library.
|
||||
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
|
||||
|
@ -14,10 +14,6 @@
|
|||
# this list of conditions and the following disclaimer in the documentation
|
||||
# and/or other materials provided with the distribution.
|
||||
#
|
||||
# 3. Neither the name of the copyright holder nor the names of its
|
||||
# contributors may be used to endorse or promote products derived from
|
||||
# this software without specific prior written permission.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# BSD 3-Clause License
|
||||
# BSD 2-Clause License
|
||||
#
|
||||
# Apprise - Push Notification Library.
|
||||
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
|
||||
|
@ -14,10 +14,6 @@
|
|||
# this list of conditions and the following disclaimer in the documentation
|
||||
# and/or other materials provided with the distribution.
|
||||
#
|
||||
# 3. Neither the name of the copyright holder nor the names of its
|
||||
# contributors may be used to endorse or promote products derived from
|
||||
# this software without specific prior written permission.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
|
@ -33,14 +29,13 @@
|
|||
import ctypes
|
||||
import locale
|
||||
import contextlib
|
||||
import os
|
||||
import re
|
||||
from os.path import join
|
||||
from os.path import dirname
|
||||
from os.path import abspath
|
||||
from .logger import logger
|
||||
|
||||
# Define our translation domain
|
||||
DOMAIN = 'apprise'
|
||||
LOCALE_DIR = abspath(join(dirname(__file__), 'i18n'))
|
||||
|
||||
# This gets toggled to True if we succeed
|
||||
GETTEXT_LOADED = False
|
||||
|
@ -49,17 +44,220 @@ try:
|
|||
# Initialize gettext
|
||||
import gettext
|
||||
|
||||
# install() creates a _() in our builtins
|
||||
gettext.install(DOMAIN, localedir=LOCALE_DIR)
|
||||
|
||||
# Toggle our flag
|
||||
GETTEXT_LOADED = True
|
||||
|
||||
except ImportError:
|
||||
# gettext isn't available; no problem, just fall back to using
|
||||
# the library features without multi-language support.
|
||||
import builtins
|
||||
builtins.__dict__['_'] = lambda x: x # pragma: no branch
|
||||
# gettext isn't available; no problem; Use the library features without
|
||||
# multi-language support.
|
||||
pass
|
||||
|
||||
|
||||
class AppriseLocale:
|
||||
"""
|
||||
A wrapper class to gettext so that we can manipulate multiple lanaguages
|
||||
on the fly if required.
|
||||
|
||||
"""
|
||||
|
||||
# Define our translation domain
|
||||
_domain = 'apprise'
|
||||
|
||||
# The path to our translations
|
||||
_locale_dir = abspath(join(dirname(__file__), 'i18n'))
|
||||
|
||||
# Locale regular expression
|
||||
_local_re = re.compile(
|
||||
r'^((?P<ansii>C)|(?P<lang>([a-z]{2}))([_:](?P<country>[a-z]{2}))?)'
|
||||
r'(\.(?P<enc>[a-z0-9-]+))?$', re.IGNORECASE)
|
||||
|
||||
# Define our default encoding
|
||||
_default_encoding = 'utf-8'
|
||||
|
||||
# The function to assign `_` by default
|
||||
_fn = 'gettext'
|
||||
|
||||
# The language we should fall back to if all else fails
|
||||
_default_language = 'en'
|
||||
|
||||
def __init__(self, language=None):
|
||||
"""
|
||||
Initializes our object, if a language is specified, then we
|
||||
initialize ourselves to that, otherwise we use whatever we detect
|
||||
from the local operating system. If all else fails, we resort to the
|
||||
defined default_language.
|
||||
|
||||
"""
|
||||
|
||||
# Cache previously loaded translations
|
||||
self._gtobjs = {}
|
||||
|
||||
# Get our language
|
||||
self.lang = AppriseLocale.detect_language(language)
|
||||
|
||||
# Our mapping to our _fn
|
||||
self.__fn_map = None
|
||||
|
||||
if GETTEXT_LOADED is False:
|
||||
# We're done
|
||||
return
|
||||
|
||||
# Add language
|
||||
self.add(self.lang)
|
||||
|
||||
def add(self, lang=None, set_default=True):
|
||||
"""
|
||||
Add a language to our list
|
||||
"""
|
||||
lang = lang if lang else self._default_language
|
||||
if lang not in self._gtobjs:
|
||||
# Load our gettext object and install our language
|
||||
try:
|
||||
self._gtobjs[lang] = gettext.translation(
|
||||
self._domain, localedir=self._locale_dir, languages=[lang],
|
||||
fallback=False)
|
||||
|
||||
# The non-intrusive method of applying the gettext change to
|
||||
# the global namespace only
|
||||
self.__fn_map = getattr(self._gtobjs[lang], self._fn)
|
||||
|
||||
except FileNotFoundError:
|
||||
# The translation directory does not exist
|
||||
logger.debug(
|
||||
'Could not load translation path: %s',
|
||||
join(self._locale_dir, lang))
|
||||
|
||||
# Fallback (handle case where self.lang does not exist)
|
||||
if self.lang not in self._gtobjs:
|
||||
self._gtobjs[self.lang] = gettext
|
||||
self.__fn_map = getattr(self._gtobjs[self.lang], self._fn)
|
||||
|
||||
return False
|
||||
|
||||
logger.trace('Loaded language %s', lang)
|
||||
|
||||
if set_default:
|
||||
logger.debug('Language set to %s', lang)
|
||||
self.lang = lang
|
||||
|
||||
return True
|
||||
|
||||
@contextlib.contextmanager
|
||||
def lang_at(self, lang, mapto=_fn):
|
||||
"""
|
||||
The syntax works as:
|
||||
with at.lang_at('fr'):
|
||||
# apprise works as though the french language has been
|
||||
# defined. afterwards, the language falls back to whatever
|
||||
# it was.
|
||||
"""
|
||||
|
||||
if GETTEXT_LOADED is False:
|
||||
# Do nothing
|
||||
yield None
|
||||
|
||||
# we're done
|
||||
return
|
||||
|
||||
# Tidy the language
|
||||
lang = AppriseLocale.detect_language(lang, detect_fallback=False)
|
||||
if lang not in self._gtobjs and not self.add(lang, set_default=False):
|
||||
# Do Nothing
|
||||
yield getattr(self._gtobjs[self.lang], mapto)
|
||||
else:
|
||||
# Yield
|
||||
yield getattr(self._gtobjs[lang], mapto)
|
||||
|
||||
return
|
||||
|
||||
@property
|
||||
def gettext(self):
|
||||
"""
|
||||
Return the current language gettext() function
|
||||
|
||||
Useful for assigning to `_`
|
||||
"""
|
||||
return self._gtobjs[self.lang].gettext
|
||||
|
||||
@staticmethod
|
||||
def detect_language(lang=None, detect_fallback=True):
|
||||
"""
|
||||
Returns the language (if it's retrievable)
|
||||
"""
|
||||
# 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, str):
|
||||
if detect_fallback is False:
|
||||
# no detection enabled; we're done
|
||||
return None
|
||||
|
||||
# Posix lookup
|
||||
lookup = os.environ.get
|
||||
localename = None
|
||||
for variable in ('LC_ALL', 'LC_CTYPE', 'LANG', 'LANGUAGE'):
|
||||
localename = lookup(variable, None)
|
||||
if localename:
|
||||
result = AppriseLocale._local_re.match(localename)
|
||||
if result and result.group('lang'):
|
||||
return result.group('lang').lower()
|
||||
|
||||
# Windows handling
|
||||
if hasattr(ctypes, 'windll'):
|
||||
windll = ctypes.windll.kernel32
|
||||
try:
|
||||
lang = locale.windows_locale[
|
||||
windll.GetUserDefaultUILanguage()]
|
||||
|
||||
# Our detected windows language
|
||||
return lang[0:2].lower()
|
||||
|
||||
except (TypeError, KeyError):
|
||||
# Fallback to posix detection
|
||||
pass
|
||||
|
||||
# Built in locale library check
|
||||
try:
|
||||
# Acquire our locale
|
||||
lang = locale.getlocale()[0]
|
||||
|
||||
except (ValueError, TypeError) as e:
|
||||
# This occurs when an invalid locale was parsed from the
|
||||
# environment variable. While we still return None in this
|
||||
# case, we want to better notify the end user of this. Users
|
||||
# receiving this error should check their environment
|
||||
# variables.
|
||||
logger.warning(
|
||||
'Language detection failure / {}'.format(str(e)))
|
||||
return None
|
||||
|
||||
return None if not lang else lang[0:2].lower()
|
||||
|
||||
def __getstate__(self):
|
||||
"""
|
||||
Pickle Support dumps()
|
||||
"""
|
||||
state = self.__dict__.copy()
|
||||
|
||||
# Remove the unpicklable entries.
|
||||
del state['_gtobjs']
|
||||
del state['_AppriseLocale__fn_map']
|
||||
return state
|
||||
|
||||
def __setstate__(self, state):
|
||||
"""
|
||||
Pickle Support loads()
|
||||
"""
|
||||
self.__dict__.update(state)
|
||||
# Our mapping to our _fn
|
||||
self.__fn_map = None
|
||||
self._gtobjs = {}
|
||||
self.add(state['lang'], set_default=True)
|
||||
|
||||
|
||||
#
|
||||
# Prepare our default LOCALE Singleton
|
||||
#
|
||||
LOCALE = AppriseLocale()
|
||||
|
||||
|
||||
class LazyTranslation:
|
||||
|
@ -77,7 +275,7 @@ class LazyTranslation:
|
|||
super().__init__(*args, **kwargs)
|
||||
|
||||
def __str__(self):
|
||||
return gettext.gettext(self.text)
|
||||
return LOCALE.gettext(self.text) if GETTEXT_LOADED else self.text
|
||||
|
||||
|
||||
# Lazy translation handling
|
||||
|
@ -86,140 +284,3 @@ def gettext_lazy(text):
|
|||
A dummy function that can be referenced
|
||||
"""
|
||||
return LazyTranslation(text=text)
|
||||
|
||||
|
||||
class AppriseLocale:
|
||||
"""
|
||||
A wrapper class to gettext so that we can manipulate multiple lanaguages
|
||||
on the fly if required.
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, language=None):
|
||||
"""
|
||||
Initializes our object, if a language is specified, then we
|
||||
initialize ourselves to that, otherwise we use whatever we detect
|
||||
from the local operating system. If all else fails, we resort to the
|
||||
defined default_language.
|
||||
|
||||
"""
|
||||
|
||||
# Cache previously loaded translations
|
||||
self._gtobjs = {}
|
||||
|
||||
# Get our language
|
||||
self.lang = AppriseLocale.detect_language(language)
|
||||
|
||||
if GETTEXT_LOADED is False:
|
||||
# We're done
|
||||
return
|
||||
|
||||
if self.lang:
|
||||
# Load our gettext object and install our language
|
||||
try:
|
||||
self._gtobjs[self.lang] = gettext.translation(
|
||||
DOMAIN, localedir=LOCALE_DIR, languages=[self.lang])
|
||||
|
||||
# Install our language
|
||||
self._gtobjs[self.lang].install()
|
||||
|
||||
except IOError:
|
||||
# This occurs if we can't access/load our translations
|
||||
pass
|
||||
|
||||
@contextlib.contextmanager
|
||||
def lang_at(self, lang):
|
||||
"""
|
||||
The syntax works as:
|
||||
with at.lang_at('fr'):
|
||||
# apprise works as though the french language has been
|
||||
# defined. afterwards, the language falls back to whatever
|
||||
# it was.
|
||||
"""
|
||||
|
||||
if GETTEXT_LOADED is False:
|
||||
# yield
|
||||
yield
|
||||
|
||||
# we're done
|
||||
return
|
||||
|
||||
# Tidy the language
|
||||
lang = AppriseLocale.detect_language(lang, detect_fallback=False)
|
||||
|
||||
# Now attempt to load it
|
||||
try:
|
||||
if lang in self._gtobjs:
|
||||
if lang != self.lang:
|
||||
# Install our language only if we aren't using it
|
||||
# already
|
||||
self._gtobjs[lang].install()
|
||||
|
||||
else:
|
||||
self._gtobjs[lang] = gettext.translation(
|
||||
DOMAIN, localedir=LOCALE_DIR, languages=[self.lang])
|
||||
|
||||
# Install our language
|
||||
self._gtobjs[lang].install()
|
||||
|
||||
# Yield
|
||||
yield
|
||||
|
||||
except (IOError, KeyError):
|
||||
# This occurs if we can't access/load our translations
|
||||
# Yield reguardless
|
||||
yield
|
||||
|
||||
finally:
|
||||
# Fall back to our previous language
|
||||
if lang != self.lang and lang in self._gtobjs:
|
||||
# Install our language
|
||||
self._gtobjs[self.lang].install()
|
||||
|
||||
return
|
||||
|
||||
@staticmethod
|
||||
def detect_language(lang=None, detect_fallback=True):
|
||||
"""
|
||||
returns the language (if it's retrievable)
|
||||
"""
|
||||
# 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, str):
|
||||
if detect_fallback is False:
|
||||
# no detection enabled; we're done
|
||||
return None
|
||||
|
||||
if hasattr(ctypes, 'windll'):
|
||||
windll = ctypes.windll.kernel32
|
||||
try:
|
||||
lang = locale.windows_locale[
|
||||
windll.GetUserDefaultUILanguage()]
|
||||
|
||||
# Our detected windows language
|
||||
return lang[0:2].lower()
|
||||
|
||||
except (TypeError, KeyError):
|
||||
# Fallback to posix detection
|
||||
pass
|
||||
|
||||
try:
|
||||
# Detect language
|
||||
lang = locale.getdefaultlocale()[0]
|
||||
|
||||
except ValueError as e:
|
||||
# This occurs when an invalid locale was parsed from the
|
||||
# environment variable. While we still return None in this
|
||||
# case, we want to better notify the end user of this. Users
|
||||
# receiving this error should check their environment
|
||||
# variables.
|
||||
logger.warning(
|
||||
'Language detection failure / {}'.format(str(e)))
|
||||
return None
|
||||
|
||||
except TypeError:
|
||||
# None is returned if the default can't be determined
|
||||
# we're done in this case
|
||||
return None
|
||||
|
||||
return None if not lang else lang[0:2].lower()
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# BSD 3-Clause License
|
||||
# BSD 2-Clause License
|
||||
#
|
||||
# Apprise - Push Notification Library.
|
||||
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
|
||||
|
@ -14,10 +14,6 @@
|
|||
# this list of conditions and the following disclaimer in the documentation
|
||||
# and/or other materials provided with the distribution.
|
||||
#
|
||||
# 3. Neither the name of the copyright holder nor the names of its
|
||||
# contributors may be used to endorse or promote products derived from
|
||||
# this software without specific prior written permission.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
|
@ -204,7 +200,14 @@ class URLBase:
|
|||
self.verify_certificate = parse_bool(kwargs.get('verify', True))
|
||||
|
||||
# Secure Mode
|
||||
self.secure = kwargs.get('secure', False)
|
||||
self.secure = kwargs.get('secure', None)
|
||||
try:
|
||||
if not isinstance(self.secure, bool):
|
||||
# Attempt to detect
|
||||
self.secure = kwargs.get('schema', '')[-1].lower() == 's'
|
||||
|
||||
except (TypeError, IndexError):
|
||||
self.secure = False
|
||||
|
||||
self.host = URLBase.unquote(kwargs.get('host'))
|
||||
self.port = kwargs.get('port')
|
||||
|
@ -228,6 +231,11 @@ class URLBase:
|
|||
# Always unquote the password if it exists
|
||||
self.password = URLBase.unquote(self.password)
|
||||
|
||||
# Store our full path consistently ensuring it ends with a `/'
|
||||
self.fullpath = URLBase.unquote(kwargs.get('fullpath'))
|
||||
if not isinstance(self.fullpath, str) or not self.fullpath:
|
||||
self.fullpath = '/'
|
||||
|
||||
# Store our Timeout Variables
|
||||
if 'rto' in kwargs:
|
||||
try:
|
||||
|
@ -307,7 +315,36 @@ class URLBase:
|
|||
arguments provied.
|
||||
|
||||
"""
|
||||
raise NotImplementedError("url() is implimented by the child class.")
|
||||
|
||||
# Our default parameters
|
||||
params = self.url_parameters(privacy=privacy, *args, **kwargs)
|
||||
|
||||
# Determine Authentication
|
||||
auth = ''
|
||||
if self.user and self.password:
|
||||
auth = '{user}:{password}@'.format(
|
||||
user=URLBase.quote(self.user, safe=''),
|
||||
password=self.pprint(
|
||||
self.password, privacy, mode=PrivacyMode.Secret, safe=''),
|
||||
)
|
||||
elif self.user:
|
||||
auth = '{user}@'.format(
|
||||
user=URLBase.quote(self.user, safe=''),
|
||||
)
|
||||
|
||||
default_port = 443 if self.secure else 80
|
||||
|
||||
return '{schema}://{auth}{hostname}{port}{fullpath}?{params}'.format(
|
||||
schema='https' if self.secure else 'http',
|
||||
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),
|
||||
fullpath=URLBase.quote(self.fullpath, safe='/')
|
||||
if self.fullpath else '/',
|
||||
params=URLBase.urlencode(params),
|
||||
)
|
||||
|
||||
def __contains__(self, tags):
|
||||
"""
|
||||
|
@ -583,6 +620,33 @@ class URLBase:
|
|||
"""
|
||||
return (self.socket_connect_timeout, self.socket_read_timeout)
|
||||
|
||||
@property
|
||||
def request_auth(self):
|
||||
"""This is primarily used to fullfill the `auth` keyword argument
|
||||
that is used by requests.get() and requests.put() calls.
|
||||
"""
|
||||
return (self.user, self.password) if self.user else None
|
||||
|
||||
@property
|
||||
def request_url(self):
|
||||
"""
|
||||
Assemble a simple URL that can be used by the requests library
|
||||
|
||||
"""
|
||||
|
||||
# Acquire our schema
|
||||
schema = 'https' if self.secure else 'http'
|
||||
|
||||
# Prepare our URL
|
||||
url = '%s://%s' % (schema, self.host)
|
||||
|
||||
# Apply Port information if present
|
||||
if isinstance(self.port, int):
|
||||
url += ':%d' % self.port
|
||||
|
||||
# Append our full path
|
||||
return url + self.fullpath
|
||||
|
||||
def url_parameters(self, *args, **kwargs):
|
||||
"""
|
||||
Provides a default set of args to work with. This can greatly
|
||||
|
@ -603,7 +667,8 @@ class URLBase:
|
|||
}
|
||||
|
||||
@staticmethod
|
||||
def parse_url(url, verify_host=True, plus_to_space=False):
|
||||
def parse_url(url, verify_host=True, plus_to_space=False,
|
||||
strict_port=False):
|
||||
"""Parses the URL and returns it broken apart into a dictionary.
|
||||
|
||||
This is very specific and customized for Apprise.
|
||||
|
@ -624,13 +689,13 @@ class URLBase:
|
|||
|
||||
results = parse_url(
|
||||
url, default_schema='unknown', verify_host=verify_host,
|
||||
plus_to_space=plus_to_space)
|
||||
plus_to_space=plus_to_space, strict_port=strict_port)
|
||||
|
||||
if not results:
|
||||
# We're done; we failed to parse our url
|
||||
return results
|
||||
|
||||
# if our URL ends with an 's', then assueme our secure flag is set.
|
||||
# if our URL ends with an 's', then assume our secure flag is set.
|
||||
results['secure'] = (results['schema'][-1] == 's')
|
||||
|
||||
# Support SSL Certificate 'verify' keyword. Default to being enabled
|
||||
|
@ -650,6 +715,21 @@ class URLBase:
|
|||
if 'user' in results['qsd']:
|
||||
results['user'] = results['qsd']['user']
|
||||
|
||||
# parse_url() always creates a 'password' and 'user' entry in the
|
||||
# results returned. Entries are set to None if they weren't specified
|
||||
if results['password'] is None and 'user' in results['qsd']:
|
||||
# Handle cases where the user= provided in 2 locations, we want
|
||||
# the original to fall back as a being a password (if one wasn't
|
||||
# otherwise defined)
|
||||
# e.g.
|
||||
# mailtos://PASSWORD@hostname?user=admin@mail-domain.com
|
||||
# - the PASSWORD gets lost in the parse url() since a user=
|
||||
# over-ride is specified.
|
||||
presults = parse_url(results['url'])
|
||||
if presults:
|
||||
# Store our Password
|
||||
results['password'] = presults['user']
|
||||
|
||||
# Store our socket read timeout if specified
|
||||
if 'rto' in results['qsd']:
|
||||
results['rto'] = results['qsd']['rto']
|
||||
|
@ -685,6 +765,15 @@ class URLBase:
|
|||
|
||||
return response
|
||||
|
||||
def __len__(self):
|
||||
"""
|
||||
Should be over-ridden and allows the tracking of how many targets
|
||||
are associated with each URLBase object.
|
||||
|
||||
Default is always 1
|
||||
"""
|
||||
return 1
|
||||
|
||||
def schemas(self):
|
||||
"""A simple function that returns a set of all schemas associated
|
||||
with this object based on the object.protocol and
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# BSD 3-Clause License
|
||||
# BSD 2-Clause License
|
||||
#
|
||||
# Apprise - Push Notification Library.
|
||||
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
|
||||
|
@ -14,10 +14,6 @@
|
|||
# this list of conditions and the following disclaimer in the documentation
|
||||
# and/or other materials provided with the distribution.
|
||||
#
|
||||
# 3. Neither the name of the copyright holder nor the names of its
|
||||
# contributors may be used to endorse or promote products derived from
|
||||
# this software without specific prior written permission.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
|
@ -31,7 +27,7 @@
|
|||
# POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
__title__ = 'Apprise'
|
||||
__version__ = '1.3.0'
|
||||
__version__ = '1.6.0'
|
||||
__author__ = 'Chris Caron'
|
||||
__license__ = 'BSD'
|
||||
__copywrite__ = 'Copyright (C) 2023 Chris Caron <lead2gold@gmail.com>'
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# BSD 3-Clause License
|
||||
# BSD 2-Clause License
|
||||
#
|
||||
# Apprise - Push Notification Library.
|
||||
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
|
||||
|
@ -14,10 +14,6 @@
|
|||
# this list of conditions and the following disclaimer in the documentation
|
||||
# and/or other materials provided with the distribution.
|
||||
#
|
||||
# 3. Neither the name of the copyright holder nor the names of its
|
||||
# contributors may be used to endorse or promote products derived from
|
||||
# this software without specific prior written permission.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
|
@ -68,7 +64,8 @@ class AttachBase(URLBase):
|
|||
# set to zero (0), then no check is performed
|
||||
# 1 MB = 1048576 bytes
|
||||
# 5 MB = 5242880 bytes
|
||||
max_file_size = 5242880
|
||||
# 1 GB = 1048576000 bytes
|
||||
max_file_size = 1048576000
|
||||
|
||||
# By default all attachments types are inaccessible.
|
||||
# Developers of items identified in the attachment plugin directory
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# BSD 3-Clause License
|
||||
# BSD 2-Clause License
|
||||
#
|
||||
# Apprise - Push Notification Library.
|
||||
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
|
||||
|
@ -14,10 +14,6 @@
|
|||
# this list of conditions and the following disclaimer in the documentation
|
||||
# and/or other materials provided with the distribution.
|
||||
#
|
||||
# 3. Neither the name of the copyright holder nor the names of its
|
||||
# contributors may be used to endorse or promote products derived from
|
||||
# this software without specific prior written permission.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# BSD 3-Clause License
|
||||
# BSD 2-Clause License
|
||||
#
|
||||
# Apprise - Push Notification Library.
|
||||
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
|
||||
|
@ -14,10 +14,6 @@
|
|||
# this list of conditions and the following disclaimer in the documentation
|
||||
# and/or other materials provided with the distribution.
|
||||
#
|
||||
# 3. Neither the name of the copyright holder nor the names of its
|
||||
# contributors may be used to endorse or promote products derived from
|
||||
# this software without specific prior written permission.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# BSD 3-Clause License
|
||||
# BSD 2-Clause License
|
||||
#
|
||||
# Apprise - Push Notification Library.
|
||||
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
|
||||
|
@ -14,10 +14,6 @@
|
|||
# this list of conditions and the following disclaimer in the documentation
|
||||
# and/or other materials provided with the distribution.
|
||||
#
|
||||
# 3. Neither the name of the copyright holder nor the names of its
|
||||
# contributors may be used to endorse or promote products derived from
|
||||
# this software without specific prior written permission.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# BSD 3-Clause License
|
||||
# BSD 2-Clause License
|
||||
#
|
||||
# Apprise - Push Notification Library.
|
||||
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
|
||||
|
@ -14,10 +14,6 @@
|
|||
# this list of conditions and the following disclaimer in the documentation
|
||||
# and/or other materials provided with the distribution.
|
||||
#
|
||||
# 3. Neither the name of the copyright holder nor the names of its
|
||||
# contributors may be used to endorse or promote products derived from
|
||||
# this software without specific prior written permission.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# BSD 3-Clause License
|
||||
# BSD 2-Clause License
|
||||
#
|
||||
# Apprise - Push Notification Library.
|
||||
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
|
||||
|
@ -14,10 +14,6 @@
|
|||
# this list of conditions and the following disclaimer in the documentation
|
||||
# and/or other materials provided with the distribution.
|
||||
#
|
||||
# 3. Neither the name of the copyright holder nor the names of its
|
||||
# contributors may be used to endorse or promote products derived from
|
||||
# this software without specific prior written permission.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
|
@ -355,6 +351,77 @@ class ConfigBase(URLBase):
|
|||
# missing and/or expired.
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
def __normalize_tag_groups(group_tags):
|
||||
"""
|
||||
Used to normalize a tag assign map which looks like:
|
||||
{
|
||||
'group': set('{tag1}', '{group1}', '{tag2}'),
|
||||
'group1': set('{tag2}','{tag3}'),
|
||||
}
|
||||
|
||||
Then normalized it (merging groups); with respect to the above, the
|
||||
output would be:
|
||||
{
|
||||
'group': set('{tag1}', '{tag2}', '{tag3}),
|
||||
'group1': set('{tag2}','{tag3}'),
|
||||
}
|
||||
|
||||
"""
|
||||
# Prepare a key set list we can use
|
||||
tag_groups = set([str(x) for x in group_tags.keys()])
|
||||
|
||||
def _expand(tags, ignore=None):
|
||||
"""
|
||||
Expands based on tag provided and returns a set
|
||||
|
||||
this also updates the group_tags while it goes
|
||||
"""
|
||||
|
||||
# Prepare ourselves a return set
|
||||
results = set()
|
||||
ignore = set() if ignore is None else ignore
|
||||
|
||||
# track groups
|
||||
groups = set()
|
||||
|
||||
for tag in tags:
|
||||
if tag in ignore:
|
||||
continue
|
||||
|
||||
# Track our groups
|
||||
groups.add(tag)
|
||||
|
||||
# Store what we know is worth keping
|
||||
results |= group_tags[tag] - tag_groups
|
||||
|
||||
# Get simple tag assignments
|
||||
found = group_tags[tag] & tag_groups
|
||||
if not found:
|
||||
continue
|
||||
|
||||
for gtag in found:
|
||||
if gtag in ignore:
|
||||
continue
|
||||
|
||||
# Go deeper (recursion)
|
||||
ignore.add(tag)
|
||||
group_tags[gtag] = _expand(set([gtag]), ignore=ignore)
|
||||
results |= group_tags[gtag]
|
||||
|
||||
# Pop ignore
|
||||
ignore.remove(tag)
|
||||
|
||||
return results
|
||||
|
||||
for tag in tag_groups:
|
||||
# Get our tags
|
||||
group_tags[tag] |= _expand(set([tag]))
|
||||
if not group_tags[tag]:
|
||||
ConfigBase.logger.warning(
|
||||
'The group {} has no tags assigned to it'.format(tag))
|
||||
del group_tags[tag]
|
||||
|
||||
@staticmethod
|
||||
def parse_url(url, verify_host=True):
|
||||
"""Parses the URL and returns it broken apart into a dictionary.
|
||||
|
@ -533,6 +600,9 @@ class ConfigBase(URLBase):
|
|||
# as additional configuration entries when loaded.
|
||||
include <ConfigURL>
|
||||
|
||||
# Assign tag contents to a group identifier
|
||||
<Group(s)>=<Tag(s)>
|
||||
|
||||
"""
|
||||
# A list of loaded Notification Services
|
||||
servers = list()
|
||||
|
@ -541,6 +611,12 @@ class ConfigBase(URLBase):
|
|||
# the include keyword
|
||||
configs = list()
|
||||
|
||||
# Track all of the tags we want to assign later on
|
||||
group_tags = {}
|
||||
|
||||
# Track our entries to preload
|
||||
preloaded = []
|
||||
|
||||
# Prepare our Asset Object
|
||||
asset = asset if isinstance(asset, AppriseAsset) else AppriseAsset()
|
||||
|
||||
|
@ -548,7 +624,7 @@ class ConfigBase(URLBase):
|
|||
valid_line_re = re.compile(
|
||||
r'^\s*(?P<line>([;#]+(?P<comment>.*))|'
|
||||
r'(\s*(?P<tags>[a-z0-9, \t_-]+)\s*=|=)?\s*'
|
||||
r'(?P<url>[a-z0-9]{2,9}://.*)|'
|
||||
r'((?P<url>[a-z0-9]{1,12}://.*)|(?P<assign>[a-z0-9, \t_-]+))|'
|
||||
r'include\s+(?P<config>.+))?\s*$', re.I)
|
||||
|
||||
try:
|
||||
|
@ -574,8 +650,13 @@ class ConfigBase(URLBase):
|
|||
# otherwise.
|
||||
return (list(), list())
|
||||
|
||||
url, config = result.group('url'), result.group('config')
|
||||
if not (url or config):
|
||||
# Retrieve our line
|
||||
url, assign, config = \
|
||||
result.group('url'), \
|
||||
result.group('assign'), \
|
||||
result.group('config')
|
||||
|
||||
if not (url or config or assign):
|
||||
# Comment/empty line; do nothing
|
||||
continue
|
||||
|
||||
|
@ -595,6 +676,33 @@ class ConfigBase(URLBase):
|
|||
loggable_url = url if not asset.secure_logging \
|
||||
else cwe312_url(url)
|
||||
|
||||
if assign:
|
||||
groups = set(parse_list(result.group('tags'), cast=str))
|
||||
if not groups:
|
||||
# no tags were assigned
|
||||
ConfigBase.logger.warning(
|
||||
'Unparseable tag assignment - no group(s) '
|
||||
'on line {}'.format(line))
|
||||
continue
|
||||
|
||||
# Get our tags
|
||||
tags = set(parse_list(assign, cast=str))
|
||||
if not tags:
|
||||
# no tags were assigned
|
||||
ConfigBase.logger.warning(
|
||||
'Unparseable tag assignment - no tag(s) to assign '
|
||||
'on line {}'.format(line))
|
||||
continue
|
||||
|
||||
# Update our tag group map
|
||||
for tag_group in groups:
|
||||
if tag_group not in group_tags:
|
||||
group_tags[tag_group] = set()
|
||||
|
||||
# ensure our tag group is never included in the assignment
|
||||
group_tags[tag_group] |= tags - set([tag_group])
|
||||
continue
|
||||
|
||||
# Acquire our url tokens
|
||||
results = plugins.url_to_dict(
|
||||
url, secure_logging=asset.secure_logging)
|
||||
|
@ -607,25 +715,57 @@ class ConfigBase(URLBase):
|
|||
|
||||
# Build a list of tags to associate with the newly added
|
||||
# notifications if any were set
|
||||
results['tag'] = set(parse_list(result.group('tags')))
|
||||
results['tag'] = set(parse_list(result.group('tags'), cast=str))
|
||||
|
||||
# Set our Asset Object
|
||||
results['asset'] = asset
|
||||
|
||||
# Store our preloaded entries
|
||||
preloaded.append({
|
||||
'results': results,
|
||||
'line': line,
|
||||
'loggable_url': loggable_url,
|
||||
})
|
||||
|
||||
#
|
||||
# Normalize Tag Groups
|
||||
# - Expand Groups of Groups so that they don't exist
|
||||
#
|
||||
ConfigBase.__normalize_tag_groups(group_tags)
|
||||
|
||||
#
|
||||
# URL Processing
|
||||
#
|
||||
for entry in preloaded:
|
||||
# Point to our results entry for easier reference below
|
||||
results = entry['results']
|
||||
|
||||
#
|
||||
# Apply our tag groups if they're defined
|
||||
#
|
||||
for group, tags in group_tags.items():
|
||||
# Detect if anything assigned to this tag also maps back to a
|
||||
# group. If so we want to add the group to our list
|
||||
if next((True for tag in results['tag']
|
||||
if tag in tags), False):
|
||||
results['tag'].add(group)
|
||||
|
||||
try:
|
||||
# Attempt to create an instance of our plugin using the
|
||||
# parsed URL information
|
||||
plugin = common.NOTIFY_SCHEMA_MAP[results['schema']](**results)
|
||||
plugin = common.NOTIFY_SCHEMA_MAP[
|
||||
results['schema']](**results)
|
||||
|
||||
# Create log entry of loaded URL
|
||||
ConfigBase.logger.debug(
|
||||
'Loaded URL: %s', plugin.url(privacy=asset.secure_logging))
|
||||
'Loaded URL: %s', plugin.url(
|
||||
privacy=results['asset'].secure_logging))
|
||||
|
||||
except Exception as e:
|
||||
# the arguments are invalid or can not be used.
|
||||
ConfigBase.logger.warning(
|
||||
'Could not load URL {} on line {}.'.format(
|
||||
loggable_url, line))
|
||||
entry['loggable_url'], entry['line']))
|
||||
ConfigBase.logger.debug('Loading Exception: %s' % str(e))
|
||||
continue
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# BSD 3-Clause License
|
||||
# BSD 2-Clause License
|
||||
#
|
||||
# Apprise - Push Notification Library.
|
||||
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
|
||||
|
@ -14,10 +14,6 @@
|
|||
# this list of conditions and the following disclaimer in the documentation
|
||||
# and/or other materials provided with the distribution.
|
||||
#
|
||||
# 3. Neither the name of the copyright holder nor the names of its
|
||||
# contributors may be used to endorse or promote products derived from
|
||||
# this software without specific prior written permission.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# BSD 3-Clause License
|
||||
# BSD 2-Clause License
|
||||
#
|
||||
# Apprise - Push Notification Library.
|
||||
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
|
||||
|
@ -14,10 +14,6 @@
|
|||
# this list of conditions and the following disclaimer in the documentation
|
||||
# and/or other materials provided with the distribution.
|
||||
#
|
||||
# 3. Neither the name of the copyright holder nor the names of its
|
||||
# contributors may be used to endorse or promote products derived from
|
||||
# this software without specific prior written permission.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# BSD 3-Clause License
|
||||
# BSD 2-Clause License
|
||||
#
|
||||
# Apprise - Push Notification Library.
|
||||
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
|
||||
|
@ -14,10 +14,6 @@
|
|||
# this list of conditions and the following disclaimer in the documentation
|
||||
# and/or other materials provided with the distribution.
|
||||
#
|
||||
# 3. Neither the name of the copyright holder nor the names of its
|
||||
# contributors may be used to endorse or promote products derived from
|
||||
# this software without specific prior written permission.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# BSD 3-Clause License
|
||||
# BSD 2-Clause License
|
||||
#
|
||||
# Apprise - Push Notification Library.
|
||||
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
|
||||
|
@ -14,10 +14,6 @@
|
|||
# this list of conditions and the following disclaimer in the documentation
|
||||
# and/or other materials provided with the distribution.
|
||||
#
|
||||
# 3. Neither the name of the copyright holder nor the names of its
|
||||
# contributors may be used to endorse or promote products derived from
|
||||
# this software without specific prior written permission.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# BSD 3-Clause License
|
||||
# BSD 2-Clause License
|
||||
#
|
||||
# Apprise - Push Notification Library.
|
||||
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
|
||||
|
@ -14,10 +14,6 @@
|
|||
# this list of conditions and the following disclaimer in the documentation
|
||||
# and/or other materials provided with the distribution.
|
||||
#
|
||||
# 3. Neither the name of the copyright holder nor the names of its
|
||||
# contributors may be used to endorse or promote products derived from
|
||||
# this software without specific prior written permission.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# BSD 3-Clause License
|
||||
# BSD 2-Clause License
|
||||
#
|
||||
# Apprise - Push Notification Library.
|
||||
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
|
||||
|
@ -14,10 +14,6 @@
|
|||
# this list of conditions and the following disclaimer in the documentation
|
||||
# and/or other materials provided with the distribution.
|
||||
#
|
||||
# 3. Neither the name of the copyright holder nor the names of its
|
||||
# contributors may be used to endorse or promote products derived from
|
||||
# this software without specific prior written permission.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
|
@ -28,6 +24,7 @@
|
|||
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
# POSSIBILITY OF SUCH DAMAGE.USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
# POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
from ..plugins.NotifyBase import NotifyBase
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# BSD 3-Clause License
|
||||
# BSD 2-Clause License
|
||||
#
|
||||
# Apprise - Push Notification Library.
|
||||
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
|
||||
|
@ -14,10 +14,6 @@
|
|||
# this list of conditions and the following disclaimer in the documentation
|
||||
# and/or other materials provided with the distribution.
|
||||
#
|
||||
# 3. Neither the name of the copyright holder nor the names of its
|
||||
# contributors may be used to endorse or promote products derived from
|
||||
# this software without specific prior written permission.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# BSD 3-Clause License
|
||||
# BSD 2-Clause License
|
||||
#
|
||||
# Apprise - Push Notification Library.
|
||||
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
|
||||
|
@ -14,10 +14,6 @@
|
|||
# this list of conditions and the following disclaimer in the documentation
|
||||
# and/or other materials provided with the distribution.
|
||||
#
|
||||
# 3. Neither the name of the copyright holder nor the names of its
|
||||
# contributors may be used to endorse or promote products derived from
|
||||
# this software without specific prior written permission.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
|
|
|
@ -6,16 +6,16 @@
|
|||
#, fuzzy
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: apprise 1.3.0\n"
|
||||
"Project-Id-Version: apprise 1.6.0\n"
|
||||
"Report-Msgid-Bugs-To: lead2gold@gmail.com\n"
|
||||
"POT-Creation-Date: 2023-02-22 17:31-0500\n"
|
||||
"POT-Creation-Date: 2023-10-15 15:56-0400\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=utf-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Generated-By: Babel 2.9.1\n"
|
||||
"Generated-By: Babel 2.11.0\n"
|
||||
|
||||
msgid "A local Gnome environment is required."
|
||||
msgstr ""
|
||||
|
@ -164,6 +164,9 @@ msgstr ""
|
|||
msgid "Consumer Secret"
|
||||
msgstr ""
|
||||
|
||||
msgid "Content Placement"
|
||||
msgstr ""
|
||||
|
||||
msgid "Country"
|
||||
msgstr ""
|
||||
|
||||
|
@ -209,6 +212,9 @@ msgstr ""
|
|||
msgid "Device Name"
|
||||
msgstr ""
|
||||
|
||||
msgid "Discord Event ID"
|
||||
msgstr ""
|
||||
|
||||
msgid "Display Footer"
|
||||
msgstr ""
|
||||
|
||||
|
@ -224,12 +230,6 @@ msgstr ""
|
|||
msgid "Email Header"
|
||||
msgstr ""
|
||||
|
||||
msgid "Encrypted Password"
|
||||
msgstr ""
|
||||
|
||||
msgid "Encrypted Salt"
|
||||
msgstr ""
|
||||
|
||||
msgid "Entity"
|
||||
msgstr ""
|
||||
|
||||
|
@ -272,6 +272,9 @@ msgstr ""
|
|||
msgid "From Name"
|
||||
msgstr ""
|
||||
|
||||
msgid "From Phone ID"
|
||||
msgstr ""
|
||||
|
||||
msgid "From Phone No"
|
||||
msgstr ""
|
||||
|
||||
|
@ -317,6 +320,9 @@ msgstr ""
|
|||
msgid "Integration ID"
|
||||
msgstr ""
|
||||
|
||||
msgid "Integration Key"
|
||||
msgstr ""
|
||||
|
||||
msgid "Is Ad?"
|
||||
msgstr ""
|
||||
|
||||
|
@ -353,6 +359,9 @@ msgstr ""
|
|||
msgid "Master Key"
|
||||
msgstr ""
|
||||
|
||||
msgid "Matrix API Verion"
|
||||
msgstr ""
|
||||
|
||||
msgid "Memory"
|
||||
msgstr ""
|
||||
|
||||
|
@ -433,6 +442,12 @@ msgstr ""
|
|||
msgid "Payload Extras"
|
||||
msgstr ""
|
||||
|
||||
msgid "Ping Discord Role"
|
||||
msgstr ""
|
||||
|
||||
msgid "Ping Discord User"
|
||||
msgstr ""
|
||||
|
||||
msgid "Port"
|
||||
msgstr ""
|
||||
|
||||
|
@ -451,9 +466,15 @@ msgstr ""
|
|||
msgid "Provider Key"
|
||||
msgstr ""
|
||||
|
||||
msgid "Pushkey"
|
||||
msgstr ""
|
||||
|
||||
msgid "QOS"
|
||||
msgstr ""
|
||||
|
||||
msgid "Query Method"
|
||||
msgstr ""
|
||||
|
||||
msgid "Region"
|
||||
msgstr ""
|
||||
|
||||
|
@ -475,24 +496,27 @@ msgstr ""
|
|||
msgid "Retry"
|
||||
msgstr ""
|
||||
|
||||
msgid "Rooms"
|
||||
msgstr ""
|
||||
|
||||
msgid "Route"
|
||||
msgid "Room ID"
|
||||
msgstr ""
|
||||
|
||||
msgid "Route Group"
|
||||
msgstr ""
|
||||
|
||||
msgid "Routing Key"
|
||||
msgstr ""
|
||||
|
||||
msgid "SMTP Server"
|
||||
msgstr ""
|
||||
|
||||
msgid "Salt"
|
||||
msgstr ""
|
||||
|
||||
msgid "Schema"
|
||||
msgstr ""
|
||||
|
||||
msgid "Secret"
|
||||
msgstr ""
|
||||
|
||||
msgid "Secret API Key"
|
||||
msgstr ""
|
||||
|
||||
msgid "Secret Access Key"
|
||||
msgstr ""
|
||||
|
||||
|
@ -520,6 +544,9 @@ msgstr ""
|
|||
msgid "Severity"
|
||||
msgstr ""
|
||||
|
||||
msgid "Short URL"
|
||||
msgstr ""
|
||||
|
||||
msgid "Show Status"
|
||||
msgstr ""
|
||||
|
||||
|
@ -559,9 +586,6 @@ msgstr ""
|
|||
msgid "Subtitle"
|
||||
msgstr ""
|
||||
|
||||
msgid "Syslog Mode"
|
||||
msgstr ""
|
||||
|
||||
msgid "Tags"
|
||||
msgstr ""
|
||||
|
||||
|
@ -658,6 +682,15 @@ msgstr ""
|
|||
msgid "Template Data"
|
||||
msgstr ""
|
||||
|
||||
msgid "Template ID"
|
||||
msgstr ""
|
||||
|
||||
msgid "Template Mapping"
|
||||
msgstr ""
|
||||
|
||||
msgid "Template Name"
|
||||
msgstr ""
|
||||
|
||||
msgid "Template Path"
|
||||
msgstr ""
|
||||
|
||||
|
@ -706,12 +739,18 @@ msgstr ""
|
|||
msgid "Topic"
|
||||
msgstr ""
|
||||
|
||||
msgid "Topic Thread ID"
|
||||
msgstr ""
|
||||
|
||||
msgid "Transmitter Groups"
|
||||
msgstr ""
|
||||
|
||||
msgid "URL"
|
||||
msgstr ""
|
||||
|
||||
msgid "URL Prefix"
|
||||
msgstr ""
|
||||
|
||||
msgid "URL Title"
|
||||
msgstr ""
|
||||
|
||||
|
@ -796,3 +835,6 @@ msgstr ""
|
|||
msgid "ttl"
|
||||
msgstr ""
|
||||
|
||||
msgid "validity"
|
||||
msgstr ""
|
||||
|
||||
|
|
|
@ -3,9 +3,10 @@
|
|||
# This file is distributed under the same license as the apprise project.
|
||||
# Chris Caron <lead2gold@gmail.com>, 2019.
|
||||
#
|
||||
msgid ""
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: apprise 0.7.6\n"
|
||||
|
||||
"Project-Id-Version: apprise 1.4.5\n"
|
||||
"Report-Msgid-Bugs-To: lead2gold@gmail.com\n"
|
||||
"POT-Creation-Date: 2019-05-28 16:56-0400\n"
|
||||
"PO-Revision-Date: 2019-05-24 20:00-0400\n"
|
||||
|
@ -18,276 +19,272 @@ msgstr ""
|
|||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Generated-By: Babel 2.6.0\n"
|
||||
|
||||
msgid "API Key"
|
||||
msgstr ""
|
||||
msgid "API Key"
|
||||
msgstr "API Key"
|
||||
|
||||
msgid "Access Key"
|
||||
msgstr ""
|
||||
msgid "Access Key"
|
||||
msgstr "Access Key"
|
||||
|
||||
msgid "Access Key ID"
|
||||
msgstr ""
|
||||
msgid "Access Key ID"
|
||||
msgstr "Access Key ID"
|
||||
|
||||
msgid "Access Secret"
|
||||
msgstr ""
|
||||
msgid "Access Secret"
|
||||
msgstr "Access Secret"
|
||||
|
||||
msgid "Access Token"
|
||||
msgstr ""
|
||||
msgid "Access Token"
|
||||
msgstr "Access Token"
|
||||
|
||||
msgid "Account SID"
|
||||
msgstr ""
|
||||
msgid "Account SID"
|
||||
msgstr "Account SID"
|
||||
|
||||
msgid "Add Tokens"
|
||||
msgstr ""
|
||||
msgid "Add Tokens"
|
||||
msgstr "Add Tokens"
|
||||
|
||||
msgid "Application Key"
|
||||
msgstr ""
|
||||
msgid "Application Key"
|
||||
msgstr "Application Key"
|
||||
|
||||
msgid "Application Secret"
|
||||
msgstr ""
|
||||
msgid "Application Secret"
|
||||
msgstr "Application Secret"
|
||||
|
||||
msgid "Auth Token"
|
||||
msgstr ""
|
||||
msgid "Auth Token"
|
||||
msgstr "Auth Token"
|
||||
|
||||
msgid "Authorization Token"
|
||||
msgstr ""
|
||||
msgid "Authorization Token"
|
||||
msgstr "Authorization Token"
|
||||
|
||||
msgid "Avatar Image"
|
||||
msgstr ""
|
||||
msgid "Avatar Image"
|
||||
msgstr "Avatar Image"
|
||||
|
||||
msgid "Bot Name"
|
||||
msgstr ""
|
||||
msgid "Bot Name"
|
||||
msgstr "Bot Name"
|
||||
|
||||
msgid "Bot Token"
|
||||
msgstr ""
|
||||
msgid "Bot Token"
|
||||
msgstr "Bot Token"
|
||||
|
||||
msgid "Channels"
|
||||
msgstr ""
|
||||
msgid "Channels"
|
||||
msgstr "Channels"
|
||||
|
||||
msgid "Consumer Key"
|
||||
msgstr ""
|
||||
msgid "Consumer Key"
|
||||
msgstr "Consumer Key"
|
||||
|
||||
msgid "Consumer Secret"
|
||||
msgstr ""
|
||||
msgid "Consumer Secret"
|
||||
msgstr "Consumer Secret"
|
||||
|
||||
msgid "Detect Bot Owner"
|
||||
msgstr ""
|
||||
msgid "Detect Bot Owner"
|
||||
msgstr "Detect Bot Owner"
|
||||
|
||||
msgid "Device ID"
|
||||
msgstr ""
|
||||
msgid "Device ID"
|
||||
msgstr "Device ID"
|
||||
|
||||
msgid "Display Footer"
|
||||
msgstr ""
|
||||
msgid "Display Footer"
|
||||
msgstr "Display Footer"
|
||||
|
||||
msgid "Domain"
|
||||
msgstr ""
|
||||
msgid "Domain"
|
||||
msgstr "Domain"
|
||||
|
||||
msgid "Duration"
|
||||
msgstr ""
|
||||
msgid "Duration"
|
||||
msgstr "Duration"
|
||||
|
||||
msgid "Events"
|
||||
msgstr ""
|
||||
msgid "Events"
|
||||
msgstr "Events"
|
||||
|
||||
msgid "Footer Logo"
|
||||
msgstr ""
|
||||
msgid "Footer Logo"
|
||||
msgstr "Footer Logo"
|
||||
|
||||
msgid "From Email"
|
||||
msgstr ""
|
||||
msgid "From Email"
|
||||
msgstr "From Email"
|
||||
|
||||
msgid "From Name"
|
||||
msgstr ""
|
||||
msgid "From Name"
|
||||
msgstr "From Name"
|
||||
|
||||
msgid "From Phone No"
|
||||
msgstr ""
|
||||
msgid "From Phone No"
|
||||
msgstr "From Phone No"
|
||||
|
||||
msgid "Group"
|
||||
msgstr ""
|
||||
msgid "Group"
|
||||
msgstr "Group"
|
||||
|
||||
msgid "HTTP Header"
|
||||
msgstr ""
|
||||
msgid "HTTP Header"
|
||||
msgstr "HTTP Header"
|
||||
|
||||
msgid "Hostname"
|
||||
msgstr ""
|
||||
msgid "Hostname"
|
||||
msgstr "Hostname"
|
||||
|
||||
msgid "Include Image"
|
||||
msgstr ""
|
||||
msgid "Include Image"
|
||||
msgstr "Include Image"
|
||||
|
||||
msgid "Modal"
|
||||
msgstr ""
|
||||
msgid "Modal"
|
||||
msgstr "Modal"
|
||||
|
||||
msgid "Notify Format"
|
||||
msgstr ""
|
||||
msgid "Notify Format"
|
||||
msgstr "Notify Format"
|
||||
|
||||
msgid "Organization"
|
||||
msgstr ""
|
||||
msgid "Organization"
|
||||
msgstr "Organization"
|
||||
|
||||
msgid "Overflow Mode"
|
||||
msgstr ""
|
||||
msgid "Overflow Mode"
|
||||
msgstr "Overflow Mode"
|
||||
|
||||
msgid "Password"
|
||||
msgstr ""
|
||||
msgid "Password"
|
||||
msgstr "Password"
|
||||
|
||||
msgid "Port"
|
||||
msgstr ""
|
||||
msgid "Port"
|
||||
msgstr "Port"
|
||||
|
||||
msgid "Priority"
|
||||
msgstr ""
|
||||
msgid "Priority"
|
||||
msgstr "Priority"
|
||||
|
||||
msgid "Provider Key"
|
||||
msgstr ""
|
||||
msgid "Provider Key"
|
||||
msgstr "Provider Key"
|
||||
|
||||
msgid "Region"
|
||||
msgstr ""
|
||||
msgid "Region"
|
||||
msgstr "Region"
|
||||
|
||||
msgid "Region Name"
|
||||
msgstr ""
|
||||
msgid "Region Name"
|
||||
msgstr "Region Name"
|
||||
|
||||
msgid "Remove Tokens"
|
||||
msgstr ""
|
||||
msgid "Remove Tokens"
|
||||
msgstr "Remove Tokens"
|
||||
|
||||
msgid "Rooms"
|
||||
msgstr ""
|
||||
msgid "Rooms"
|
||||
msgstr "Rooms"
|
||||
|
||||
msgid "SMTP Server"
|
||||
msgstr ""
|
||||
msgid "SMTP Server"
|
||||
msgstr "SMTP Server"
|
||||
|
||||
msgid "Schema"
|
||||
msgstr ""
|
||||
msgid "Schema"
|
||||
msgstr "Schema"
|
||||
|
||||
msgid "Secret Access Key"
|
||||
msgstr ""
|
||||
msgid "Secret Access Key"
|
||||
msgstr "Secret Access Key"
|
||||
|
||||
msgid "Secret Key"
|
||||
msgstr ""
|
||||
msgid "Secret Key"
|
||||
msgstr "Secret Key"
|
||||
|
||||
msgid "Secure Mode"
|
||||
msgstr ""
|
||||
msgid "Secure Mode"
|
||||
msgstr "Secure Mode"
|
||||
|
||||
msgid "Server Timeout"
|
||||
msgstr ""
|
||||
msgid "Server Timeout"
|
||||
msgstr "Server Timeout"
|
||||
|
||||
msgid "Sound"
|
||||
msgstr ""
|
||||
msgid "Sound"
|
||||
msgstr "Sound"
|
||||
|
||||
msgid "Source JID"
|
||||
msgstr ""
|
||||
msgid "Source JID"
|
||||
msgstr "Source JID"
|
||||
|
||||
msgid "Target Channel"
|
||||
msgstr ""
|
||||
msgid "Target Channel"
|
||||
msgstr "Target Channel"
|
||||
|
||||
msgid "Target Chat ID"
|
||||
msgstr ""
|
||||
msgid "Target Chat ID"
|
||||
msgstr "Target Chat ID"
|
||||
|
||||
msgid "Target Device"
|
||||
msgstr ""
|
||||
msgid "Target Device"
|
||||
msgstr "Target Device"
|
||||
|
||||
msgid "Target Device ID"
|
||||
msgstr ""
|
||||
msgid "Target Device ID"
|
||||
msgstr "Target Device ID"
|
||||
|
||||
msgid "Target Email"
|
||||
msgstr ""
|
||||
msgid "Target Email"
|
||||
msgstr "Target Email"
|
||||
|
||||
msgid "Target Emails"
|
||||
msgstr ""
|
||||
msgid "Target Emails"
|
||||
msgstr "Target Emails"
|
||||
|
||||
msgid "Target Encoded ID"
|
||||
msgstr ""
|
||||
msgid "Target Encoded ID"
|
||||
msgstr "Target Encoded ID"
|
||||
|
||||
msgid "Target JID"
|
||||
msgstr ""
|
||||
msgid "Target JID"
|
||||
msgstr "Target JID"
|
||||
|
||||
msgid "Target Phone No"
|
||||
msgstr ""
|
||||
msgid "Target Phone No"
|
||||
msgstr "Target Phone No"
|
||||
|
||||
msgid "Target Room Alias"
|
||||
msgstr ""
|
||||
msgid "Target Room Alias"
|
||||
msgstr "Target Room Alias"
|
||||
|
||||
msgid "Target Room ID"
|
||||
msgstr ""
|
||||
msgid "Target Room ID"
|
||||
msgstr "Target Room ID"
|
||||
|
||||
msgid "Target Short Code"
|
||||
msgstr ""
|
||||
msgid "Target Short Code"
|
||||
msgstr "Target Short Code"
|
||||
|
||||
msgid "Target Tag ID"
|
||||
msgstr ""
|
||||
msgid "Target Tag ID"
|
||||
msgstr "Target Tag ID"
|
||||
|
||||
msgid "Target Topic"
|
||||
msgstr ""
|
||||
msgid "Target Topic"
|
||||
msgstr "Target Topic"
|
||||
|
||||
msgid "Target User"
|
||||
msgstr ""
|
||||
msgid "Target User"
|
||||
msgstr "Target User"
|
||||
|
||||
msgid "Targets"
|
||||
msgstr ""
|
||||
msgid "Targets"
|
||||
msgstr "Targets"
|
||||
|
||||
msgid "Text To Speech"
|
||||
msgstr ""
|
||||
msgid "Text To Speech"
|
||||
msgstr "Text To Speech"
|
||||
|
||||
msgid "To Channel ID"
|
||||
msgstr ""
|
||||
msgid "To Channel ID"
|
||||
msgstr "To Channel ID"
|
||||
|
||||
msgid "To Email"
|
||||
msgstr ""
|
||||
msgid "To Email"
|
||||
msgstr "To Email"
|
||||
|
||||
msgid "To User ID"
|
||||
msgstr ""
|
||||
msgid "To User ID"
|
||||
msgstr "To User ID"
|
||||
|
||||
msgid "Token"
|
||||
msgstr ""
|
||||
msgid "Token"
|
||||
msgstr "Token"
|
||||
|
||||
msgid "Token A"
|
||||
msgstr ""
|
||||
msgid "Token A"
|
||||
msgstr "Token A"
|
||||
|
||||
msgid "Token B"
|
||||
msgstr ""
|
||||
msgid "Token B"
|
||||
msgstr "Token B"
|
||||
|
||||
msgid "Token C"
|
||||
msgstr ""
|
||||
msgid "Token C"
|
||||
msgstr "Token C"
|
||||
|
||||
msgid "Urgency"
|
||||
msgstr ""
|
||||
msgid "Urgency"
|
||||
msgstr "Urgency"
|
||||
|
||||
msgid "Use Avatar"
|
||||
msgstr ""
|
||||
msgid "Use Avatar"
|
||||
msgstr "Use Avatar"
|
||||
|
||||
msgid "User"
|
||||
msgstr ""
|
||||
msgid "User"
|
||||
msgstr "User"
|
||||
|
||||
msgid "User Key"
|
||||
msgstr ""
|
||||
msgid "User Key"
|
||||
msgstr "User Key"
|
||||
|
||||
msgid "User Name"
|
||||
msgstr ""
|
||||
msgid "User Name"
|
||||
msgstr "User Name"
|
||||
|
||||
msgid "Username"
|
||||
msgstr ""
|
||||
msgid "Username"
|
||||
msgstr "Username"
|
||||
|
||||
msgid "Verify SSL"
|
||||
msgstr ""
|
||||
msgid "Verify SSL"
|
||||
msgstr "Verify SSL"
|
||||
|
||||
msgid "Version"
|
||||
msgstr ""
|
||||
msgid "Version"
|
||||
msgstr "Version"
|
||||
|
||||
msgid "Webhook"
|
||||
msgstr ""
|
||||
msgid "Webhook"
|
||||
msgstr "Webhook"
|
||||
|
||||
msgid "Webhook ID"
|
||||
msgstr ""
|
||||
msgid "Webhook ID"
|
||||
msgstr "Webhook ID"
|
||||
|
||||
msgid "Webhook Mode"
|
||||
msgstr ""
|
||||
msgid "Webhook Mode"
|
||||
msgstr "Webhook Mode"
|
||||
|
||||
msgid "Webhook Token"
|
||||
msgstr ""
|
||||
msgid "Webhook Token"
|
||||
msgstr "Webhook Token"
|
||||
|
||||
msgid "X-Axis"
|
||||
msgstr ""
|
||||
msgid "X-Axis"
|
||||
msgstr "X-Axis"
|
||||
|
||||
msgid "XEP"
|
||||
msgstr ""
|
||||
|
||||
msgid "Y-Axis"
|
||||
msgstr ""
|
||||
|
||||
#~ msgid "Access Key Secret"
|
||||
#~ msgstr ""
|
||||
msgid "XEP"
|
||||
msgstr "XEP"
|
||||
|
||||
msgid "Y-Axis"
|
||||
msgstr "Y-Axis"
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# BSD 3-Clause License
|
||||
# BSD 2-Clause License
|
||||
#
|
||||
# Apprise - Push Notification Library.
|
||||
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
|
||||
|
@ -14,10 +14,6 @@
|
|||
# this list of conditions and the following disclaimer in the documentation
|
||||
# and/or other materials provided with the distribution.
|
||||
#
|
||||
# 3. Neither the name of the copyright holder nor the names of its
|
||||
# contributors may be used to endorse or promote products derived from
|
||||
# this software without specific prior written permission.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# BSD 3-Clause License
|
||||
# BSD 2-Clause License
|
||||
#
|
||||
# Apprise - Push Notification Library.
|
||||
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
|
||||
|
@ -14,10 +14,6 @@
|
|||
# this list of conditions and the following disclaimer in the documentation
|
||||
# and/or other materials provided with the distribution.
|
||||
#
|
||||
# 3. Neither the name of the copyright holder nor the names of its
|
||||
# contributors may be used to endorse or promote products derived from
|
||||
# this software without specific prior written permission.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
|
@ -33,6 +29,7 @@
|
|||
import re
|
||||
import requests
|
||||
from json import dumps
|
||||
import base64
|
||||
|
||||
from .NotifyBase import NotifyBase
|
||||
from ..URLBase import PrivacyMode
|
||||
|
@ -42,6 +39,20 @@ from ..utils import validate_regex
|
|||
from ..AppriseLocale import gettext_lazy as _
|
||||
|
||||
|
||||
class AppriseAPIMethod:
|
||||
"""
|
||||
Defines the method to post data tot he remote server
|
||||
"""
|
||||
JSON = 'json'
|
||||
FORM = 'form'
|
||||
|
||||
|
||||
APPRISE_API_METHODS = (
|
||||
AppriseAPIMethod.FORM,
|
||||
AppriseAPIMethod.JSON,
|
||||
)
|
||||
|
||||
|
||||
class NotifyAppriseAPI(NotifyBase):
|
||||
"""
|
||||
A wrapper for Apprise (Persistent) API Notifications
|
||||
|
@ -62,9 +73,12 @@ class NotifyAppriseAPI(NotifyBase):
|
|||
# A URL that takes you to the setup/help of the specific protocol
|
||||
setup_url = 'https://github.com/caronc/apprise/wiki/Notify_apprise_api'
|
||||
|
||||
# Support attachments
|
||||
attachment_support = True
|
||||
|
||||
# Depending on the number of transactions/notifications taking place, this
|
||||
# could take a while. 30 seconds should be enough to perform the task
|
||||
socket_connect_timeout = 30.0
|
||||
socket_read_timeout = 30.0
|
||||
|
||||
# Disable throttle rate for Apprise API requests since they are normally
|
||||
# local anyway
|
||||
|
@ -119,6 +133,12 @@ class NotifyAppriseAPI(NotifyBase):
|
|||
'name': _('Tags'),
|
||||
'type': 'string',
|
||||
},
|
||||
'method': {
|
||||
'name': _('Query Method'),
|
||||
'type': 'choice:string',
|
||||
'values': APPRISE_API_METHODS,
|
||||
'default': APPRISE_API_METHODS[0],
|
||||
},
|
||||
'to': {
|
||||
'alias_of': 'token',
|
||||
},
|
||||
|
@ -132,7 +152,8 @@ class NotifyAppriseAPI(NotifyBase):
|
|||
},
|
||||
}
|
||||
|
||||
def __init__(self, token=None, tags=None, headers=None, **kwargs):
|
||||
def __init__(self, token=None, tags=None, method=None, headers=None,
|
||||
**kwargs):
|
||||
"""
|
||||
Initialize Apprise API Object
|
||||
|
||||
|
@ -142,10 +163,6 @@ class NotifyAppriseAPI(NotifyBase):
|
|||
"""
|
||||
super().__init__(**kwargs)
|
||||
|
||||
self.fullpath = kwargs.get('fullpath')
|
||||
if not isinstance(self.fullpath, str):
|
||||
self.fullpath = '/'
|
||||
|
||||
self.token = validate_regex(
|
||||
token, *self.template_tokens['token']['regex'])
|
||||
if not self.token:
|
||||
|
@ -154,6 +171,14 @@ class NotifyAppriseAPI(NotifyBase):
|
|||
self.logger.warning(msg)
|
||||
raise TypeError(msg)
|
||||
|
||||
self.method = self.template_args['method']['default'] \
|
||||
if not isinstance(method, str) else method.lower()
|
||||
|
||||
if self.method not in APPRISE_API_METHODS:
|
||||
msg = 'The method specified ({}) is invalid.'.format(method)
|
||||
self.logger.warning(msg)
|
||||
raise TypeError(msg)
|
||||
|
||||
# Build list of tags
|
||||
self.__tags = parse_list(tags)
|
||||
|
||||
|
@ -169,8 +194,13 @@ class NotifyAppriseAPI(NotifyBase):
|
|||
Returns the URL built dynamically based on specified arguments.
|
||||
"""
|
||||
|
||||
# Our URL parameters
|
||||
params = self.url_parameters(privacy=privacy, *args, **kwargs)
|
||||
# Define any URL parameters
|
||||
params = {
|
||||
'method': self.method,
|
||||
}
|
||||
|
||||
# Extend our parameters
|
||||
params.update(self.url_parameters(privacy=privacy, *args, **kwargs))
|
||||
|
||||
# Append our headers into our parameters
|
||||
params.update({'+{}'.format(k): v for k, v in self.headers.items()})
|
||||
|
@ -209,15 +239,61 @@ class NotifyAppriseAPI(NotifyBase):
|
|||
token=self.pprint(self.token, privacy, safe=''),
|
||||
params=NotifyAppriseAPI.urlencode(params))
|
||||
|
||||
def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs):
|
||||
def send(self, body, title='', notify_type=NotifyType.INFO, attach=None,
|
||||
**kwargs):
|
||||
"""
|
||||
Perform Apprise API Notification
|
||||
"""
|
||||
|
||||
headers = {}
|
||||
# Prepare HTTP Headers
|
||||
headers = {
|
||||
'User-Agent': self.app_id,
|
||||
}
|
||||
|
||||
# Apply any/all header over-rides defined
|
||||
headers.update(self.headers)
|
||||
|
||||
attachments = []
|
||||
files = []
|
||||
if attach and self.attachment_support:
|
||||
for no, attachment in enumerate(attach, start=1):
|
||||
# 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:
|
||||
if self.method == AppriseAPIMethod.JSON:
|
||||
with open(attachment.path, 'rb') as f:
|
||||
# Output must be in a DataURL format (that's what
|
||||
# PushSafer calls it):
|
||||
attachments.append({
|
||||
'filename': attachment.name,
|
||||
'base64': base64.b64encode(f.read())
|
||||
.decode('utf-8'),
|
||||
'mimetype': attachment.mimetype,
|
||||
})
|
||||
|
||||
else: # AppriseAPIMethod.FORM
|
||||
files.append((
|
||||
'file{:02d}'.format(no),
|
||||
(
|
||||
attachment.name,
|
||||
open(attachment.path, 'rb'),
|
||||
attachment.mimetype,
|
||||
)
|
||||
))
|
||||
|
||||
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 Apprise API Object
|
||||
payload = {
|
||||
# Apprise API Payload
|
||||
|
@ -227,6 +303,14 @@ class NotifyAppriseAPI(NotifyBase):
|
|||
'format': self.notify_format,
|
||||
}
|
||||
|
||||
if self.method == AppriseAPIMethod.JSON:
|
||||
headers['Content-Type'] = 'application/json'
|
||||
|
||||
if attachments:
|
||||
payload['attachments'] = attachments
|
||||
|
||||
payload = dumps(payload)
|
||||
|
||||
if self.__tags:
|
||||
payload['tag'] = self.__tags
|
||||
|
||||
|
@ -242,13 +326,13 @@ class NotifyAppriseAPI(NotifyBase):
|
|||
url += ':%d' % self.port
|
||||
|
||||
fullpath = self.fullpath.strip('/')
|
||||
url += '/{}/'.format(fullpath) if fullpath else '/'
|
||||
url += 'notify/{}'.format(self.token)
|
||||
url += '{}'.format('/' + fullpath) if fullpath else ''
|
||||
url += '/notify/{}'.format(self.token)
|
||||
|
||||
# Some entries can not be over-ridden
|
||||
headers.update({
|
||||
'User-Agent': self.app_id,
|
||||
'Content-Type': 'application/json',
|
||||
# Our response to be in JSON format always
|
||||
'Accept': 'application/json',
|
||||
# Pass our Source UUID4 Identifier
|
||||
'X-Apprise-ID': self.asset._uid,
|
||||
# Pass our current recursion count to our upstream server
|
||||
|
@ -266,9 +350,10 @@ class NotifyAppriseAPI(NotifyBase):
|
|||
try:
|
||||
r = requests.post(
|
||||
url,
|
||||
data=dumps(payload),
|
||||
data=payload,
|
||||
headers=headers,
|
||||
auth=auth,
|
||||
files=files if files else None,
|
||||
verify=self.verify_certificate,
|
||||
timeout=self.request_timeout,
|
||||
)
|
||||
|
@ -290,7 +375,8 @@ class NotifyAppriseAPI(NotifyBase):
|
|||
return False
|
||||
|
||||
else:
|
||||
self.logger.info('Sent Apprise API notification.')
|
||||
self.logger.info(
|
||||
'Sent Apprise API notification; method=%s.', self.method)
|
||||
|
||||
except requests.RequestException as e:
|
||||
self.logger.warning(
|
||||
|
@ -301,6 +387,18 @@ class NotifyAppriseAPI(NotifyBase):
|
|||
# Return; we're done
|
||||
return False
|
||||
|
||||
except (OSError, IOError) as e:
|
||||
self.logger.warning(
|
||||
'An I/O error occurred while reading one of the '
|
||||
'attached files.')
|
||||
self.logger.debug('I/O Exception: %s' % str(e))
|
||||
return False
|
||||
|
||||
finally:
|
||||
for file in files:
|
||||
# Ensure all files are closed
|
||||
file[1][1].close()
|
||||
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
|
@ -377,4 +475,9 @@ class NotifyAppriseAPI(NotifyBase):
|
|||
# re-assemble our full path
|
||||
results['fullpath'] = '/'.join(entries)
|
||||
|
||||
# Set method if specified
|
||||
if 'method' in results['qsd'] and len(results['qsd']['method']):
|
||||
results['method'] = \
|
||||
NotifyAppriseAPI.unquote(results['qsd']['method'])
|
||||
|
||||
return results
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# BSD 3-Clause License
|
||||
# BSD 2-Clause License
|
||||
#
|
||||
# Apprise - Push Notification Library.
|
||||
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
|
||||
|
@ -14,10 +14,6 @@
|
|||
# this list of conditions and the following disclaimer in the documentation
|
||||
# and/or other materials provided with the distribution.
|
||||
#
|
||||
# 3. Neither the name of the copyright holder nor the names of its
|
||||
# contributors may be used to endorse or promote products derived from
|
||||
# this software without specific prior written permission.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
|
@ -127,10 +123,10 @@ class NotifyBark(NotifyBase):
|
|||
|
||||
# Define object templates
|
||||
templates = (
|
||||
'{schema}://{host}/{targets}',
|
||||
'{schema}://{host}:{port}/{targets}',
|
||||
'{schema}://{user}:{password}@{host}/{targets}',
|
||||
'{schema}://{user}:{password}@{host}:{port}/{targets}',
|
||||
'{schema}://{user}:{password}@{host}/{targets}',
|
||||
)
|
||||
|
||||
# Define our template arguments
|
||||
|
@ -163,6 +159,7 @@ class NotifyBark(NotifyBase):
|
|||
'targets': {
|
||||
'name': _('Targets'),
|
||||
'type': 'list:string',
|
||||
'required': True,
|
||||
},
|
||||
})
|
||||
|
||||
|
@ -280,7 +277,7 @@ class NotifyBark(NotifyBase):
|
|||
# error tracking (used for function return)
|
||||
has_error = False
|
||||
|
||||
if not len(self.targets):
|
||||
if not self.targets:
|
||||
# We have nothing to notify; we're done
|
||||
self.logger.warning('There are no Bark devices to notify')
|
||||
return False
|
||||
|
@ -456,6 +453,12 @@ class NotifyBark(NotifyBase):
|
|||
params=NotifyBark.urlencode(params),
|
||||
)
|
||||
|
||||
def __len__(self):
|
||||
"""
|
||||
Returns the number of targets associated with this notification
|
||||
"""
|
||||
return len(self.targets)
|
||||
|
||||
@staticmethod
|
||||
def parse_url(url):
|
||||
"""
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# BSD 3-Clause License
|
||||
# BSD 2-Clause License
|
||||
#
|
||||
# Apprise - Push Notification Library.
|
||||
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
|
||||
|
@ -14,10 +14,6 @@
|
|||
# this list of conditions and the following disclaimer in the documentation
|
||||
# and/or other materials provided with the distribution.
|
||||
#
|
||||
# 3. Neither the name of the copyright holder nor the names of its
|
||||
# contributors may be used to endorse or promote products derived from
|
||||
# this software without specific prior written permission.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
|
@ -139,6 +135,18 @@ class NotifyBase(URLBase):
|
|||
# Default Overflow Mode
|
||||
overflow_mode = OverflowMode.UPSTREAM
|
||||
|
||||
# Support Attachments; this defaults to being disabled.
|
||||
# Since apprise allows you to send attachments without a body or title
|
||||
# defined, by letting Apprise know the plugin won't support attachments
|
||||
# up front, it can quickly pass over and ignore calls to these end points.
|
||||
|
||||
# You must set this to true if your application can handle attachments.
|
||||
# You must also consider a flow change to your notification if this is set
|
||||
# to True as well as now there will be cases where both the body and title
|
||||
# may not be set. There will never be a case where a body, or attachment
|
||||
# isn't set in the same call to your notify() function.
|
||||
attachment_support = False
|
||||
|
||||
# Default Title HTML Tagging
|
||||
# When a title is specified for a notification service that doesn't accept
|
||||
# titles, by default apprise tries to give a plesant view and convert the
|
||||
|
@ -316,7 +324,7 @@ class NotifyBase(URLBase):
|
|||
the_cors = (do_send(**kwargs2) for kwargs2 in send_calls)
|
||||
return all(await asyncio.gather(*the_cors))
|
||||
|
||||
def _build_send_calls(self, body, title=None,
|
||||
def _build_send_calls(self, body=None, title=None,
|
||||
notify_type=NotifyType.INFO, overflow=None,
|
||||
attach=None, body_format=None, **kwargs):
|
||||
"""
|
||||
|
@ -339,6 +347,28 @@ class NotifyBase(URLBase):
|
|||
# bad attachments
|
||||
raise
|
||||
|
||||
# Handle situations where the body is None
|
||||
body = '' if not body else body
|
||||
|
||||
elif not (body or attach):
|
||||
# If there is not an attachment at the very least, a body must be
|
||||
# present
|
||||
msg = "No message body or attachment was specified."
|
||||
self.logger.warning(msg)
|
||||
raise TypeError(msg)
|
||||
|
||||
if not body and not self.attachment_support:
|
||||
# If no body was specified, then we know that an attachment
|
||||
# was. This is logic checked earlier in the code.
|
||||
#
|
||||
# Knowing this, if the plugin itself doesn't support sending
|
||||
# attachments, there is nothing further to do here, just move
|
||||
# along.
|
||||
msg = f"{self.service_name} does not support attachments; " \
|
||||
" service skipped"
|
||||
self.logger.warning(msg)
|
||||
raise TypeError(msg)
|
||||
|
||||
# Handle situations where the title is None
|
||||
title = '' if not title else title
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# BSD 3-Clause License
|
||||
# BSD 2-Clause License
|
||||
#
|
||||
# Apprise - Push Notification Library.
|
||||
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
|
||||
|
@ -14,10 +14,6 @@
|
|||
# this list of conditions and the following disclaimer in the documentation
|
||||
# and/or other materials provided with the distribution.
|
||||
#
|
||||
# 3. Neither the name of the copyright holder nor the names of its
|
||||
# contributors may be used to endorse or promote products derived from
|
||||
# this software without specific prior written permission.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
|
@ -46,6 +42,7 @@ except ImportError:
|
|||
from .NotifyBase import NotifyBase
|
||||
from ..URLBase import PrivacyMode
|
||||
from ..utils import parse_bool
|
||||
from ..utils import parse_list
|
||||
from ..utils import validate_regex
|
||||
from ..common import NotifyType
|
||||
from ..common import NotifyImageSize
|
||||
|
@ -58,7 +55,7 @@ DEFAULT_TAG = '@all'
|
|||
# list of tagged devices that the notification need to be send to, and a
|
||||
# boolean operator (‘and’ / ‘or’) that defines the criteria to match devices
|
||||
# against those tags.
|
||||
IS_TAG = re.compile(r'^[@](?P<name>[A-Z0-9]{1,63})$', re.I)
|
||||
IS_TAG = re.compile(r'^[@]?(?P<name>[A-Z0-9]{1,63})$', re.I)
|
||||
|
||||
# Device tokens are only referenced when developing.
|
||||
# It's not likely you'll send a message directly to a device, but if you do;
|
||||
|
@ -150,6 +147,12 @@ class NotifyBoxcar(NotifyBase):
|
|||
'to': {
|
||||
'alias_of': 'targets',
|
||||
},
|
||||
'access': {
|
||||
'alias_of': 'access_key',
|
||||
},
|
||||
'secret': {
|
||||
'alias_of': 'secret_key',
|
||||
},
|
||||
})
|
||||
|
||||
def __init__(self, access, secret, targets=None, include_image=True,
|
||||
|
@ -160,7 +163,7 @@ class NotifyBoxcar(NotifyBase):
|
|||
super().__init__(**kwargs)
|
||||
|
||||
# Initialize tag list
|
||||
self.tags = list()
|
||||
self._tags = list()
|
||||
|
||||
# Initialize device_token list
|
||||
self.device_tokens = list()
|
||||
|
@ -184,29 +187,27 @@ class NotifyBoxcar(NotifyBase):
|
|||
raise TypeError(msg)
|
||||
|
||||
if not targets:
|
||||
self.tags.append(DEFAULT_TAG)
|
||||
self._tags.append(DEFAULT_TAG)
|
||||
targets = []
|
||||
|
||||
elif isinstance(targets, str):
|
||||
targets = [x for x in filter(bool, TAGS_LIST_DELIM.split(
|
||||
targets,
|
||||
))]
|
||||
|
||||
# Validate targets and drop bad ones:
|
||||
for target in targets:
|
||||
if IS_TAG.match(target):
|
||||
for target in parse_list(targets):
|
||||
result = IS_TAG.match(target)
|
||||
if result:
|
||||
# store valid tag/alias
|
||||
self.tags.append(IS_TAG.match(target).group('name'))
|
||||
self._tags.append(result.group('name'))
|
||||
continue
|
||||
|
||||
elif IS_DEVICETOKEN.match(target):
|
||||
result = IS_DEVICETOKEN.match(target)
|
||||
if result:
|
||||
# store valid device
|
||||
self.device_tokens.append(target)
|
||||
continue
|
||||
|
||||
else:
|
||||
self.logger.warning(
|
||||
'Dropped invalid tag/alias/device_token '
|
||||
'({}) specified.'.format(target),
|
||||
)
|
||||
self.logger.warning(
|
||||
'Dropped invalid tag/alias/device_token '
|
||||
'({}) specified.'.format(target),
|
||||
)
|
||||
|
||||
# Track whether or not we want to send an image with our notification
|
||||
# or not.
|
||||
|
@ -235,11 +236,10 @@ class NotifyBoxcar(NotifyBase):
|
|||
if title:
|
||||
payload['aps']['@title'] = title
|
||||
|
||||
if body:
|
||||
payload['aps']['alert'] = body
|
||||
payload['aps']['alert'] = body
|
||||
|
||||
if self.tags:
|
||||
payload['tags'] = {'or': self.tags}
|
||||
if self._tags:
|
||||
payload['tags'] = {'or': self._tags}
|
||||
|
||||
if self.device_tokens:
|
||||
payload['device_tokens'] = self.device_tokens
|
||||
|
@ -341,10 +341,18 @@ class NotifyBoxcar(NotifyBase):
|
|||
self.secret, privacy, mode=PrivacyMode.Secret, safe=''),
|
||||
targets='/'.join([
|
||||
NotifyBoxcar.quote(x, safe='') for x in chain(
|
||||
self.tags, self.device_tokens) if x != DEFAULT_TAG]),
|
||||
self._tags, self.device_tokens) if x != DEFAULT_TAG]),
|
||||
params=NotifyBoxcar.urlencode(params),
|
||||
)
|
||||
|
||||
def __len__(self):
|
||||
"""
|
||||
Returns the number of targets associated with this notification
|
||||
"""
|
||||
targets = len(self._tags) + len(self.device_tokens)
|
||||
# DEFAULT_TAG is set if no tokens/tags are otherwise set
|
||||
return targets if targets > 0 else 1
|
||||
|
||||
@staticmethod
|
||||
def parse_url(url):
|
||||
"""
|
||||
|
@ -374,6 +382,16 @@ class NotifyBoxcar(NotifyBase):
|
|||
results['targets'] += \
|
||||
NotifyBoxcar.parse_list(results['qsd'].get('to'))
|
||||
|
||||
# Access
|
||||
if 'access' in results['qsd'] and results['qsd']['access']:
|
||||
results['access'] = NotifyBoxcar.unquote(
|
||||
results['qsd']['access'].strip())
|
||||
|
||||
# Secret
|
||||
if 'secret' in results['qsd'] and results['qsd']['secret']:
|
||||
results['secret'] = NotifyBoxcar.unquote(
|
||||
results['qsd']['secret'].strip())
|
||||
|
||||
# Include images with our message
|
||||
results['include_image'] = \
|
||||
parse_bool(results['qsd'].get('image', True))
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# BSD 3-Clause License
|
||||
# BSD 2-Clause License
|
||||
#
|
||||
# Apprise - Push Notification Library.
|
||||
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
|
||||
|
@ -14,10 +14,6 @@
|
|||
# this list of conditions and the following disclaimer in the documentation
|
||||
# and/or other materials provided with the distribution.
|
||||
#
|
||||
# 3. Neither the name of the copyright holder nor the names of its
|
||||
# contributors may be used to endorse or promote products derived from
|
||||
# this software without specific prior written permission.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
|
@ -121,11 +117,13 @@ class NotifyBulkSMS(NotifyBase):
|
|||
'user': {
|
||||
'name': _('User Name'),
|
||||
'type': 'string',
|
||||
'required': True,
|
||||
},
|
||||
'password': {
|
||||
'name': _('Password'),
|
||||
'type': 'string',
|
||||
'private': True,
|
||||
'required': True,
|
||||
},
|
||||
'target_phone': {
|
||||
'name': _('Target Phone No'),
|
||||
|
@ -144,6 +142,7 @@ class NotifyBulkSMS(NotifyBase):
|
|||
'targets': {
|
||||
'name': _('Targets'),
|
||||
'type': 'list:string',
|
||||
'required': True,
|
||||
},
|
||||
})
|
||||
|
||||
|
@ -414,6 +413,24 @@ class NotifyBulkSMS(NotifyBase):
|
|||
for x in self.groups])),
|
||||
params=NotifyBulkSMS.urlencode(params))
|
||||
|
||||
def __len__(self):
|
||||
"""
|
||||
Returns the number of targets associated with this notification
|
||||
"""
|
||||
|
||||
#
|
||||
# Factor batch into calculation
|
||||
#
|
||||
# Note: Groups always require a separate request (and can not be
|
||||
# included in batch calculations)
|
||||
batch_size = 1 if not self.batch else self.default_batch_size
|
||||
targets = len(self.targets)
|
||||
if batch_size > 1:
|
||||
targets = int(targets / batch_size) + \
|
||||
(1 if targets % batch_size else 0)
|
||||
|
||||
return targets + len(self.groups)
|
||||
|
||||
@staticmethod
|
||||
def parse_url(url):
|
||||
"""
|
||||
|
|
460
lib/apprise/plugins/NotifyBurstSMS.py
Normal file
460
lib/apprise/plugins/NotifyBurstSMS.py
Normal file
|
@ -0,0 +1,460 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# BSD 2-Clause License
|
||||
#
|
||||
# Apprise - Push Notification Library.
|
||||
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
|
||||
#
|
||||
# Redistribution and use in source and binary forms, with or without
|
||||
# modification, are permitted provided that the following conditions are met:
|
||||
#
|
||||
# 1. Redistributions of source code must retain the above copyright notice,
|
||||
# this list of conditions and the following disclaimer.
|
||||
#
|
||||
# 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
# this list of conditions and the following disclaimer in the documentation
|
||||
# and/or other materials provided with the distribution.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
|
||||
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
||||
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
# POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
# Sign-up with https://burstsms.com/
|
||||
#
|
||||
# Define your API Secret here and acquire your API Key
|
||||
# - https://can.transmitsms.com/profile
|
||||
#
|
||||
import requests
|
||||
|
||||
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 ..utils import validate_regex
|
||||
from ..AppriseLocale import gettext_lazy as _
|
||||
|
||||
|
||||
class BurstSMSCountryCode:
|
||||
# Australia
|
||||
AU = 'au'
|
||||
# New Zeland
|
||||
NZ = 'nz'
|
||||
# United Kingdom
|
||||
UK = 'gb'
|
||||
# United States
|
||||
US = 'us'
|
||||
|
||||
|
||||
BURST_SMS_COUNTRY_CODES = (
|
||||
BurstSMSCountryCode.AU,
|
||||
BurstSMSCountryCode.NZ,
|
||||
BurstSMSCountryCode.UK,
|
||||
BurstSMSCountryCode.US,
|
||||
)
|
||||
|
||||
|
||||
class NotifyBurstSMS(NotifyBase):
|
||||
"""
|
||||
A wrapper for Burst SMS Notifications
|
||||
"""
|
||||
|
||||
# The default descriptive name associated with the Notification
|
||||
service_name = 'Burst SMS'
|
||||
|
||||
# The services URL
|
||||
service_url = 'https://burstsms.com/'
|
||||
|
||||
# The default protocol
|
||||
secure_protocol = 'burstsms'
|
||||
|
||||
# The maximum amount of SMS Messages that can reside within a single
|
||||
# batch transfer based on:
|
||||
# https://developer.transmitsms.com/#74911cf8-dec6-4319-a499-7f535a7fd08c
|
||||
default_batch_size = 500
|
||||
|
||||
# A URL that takes you to the setup/help of the specific protocol
|
||||
setup_url = 'https://github.com/caronc/apprise/wiki/Notify_burst_sms'
|
||||
|
||||
# Burst SMS uses the http protocol with JSON requests
|
||||
notify_url = 'https://api.transmitsms.com/send-sms.json'
|
||||
|
||||
# The maximum length of the body
|
||||
body_maxlen = 160
|
||||
|
||||
# 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}://{apikey}:{secret}@{sender_id}/{targets}',
|
||||
)
|
||||
|
||||
# Define our template tokens
|
||||
template_tokens = dict(NotifyBase.template_tokens, **{
|
||||
'apikey': {
|
||||
'name': _('API Key'),
|
||||
'type': 'string',
|
||||
'required': True,
|
||||
'regex': (r'^[a-z0-9]+$', 'i'),
|
||||
'private': True,
|
||||
},
|
||||
'secret': {
|
||||
'name': _('API Secret'),
|
||||
'type': 'string',
|
||||
'private': True,
|
||||
'required': True,
|
||||
'regex': (r'^[a-z0-9]+$', 'i'),
|
||||
},
|
||||
'sender_id': {
|
||||
'name': _('Sender ID'),
|
||||
'type': 'string',
|
||||
'required': True,
|
||||
'map_to': 'source',
|
||||
},
|
||||
'target_phone': {
|
||||
'name': _('Target Phone No'),
|
||||
'type': 'string',
|
||||
'prefix': '+',
|
||||
'regex': (r'^[0-9\s)(+-]+$', 'i'),
|
||||
'map_to': 'targets',
|
||||
},
|
||||
'targets': {
|
||||
'name': _('Targets'),
|
||||
'type': 'list:string',
|
||||
'required': True,
|
||||
},
|
||||
})
|
||||
|
||||
# Define our template arguments
|
||||
template_args = dict(NotifyBase.template_args, **{
|
||||
'to': {
|
||||
'alias_of': 'targets',
|
||||
},
|
||||
'from': {
|
||||
'alias_of': 'sender_id',
|
||||
},
|
||||
'key': {
|
||||
'alias_of': 'apikey',
|
||||
},
|
||||
'secret': {
|
||||
'alias_of': 'secret',
|
||||
},
|
||||
'country': {
|
||||
'name': _('Country'),
|
||||
'type': 'choice:string',
|
||||
'values': BURST_SMS_COUNTRY_CODES,
|
||||
'default': BurstSMSCountryCode.US,
|
||||
},
|
||||
# Validity
|
||||
# Expire a message send if it is undeliverable (defined in minutes)
|
||||
# If set to Zero (0); this is the default and sets the max validity
|
||||
# period
|
||||
'validity': {
|
||||
'name': _('validity'),
|
||||
'type': 'int',
|
||||
'default': 0
|
||||
},
|
||||
'batch': {
|
||||
'name': _('Batch Mode'),
|
||||
'type': 'bool',
|
||||
'default': False,
|
||||
},
|
||||
})
|
||||
|
||||
def __init__(self, apikey, secret, source, targets=None, country=None,
|
||||
validity=None, batch=None, **kwargs):
|
||||
"""
|
||||
Initialize Burst SMS Object
|
||||
"""
|
||||
super().__init__(**kwargs)
|
||||
|
||||
# API Key (associated with project)
|
||||
self.apikey = validate_regex(
|
||||
apikey, *self.template_tokens['apikey']['regex'])
|
||||
if not self.apikey:
|
||||
msg = 'An invalid Burst SMS API Key ' \
|
||||
'({}) was specified.'.format(apikey)
|
||||
self.logger.warning(msg)
|
||||
raise TypeError(msg)
|
||||
|
||||
# API Secret (associated with project)
|
||||
self.secret = validate_regex(
|
||||
secret, *self.template_tokens['secret']['regex'])
|
||||
if not self.secret:
|
||||
msg = 'An invalid Burst SMS API Secret ' \
|
||||
'({}) was specified.'.format(secret)
|
||||
self.logger.warning(msg)
|
||||
raise TypeError(msg)
|
||||
|
||||
if not country:
|
||||
self.country = self.template_args['country']['default']
|
||||
|
||||
else:
|
||||
self.country = country.lower().strip()
|
||||
if country not in BURST_SMS_COUNTRY_CODES:
|
||||
msg = 'An invalid Burst SMS country ' \
|
||||
'({}) was specified.'.format(country)
|
||||
self.logger.warning(msg)
|
||||
raise TypeError(msg)
|
||||
|
||||
# Set our Validity
|
||||
self.validity = self.template_args['validity']['default']
|
||||
if validity:
|
||||
try:
|
||||
self.validity = int(validity)
|
||||
|
||||
except (ValueError, TypeError):
|
||||
msg = 'The Burst SMS Validity specified ({}) is invalid.'\
|
||||
.format(validity)
|
||||
self.logger.warning(msg)
|
||||
raise TypeError(msg)
|
||||
|
||||
# Prepare Batch Mode Flag
|
||||
self.batch = self.template_args['batch']['default'] \
|
||||
if batch is None else batch
|
||||
|
||||
# The Sender ID
|
||||
self.source = validate_regex(source)
|
||||
if not self.source:
|
||||
msg = 'The Account Sender ID specified ' \
|
||||
'({}) is invalid.'.format(source)
|
||||
self.logger.warning(msg)
|
||||
raise TypeError(msg)
|
||||
|
||||
# Parse our targets
|
||||
self.targets = list()
|
||||
|
||||
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),
|
||||
)
|
||||
continue
|
||||
|
||||
# store valid phone number
|
||||
self.targets.append(result['full'])
|
||||
|
||||
return
|
||||
|
||||
def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs):
|
||||
"""
|
||||
Perform Burst SMS Notification
|
||||
"""
|
||||
|
||||
if not self.targets:
|
||||
self.logger.warning(
|
||||
'There are no valid Burst SMS targets to notify.')
|
||||
return False
|
||||
|
||||
# error tracking (used for function return)
|
||||
has_error = False
|
||||
|
||||
# Prepare our headers
|
||||
headers = {
|
||||
'User-Agent': self.app_id,
|
||||
'Accept': 'application/json',
|
||||
}
|
||||
|
||||
# Prepare our authentication
|
||||
auth = (self.apikey, self.secret)
|
||||
|
||||
# Prepare our payload
|
||||
payload = {
|
||||
'countrycode': self.country,
|
||||
'message': body,
|
||||
|
||||
# Sender ID
|
||||
'from': self.source,
|
||||
|
||||
# The to gets populated in the loop below
|
||||
'to': None,
|
||||
}
|
||||
|
||||
# Send in batches if identified to do so
|
||||
batch_size = 1 if not self.batch else self.default_batch_size
|
||||
|
||||
# Create a copy of the targets list
|
||||
targets = list(self.targets)
|
||||
|
||||
for index in range(0, len(targets), batch_size):
|
||||
|
||||
# Prepare our user
|
||||
payload['to'] = ','.join(self.targets[index:index + batch_size])
|
||||
|
||||
# Some Debug Logging
|
||||
self.logger.debug('Burst SMS POST URL: {} (cert_verify={})'.format(
|
||||
self.notify_url, self.verify_certificate))
|
||||
self.logger.debug('Burst SMS Payload: {}' .format(payload))
|
||||
|
||||
# Always call throttle before any remote server i/o is made
|
||||
self.throttle()
|
||||
|
||||
try:
|
||||
r = requests.post(
|
||||
self.notify_url,
|
||||
data=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 = \
|
||||
NotifyBurstSMS.http_response_code_lookup(
|
||||
r.status_code)
|
||||
|
||||
self.logger.warning(
|
||||
'Failed to send Burst SMS notification to {} '
|
||||
'target(s): {}{}error={}.'.format(
|
||||
len(self.targets[index:index + batch_size]),
|
||||
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 Burst SMS notification to %d target(s).' %
|
||||
len(self.targets[index:index + batch_size]))
|
||||
|
||||
except requests.RequestException as e:
|
||||
self.logger.warning(
|
||||
'A Connection error occurred sending Burst SMS '
|
||||
'notification to %d target(s).' %
|
||||
len(self.targets[index:index + batch_size]))
|
||||
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 = {
|
||||
'country': self.country,
|
||||
'batch': 'yes' if self.batch else 'no',
|
||||
}
|
||||
|
||||
if self.validity:
|
||||
params['validity'] = str(self.validity)
|
||||
|
||||
# Extend our parameters
|
||||
params.update(self.url_parameters(privacy=privacy, *args, **kwargs))
|
||||
|
||||
return '{schema}://{key}:{secret}@{source}/{targets}/?{params}'.format(
|
||||
schema=self.secure_protocol,
|
||||
key=self.pprint(self.apikey, privacy, safe=''),
|
||||
secret=self.pprint(
|
||||
self.secret, privacy, mode=PrivacyMode.Secret, safe=''),
|
||||
source=NotifyBurstSMS.quote(self.source, safe=''),
|
||||
targets='/'.join(
|
||||
[NotifyBurstSMS.quote(x, safe='') for x in self.targets]),
|
||||
params=NotifyBurstSMS.urlencode(params))
|
||||
|
||||
def __len__(self):
|
||||
"""
|
||||
Returns the number of targets associated with this notification
|
||||
"""
|
||||
#
|
||||
# Factor batch into calculation
|
||||
#
|
||||
batch_size = 1 if not self.batch else self.default_batch_size
|
||||
targets = len(self.targets)
|
||||
if batch_size > 1:
|
||||
targets = int(targets / batch_size) + \
|
||||
(1 if targets % batch_size else 0)
|
||||
|
||||
return targets if targets > 0 else 1
|
||||
|
||||
@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 hostname is our source (Sender ID)
|
||||
results['source'] = NotifyBurstSMS.unquote(results['host'])
|
||||
|
||||
# Get any remaining targets
|
||||
results['targets'] = NotifyBurstSMS.split_path(results['fullpath'])
|
||||
|
||||
# Get our account_side and auth_token from the user/pass config
|
||||
results['apikey'] = NotifyBurstSMS.unquote(results['user'])
|
||||
results['secret'] = NotifyBurstSMS.unquote(results['password'])
|
||||
|
||||
# API Key
|
||||
if 'key' in results['qsd'] and len(results['qsd']['key']):
|
||||
# Extract the API Key from an argument
|
||||
results['apikey'] = \
|
||||
NotifyBurstSMS.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'] = \
|
||||
NotifyBurstSMS.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'] = \
|
||||
NotifyBurstSMS.unquote(results['qsd']['from'])
|
||||
if 'source' in results['qsd'] and len(results['qsd']['source']):
|
||||
results['source'] = \
|
||||
NotifyBurstSMS.unquote(results['qsd']['source'])
|
||||
|
||||
# Support country
|
||||
if 'country' in results['qsd'] and len(results['qsd']['country']):
|
||||
results['country'] = \
|
||||
NotifyBurstSMS.unquote(results['qsd']['country'])
|
||||
|
||||
# Support validity value
|
||||
if 'validity' in results['qsd'] and len(results['qsd']['validity']):
|
||||
results['validity'] = \
|
||||
NotifyBurstSMS.unquote(results['qsd']['validity'])
|
||||
|
||||
# Get Batch Mode Flag
|
||||
if 'batch' in results['qsd'] and len(results['qsd']['batch']):
|
||||
results['batch'] = parse_bool(results['qsd']['batch'])
|
||||
|
||||
# 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'] += \
|
||||
NotifyBurstSMS.parse_phone_no(results['qsd']['to'])
|
||||
|
||||
return results
|
|
@ -1,5 +1,5 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# BSD 3-Clause License
|
||||
# BSD 2-Clause License
|
||||
#
|
||||
# Apprise - Push Notification Library.
|
||||
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
|
||||
|
@ -14,10 +14,6 @@
|
|||
# this list of conditions and the following disclaimer in the documentation
|
||||
# and/or other materials provided with the distribution.
|
||||
#
|
||||
# 3. Neither the name of the copyright holder nor the names of its
|
||||
# contributors may be used to endorse or promote products derived from
|
||||
# this software without specific prior written permission.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
|
@ -288,6 +284,21 @@ class NotifyClickSend(NotifyBase):
|
|||
params=NotifyClickSend.urlencode(params),
|
||||
)
|
||||
|
||||
def __len__(self):
|
||||
"""
|
||||
Returns the number of targets associated with this notification
|
||||
"""
|
||||
#
|
||||
# Factor batch into calculation
|
||||
#
|
||||
batch_size = 1 if not self.batch else self.default_batch_size
|
||||
targets = len(self.targets)
|
||||
if batch_size > 1:
|
||||
targets = int(targets / batch_size) + \
|
||||
(1 if targets % batch_size else 0)
|
||||
|
||||
return targets
|
||||
|
||||
@staticmethod
|
||||
def parse_url(url):
|
||||
"""
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# BSD 3-Clause License
|
||||
# BSD 2-Clause License
|
||||
#
|
||||
# Apprise - Push Notification Library.
|
||||
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
|
||||
|
@ -14,10 +14,6 @@
|
|||
# this list of conditions and the following disclaimer in the documentation
|
||||
# and/or other materials provided with the distribution.
|
||||
#
|
||||
# 3. Neither the name of the copyright holder nor the names of its
|
||||
# contributors may be used to endorse or promote products derived from
|
||||
# this software without specific prior written permission.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
|
@ -114,6 +110,7 @@ class NotifyD7Networks(NotifyBase):
|
|||
'targets': {
|
||||
'name': _('Targets'),
|
||||
'type': 'list:string',
|
||||
'required': True,
|
||||
},
|
||||
})
|
||||
|
||||
|
@ -357,6 +354,15 @@ class NotifyD7Networks(NotifyBase):
|
|||
[NotifyD7Networks.quote(x, safe='') for x in self.targets]),
|
||||
params=NotifyD7Networks.urlencode(params))
|
||||
|
||||
def __len__(self):
|
||||
"""
|
||||
Returns the number of targets associated with this notification
|
||||
"""
|
||||
#
|
||||
# Factor batch into calculation
|
||||
#
|
||||
return len(self.targets) if not self.batch else 1
|
||||
|
||||
@staticmethod
|
||||
def parse_url(url):
|
||||
"""
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# BSD 3-Clause License
|
||||
# BSD 2-Clause License
|
||||
#
|
||||
# Apprise - Push Notification Library.
|
||||
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
|
||||
|
@ -14,10 +14,6 @@
|
|||
# this list of conditions and the following disclaimer in the documentation
|
||||
# and/or other materials provided with the distribution.
|
||||
#
|
||||
# 3. Neither the name of the copyright holder nor the names of its
|
||||
# contributors may be used to endorse or promote products derived from
|
||||
# this software without specific prior written permission.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# BSD 3-Clause License
|
||||
# BSD 2-Clause License
|
||||
#
|
||||
# Apprise - Push Notification Library.
|
||||
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
|
||||
|
@ -14,10 +14,6 @@
|
|||
# this list of conditions and the following disclaimer in the documentation
|
||||
# and/or other materials provided with the distribution.
|
||||
#
|
||||
# 3. Neither the name of the copyright holder nor the names of its
|
||||
# contributors may be used to endorse or promote products derived from
|
||||
# this software without specific prior written permission.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
|
@ -350,6 +346,21 @@ class NotifyDapnet(NotifyBase):
|
|||
params=NotifyDapnet.urlencode(params),
|
||||
)
|
||||
|
||||
def __len__(self):
|
||||
"""
|
||||
Returns the number of targets associated with this notification
|
||||
"""
|
||||
#
|
||||
# Factor batch into calculation
|
||||
#
|
||||
batch_size = 1 if not self.batch else self.default_batch_size
|
||||
targets = len(self.targets)
|
||||
if batch_size > 1:
|
||||
targets = int(targets / batch_size) + \
|
||||
(1 if targets % batch_size else 0)
|
||||
|
||||
return targets
|
||||
|
||||
@staticmethod
|
||||
def parse_url(url):
|
||||
"""
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# BSD 3-Clause License
|
||||
# BSD 2-Clause License
|
||||
#
|
||||
# Apprise - Push Notification Library.
|
||||
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
|
||||
|
@ -14,10 +14,6 @@
|
|||
# this list of conditions and the following disclaimer in the documentation
|
||||
# and/or other materials provided with the distribution.
|
||||
#
|
||||
# 3. Neither the name of the copyright holder nor the names of its
|
||||
# contributors may be used to endorse or promote products derived from
|
||||
# this software without specific prior written permission.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
|
@ -103,13 +99,18 @@ class NotifyDingTalk(NotifyBase):
|
|||
'regex': (r'^[a-z0-9]+$', 'i'),
|
||||
},
|
||||
'secret': {
|
||||
'name': _('Token'),
|
||||
'name': _('Secret'),
|
||||
'type': 'string',
|
||||
'private': True,
|
||||
'regex': (r'^[a-z0-9]+$', 'i'),
|
||||
},
|
||||
'targets': {
|
||||
'target_phone_no': {
|
||||
'name': _('Target Phone No'),
|
||||
'type': 'string',
|
||||
'map_to': 'targets',
|
||||
},
|
||||
'targets': {
|
||||
'name': _('Targets'),
|
||||
'type': 'list:string',
|
||||
},
|
||||
})
|
||||
|
@ -309,6 +310,13 @@ class NotifyDingTalk(NotifyBase):
|
|||
[NotifyDingTalk.quote(x, safe='') for x in self.targets]),
|
||||
args=NotifyDingTalk.urlencode(args))
|
||||
|
||||
def __len__(self):
|
||||
"""
|
||||
Returns the number of targets associated with this notification
|
||||
"""
|
||||
targets = len(self.targets)
|
||||
return targets if targets > 0 else 1
|
||||
|
||||
@staticmethod
|
||||
def parse_url(url):
|
||||
"""
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# BSD 3-Clause License
|
||||
# BSD 2-Clause License
|
||||
#
|
||||
# Apprise - Push Notification Library.
|
||||
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
|
||||
|
@ -14,10 +14,6 @@
|
|||
# this list of conditions and the following disclaimer in the documentation
|
||||
# and/or other materials provided with the distribution.
|
||||
#
|
||||
# 3. Neither the name of the copyright holder nor the names of its
|
||||
# contributors may be used to endorse or promote products derived from
|
||||
# this software without specific prior written permission.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
|
@ -50,6 +46,9 @@
|
|||
import re
|
||||
import requests
|
||||
from json import dumps
|
||||
from datetime import timedelta
|
||||
from datetime import datetime
|
||||
from datetime import timezone
|
||||
|
||||
from .NotifyBase import NotifyBase
|
||||
from ..common import NotifyImageSize
|
||||
|
@ -81,9 +80,23 @@ class NotifyDiscord(NotifyBase):
|
|||
# Discord Webhook
|
||||
notify_url = 'https://discord.com/api/webhooks'
|
||||
|
||||
# Support attachments
|
||||
attachment_support = True
|
||||
|
||||
# Allows the user to specify the NotifyImageSize object
|
||||
image_size = NotifyImageSize.XY_256
|
||||
|
||||
# Discord is kind enough to return how many more requests we're allowed to
|
||||
# continue to make within it's header response as:
|
||||
# X-RateLimit-Reset: The epoc time (in seconds) we can expect our
|
||||
# rate-limit to be reset.
|
||||
# X-RateLimit-Remaining: an integer identifying how many requests we're
|
||||
# still allow to make.
|
||||
request_rate_per_sec = 0
|
||||
|
||||
# Taken right from google.auth.helpers:
|
||||
clock_skew = timedelta(seconds=10)
|
||||
|
||||
# The maximum allowable characters allowed in the body per message
|
||||
body_maxlen = 2000
|
||||
|
||||
|
@ -135,6 +148,13 @@ class NotifyDiscord(NotifyBase):
|
|||
'name': _('Avatar URL'),
|
||||
'type': 'string',
|
||||
},
|
||||
'href': {
|
||||
'name': _('URL'),
|
||||
'type': 'string',
|
||||
},
|
||||
'url': {
|
||||
'alias_of': 'href',
|
||||
},
|
||||
# Send a message to the specified thread within a webhook's channel.
|
||||
# The thread will automatically be unarchived.
|
||||
'thread': {
|
||||
|
@ -166,7 +186,8 @@ 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, thread=None, **kwargs):
|
||||
fields=True, avatar_url=None, href=None, thread=None,
|
||||
**kwargs):
|
||||
"""
|
||||
Initialize Discord Object
|
||||
|
||||
|
@ -215,6 +236,15 @@ class NotifyDiscord(NotifyBase):
|
|||
# dynamically generated avatar url images
|
||||
self.avatar_url = avatar_url
|
||||
|
||||
# A URL to have the title link to
|
||||
self.href = href
|
||||
|
||||
# For Tracking Purposes
|
||||
self.ratelimit_reset = datetime.now(timezone.utc).replace(tzinfo=None)
|
||||
|
||||
# Default to 1.0
|
||||
self.ratelimit_remaining = 1.0
|
||||
|
||||
return
|
||||
|
||||
def send(self, body, title='', notify_type=NotifyType.INFO, attach=None,
|
||||
|
@ -235,64 +265,6 @@ class NotifyDiscord(NotifyBase):
|
|||
# Acquire image_url
|
||||
image_url = self.image_url(notify_type)
|
||||
|
||||
# our fields variable
|
||||
fields = []
|
||||
|
||||
if self.notify_format == NotifyFormat.MARKDOWN:
|
||||
# Use embeds for payload
|
||||
payload['embeds'] = [{
|
||||
'author': {
|
||||
'name': self.app_id,
|
||||
'url': self.app_url,
|
||||
},
|
||||
'title': title,
|
||||
'description': body,
|
||||
|
||||
# Our color associated with our notification
|
||||
'color': self.color(notify_type, int),
|
||||
}]
|
||||
|
||||
if self.footer:
|
||||
# Acquire logo URL
|
||||
logo_url = self.image_url(notify_type, logo=True)
|
||||
|
||||
# Set Footer text to our app description
|
||||
payload['embeds'][0]['footer'] = {
|
||||
'text': self.app_desc,
|
||||
}
|
||||
|
||||
if self.footer_logo and logo_url:
|
||||
payload['embeds'][0]['footer']['icon_url'] = logo_url
|
||||
|
||||
if self.include_image and image_url:
|
||||
payload['embeds'][0]['thumbnail'] = {
|
||||
'url': image_url,
|
||||
'height': 256,
|
||||
'width': 256,
|
||||
}
|
||||
|
||||
if self.fields:
|
||||
# Break titles out so that we can sort them in embeds
|
||||
description, fields = self.extract_markdown_sections(body)
|
||||
|
||||
# Swap first entry for description
|
||||
payload['embeds'][0]['description'] = description
|
||||
if fields:
|
||||
# Apply our additional parsing for a better presentation
|
||||
payload['embeds'][0]['fields'] = \
|
||||
fields[:self.discord_max_fields]
|
||||
|
||||
# Remove entry from head of fields
|
||||
fields = fields[self.discord_max_fields:]
|
||||
|
||||
else:
|
||||
# not markdown
|
||||
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
|
||||
|
@ -301,21 +273,84 @@ class NotifyDiscord(NotifyBase):
|
|||
# Optionally override the default username of the webhook
|
||||
payload['username'] = self.user
|
||||
|
||||
if not self._send(payload):
|
||||
# We failed to post our message
|
||||
return False
|
||||
# Associate our thread_id with our message
|
||||
params = {'thread_id': self.thread_id} if self.thread_id else None
|
||||
|
||||
# Process any remaining fields IF set
|
||||
if fields:
|
||||
payload['embeds'][0]['description'] = ''
|
||||
for i in range(0, len(fields), self.discord_max_fields):
|
||||
payload['embeds'][0]['fields'] = \
|
||||
fields[i:i + self.discord_max_fields]
|
||||
if not self._send(payload):
|
||||
# We failed to post our message
|
||||
return False
|
||||
if body:
|
||||
# our fields variable
|
||||
fields = []
|
||||
|
||||
if attach:
|
||||
if self.notify_format == NotifyFormat.MARKDOWN:
|
||||
# Use embeds for payload
|
||||
payload['embeds'] = [{
|
||||
'author': {
|
||||
'name': self.app_id,
|
||||
'url': self.app_url,
|
||||
},
|
||||
'title': title,
|
||||
'description': body,
|
||||
|
||||
# Our color associated with our notification
|
||||
'color': self.color(notify_type, int),
|
||||
}]
|
||||
|
||||
if self.href:
|
||||
payload['embeds'][0]['url'] = self.href
|
||||
|
||||
if self.footer:
|
||||
# Acquire logo URL
|
||||
logo_url = self.image_url(notify_type, logo=True)
|
||||
|
||||
# Set Footer text to our app description
|
||||
payload['embeds'][0]['footer'] = {
|
||||
'text': self.app_desc,
|
||||
}
|
||||
|
||||
if self.footer_logo and logo_url:
|
||||
payload['embeds'][0]['footer']['icon_url'] = logo_url
|
||||
|
||||
if self.include_image and image_url:
|
||||
payload['embeds'][0]['thumbnail'] = {
|
||||
'url': image_url,
|
||||
'height': 256,
|
||||
'width': 256,
|
||||
}
|
||||
|
||||
if self.fields:
|
||||
# Break titles out so that we can sort them in embeds
|
||||
description, fields = self.extract_markdown_sections(body)
|
||||
|
||||
# Swap first entry for description
|
||||
payload['embeds'][0]['description'] = description
|
||||
if fields:
|
||||
# Apply our additional parsing for a better
|
||||
# presentation
|
||||
payload['embeds'][0]['fields'] = \
|
||||
fields[:self.discord_max_fields]
|
||||
|
||||
# Remove entry from head of fields
|
||||
fields = fields[self.discord_max_fields:]
|
||||
|
||||
else:
|
||||
# not markdown
|
||||
payload['content'] = \
|
||||
body if not title else "{}\r\n{}".format(title, body)
|
||||
|
||||
if not self._send(payload, params=params):
|
||||
# We failed to post our message
|
||||
return False
|
||||
|
||||
# Process any remaining fields IF set
|
||||
if fields:
|
||||
payload['embeds'][0]['description'] = ''
|
||||
for i in range(0, len(fields), self.discord_max_fields):
|
||||
payload['embeds'][0]['fields'] = \
|
||||
fields[i:i + self.discord_max_fields]
|
||||
if not self._send(payload):
|
||||
# We failed to post our message
|
||||
return False
|
||||
|
||||
if attach and self.attachment_support:
|
||||
# Update our payload; the idea is to preserve it's other detected
|
||||
# and assigned values for re-use here too
|
||||
payload.update({
|
||||
|
@ -338,14 +373,15 @@ class NotifyDiscord(NotifyBase):
|
|||
for attachment in attach:
|
||||
self.logger.info(
|
||||
'Posting Discord Attachment {}'.format(attachment.name))
|
||||
if not self._send(payload, attach=attachment):
|
||||
if not self._send(payload, params=params, attach=attachment):
|
||||
# We failed to post our message
|
||||
return False
|
||||
|
||||
# Otherwise return
|
||||
return True
|
||||
|
||||
def _send(self, payload, attach=None, **kwargs):
|
||||
def _send(self, payload, attach=None, params=None, rate_limit=1,
|
||||
**kwargs):
|
||||
"""
|
||||
Wrapper to the requests (post) object
|
||||
"""
|
||||
|
@ -367,8 +403,25 @@ class NotifyDiscord(NotifyBase):
|
|||
))
|
||||
self.logger.debug('Discord Payload: %s' % str(payload))
|
||||
|
||||
# Always call throttle before any remote server i/o is made
|
||||
self.throttle()
|
||||
# By default set wait to None
|
||||
wait = None
|
||||
|
||||
if self.ratelimit_remaining <= 0.0:
|
||||
# Determine how long we should wait for or if we should wait at
|
||||
# all. This isn't fool-proof because we can't be sure the client
|
||||
# time (calling this script) is completely synced up with the
|
||||
# Discord server. One would hope we're on NTP and our clocks are
|
||||
# the same allowing this to role smoothly:
|
||||
|
||||
now = datetime.now(timezone.utc).replace(tzinfo=None)
|
||||
if now < self.ratelimit_reset:
|
||||
# We need to throttle for the difference in seconds
|
||||
wait = abs(
|
||||
(self.ratelimit_reset - now + self.clock_skew)
|
||||
.total_seconds())
|
||||
|
||||
# Always call throttle before any remote server i/o is made;
|
||||
self.throttle(wait=wait)
|
||||
|
||||
# Perform some simple error checking
|
||||
if isinstance(attach, AttachBase):
|
||||
|
@ -396,12 +449,29 @@ class NotifyDiscord(NotifyBase):
|
|||
|
||||
r = requests.post(
|
||||
notify_url,
|
||||
params=params,
|
||||
data=payload if files else dumps(payload),
|
||||
headers=headers,
|
||||
files=files,
|
||||
verify=self.verify_certificate,
|
||||
timeout=self.request_timeout,
|
||||
)
|
||||
|
||||
# Handle rate limiting (if specified)
|
||||
try:
|
||||
# Store our rate limiting (if provided)
|
||||
self.ratelimit_remaining = \
|
||||
float(r.headers.get(
|
||||
'X-RateLimit-Remaining'))
|
||||
self.ratelimit_reset = datetime.fromtimestamp(
|
||||
int(r.headers.get('X-RateLimit-Reset')),
|
||||
timezone.utc).replace(tzinfo=None)
|
||||
|
||||
except (TypeError, ValueError):
|
||||
# This is returned if we could not retrieve this
|
||||
# information gracefully accept this state and move on
|
||||
pass
|
||||
|
||||
if r.status_code not in (
|
||||
requests.codes.ok, requests.codes.no_content):
|
||||
|
||||
|
@ -409,6 +479,20 @@ class NotifyDiscord(NotifyBase):
|
|||
status_str = \
|
||||
NotifyBase.http_response_code_lookup(r.status_code)
|
||||
|
||||
if r.status_code == requests.codes.too_many_requests \
|
||||
and rate_limit > 0:
|
||||
|
||||
# handle rate limiting
|
||||
self.logger.warning(
|
||||
'Discord rate limiting in effect; '
|
||||
'blocking for %.2f second(s)',
|
||||
self.ratelimit_remaining)
|
||||
|
||||
# Try one more time before failing
|
||||
return self._send(
|
||||
payload=payload, attach=attach, params=params,
|
||||
rate_limit=rate_limit - 1, **kwargs)
|
||||
|
||||
self.logger.warning(
|
||||
'Failed to send {}to Discord notification: '
|
||||
'{}{}error={}.'.format(
|
||||
|
@ -466,6 +550,9 @@ class NotifyDiscord(NotifyBase):
|
|||
if self.avatar_url:
|
||||
params['avatar_url'] = self.avatar_url
|
||||
|
||||
if self.href:
|
||||
params['href'] = self.href
|
||||
|
||||
if self.thread_id:
|
||||
params['thread'] = self.thread_id
|
||||
|
||||
|
@ -537,10 +624,23 @@ class NotifyDiscord(NotifyBase):
|
|||
results['avatar_url'] = \
|
||||
NotifyDiscord.unquote(results['qsd']['avatar_url'])
|
||||
|
||||
# Extract url if it was specified
|
||||
if 'href' in results['qsd']:
|
||||
results['href'] = \
|
||||
NotifyDiscord.unquote(results['qsd']['href'])
|
||||
|
||||
elif 'url' in results['qsd']:
|
||||
results['href'] = \
|
||||
NotifyDiscord.unquote(results['qsd']['url'])
|
||||
# Markdown is implied
|
||||
results['format'] = NotifyFormat.MARKDOWN
|
||||
|
||||
# Extract thread id if it was specified
|
||||
if 'thread' in results['qsd']:
|
||||
results['thread'] = \
|
||||
NotifyDiscord.unquote(results['qsd']['thread'])
|
||||
# Markdown is implied
|
||||
results['format'] = NotifyFormat.MARKDOWN
|
||||
|
||||
return results
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# BSD 3-Clause License
|
||||
# BSD 2-Clause License
|
||||
#
|
||||
# Apprise - Push Notification Library.
|
||||
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
|
||||
|
@ -14,10 +14,6 @@
|
|||
# this list of conditions and the following disclaimer in the documentation
|
||||
# and/or other materials provided with the distribution.
|
||||
#
|
||||
# 3. Neither the name of the copyright holder nor the names of its
|
||||
# contributors may be used to endorse or promote products derived from
|
||||
# this software without specific prior written permission.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
|
@ -43,6 +39,7 @@ from email import charset
|
|||
|
||||
from socket import error as SocketError
|
||||
from datetime import datetime
|
||||
from datetime import timezone
|
||||
|
||||
from .NotifyBase import NotifyBase
|
||||
from ..URLBase import PrivacyMode
|
||||
|
@ -340,6 +337,9 @@ class NotifyEmail(NotifyBase):
|
|||
# A URL that takes you to the setup/help of the specific protocol
|
||||
setup_url = 'https://github.com/caronc/apprise/wiki/Notify_email'
|
||||
|
||||
# Support attachments
|
||||
attachment_support = True
|
||||
|
||||
# Default Notify Format
|
||||
notify_format = NotifyFormat.HTML
|
||||
|
||||
|
@ -384,8 +384,13 @@ class NotifyEmail(NotifyBase):
|
|||
'min': 1,
|
||||
'max': 65535,
|
||||
},
|
||||
'target_email': {
|
||||
'name': _('Target Email'),
|
||||
'type': 'string',
|
||||
'map_to': 'targets',
|
||||
},
|
||||
'targets': {
|
||||
'name': _('Target Emails'),
|
||||
'name': _('Targets'),
|
||||
'type': 'list:string',
|
||||
},
|
||||
})
|
||||
|
@ -764,7 +769,7 @@ class NotifyEmail(NotifyBase):
|
|||
else:
|
||||
base = MIMEText(body, 'plain', 'utf-8')
|
||||
|
||||
if attach:
|
||||
if attach and self.attachment_support:
|
||||
mixed = MIMEMultipart("mixed")
|
||||
mixed.attach(base)
|
||||
# Now store our attachments
|
||||
|
@ -805,7 +810,8 @@ class NotifyEmail(NotifyBase):
|
|||
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")
|
||||
datetime.now(timezone.utc)\
|
||||
.strftime("%a, %d %b %Y %H:%M:%S +0000")
|
||||
base['X-Application'] = self.app_id
|
||||
|
||||
if cc:
|
||||
|
@ -999,6 +1005,13 @@ class NotifyEmail(NotifyBase):
|
|||
params=NotifyEmail.urlencode(params),
|
||||
)
|
||||
|
||||
def __len__(self):
|
||||
"""
|
||||
Returns the number of targets associated with this notification
|
||||
"""
|
||||
targets = len(self.targets)
|
||||
return targets if targets > 0 else 1
|
||||
|
||||
@staticmethod
|
||||
def parse_url(url):
|
||||
"""
|
||||
|
@ -1023,6 +1036,10 @@ class NotifyEmail(NotifyBase):
|
|||
# add one to ourselves
|
||||
results['targets'] = NotifyEmail.split_path(results['fullpath'])
|
||||
|
||||
# Attempt to detect 'to' email address
|
||||
if 'to' in results['qsd'] and len(results['qsd']['to']):
|
||||
results['targets'].append(results['qsd']['to'])
|
||||
|
||||
# Attempt to detect 'from' email address
|
||||
if 'from' in results['qsd'] and len(results['qsd']['from']):
|
||||
from_addr = NotifyEmail.unquote(results['qsd']['from'])
|
||||
|
@ -1041,10 +1058,6 @@ class NotifyEmail(NotifyBase):
|
|||
# Extract from name to associate with from address
|
||||
from_addr = NotifyEmail.unquote(results['qsd']['name'])
|
||||
|
||||
# Attempt to detect 'to' email address
|
||||
if 'to' in results['qsd'] and len(results['qsd']['to']):
|
||||
results['targets'].append(results['qsd']['to'])
|
||||
|
||||
# Store SMTP Host if specified
|
||||
if 'smtp' in results['qsd'] and len(results['qsd']['smtp']):
|
||||
# Extract the smtp server
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# BSD 3-Clause License
|
||||
# BSD 2-Clause License
|
||||
#
|
||||
# Apprise - Push Notification Library.
|
||||
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
|
||||
|
@ -14,10 +14,6 @@
|
|||
# this list of conditions and the following disclaimer in the documentation
|
||||
# and/or other materials provided with the distribution.
|
||||
#
|
||||
# 3. Neither the name of the copyright holder nor the names of its
|
||||
# contributors may be used to endorse or promote products derived from
|
||||
# this software without specific prior written permission.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# BSD 3-Clause License
|
||||
# BSD 2-Clause License
|
||||
#
|
||||
# Apprise - Push Notification Library.
|
||||
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
|
||||
|
@ -14,10 +14,6 @@
|
|||
# this list of conditions and the following disclaimer in the documentation
|
||||
# and/or other materials provided with the distribution.
|
||||
#
|
||||
# 3. Neither the name of the copyright holder nor the names of its
|
||||
# contributors may be used to endorse or promote products derived from
|
||||
# this software without specific prior written permission.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# BSD 3-Clause License
|
||||
# BSD 2-Clause License
|
||||
#
|
||||
# Apprise - Push Notification Library.
|
||||
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
|
||||
|
@ -14,10 +14,6 @@
|
|||
# this list of conditions and the following disclaimer in the documentation
|
||||
# and/or other materials provided with the distribution.
|
||||
#
|
||||
# 3. Neither the name of the copyright holder nor the names of its
|
||||
# contributors may be used to endorse or promote products derived from
|
||||
# this software without specific prior written permission.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
|
@ -157,7 +153,6 @@ class NotifyFCM(NotifyBase):
|
|||
'project': {
|
||||
'name': _('Project ID'),
|
||||
'type': 'string',
|
||||
'required': True,
|
||||
},
|
||||
'target_device': {
|
||||
'name': _('Target Device'),
|
||||
|
@ -173,6 +168,7 @@ class NotifyFCM(NotifyBase):
|
|||
'targets': {
|
||||
'name': _('Targets'),
|
||||
'type': 'list:string',
|
||||
'required': True,
|
||||
},
|
||||
})
|
||||
|
||||
|
@ -555,6 +551,12 @@ class NotifyFCM(NotifyBase):
|
|||
params=NotifyFCM.urlencode(params),
|
||||
)
|
||||
|
||||
def __len__(self):
|
||||
"""
|
||||
Returns the number of targets associated with this notification
|
||||
"""
|
||||
return len(self.targets)
|
||||
|
||||
@staticmethod
|
||||
def parse_url(url):
|
||||
"""
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# BSD 3-Clause License
|
||||
# BSD 2-Clause License
|
||||
#
|
||||
# Apprise - Push Notification Library.
|
||||
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
|
||||
|
@ -14,10 +14,6 @@
|
|||
# this list of conditions and the following disclaimer in the documentation
|
||||
# and/or other materials provided with the distribution.
|
||||
#
|
||||
# 3. Neither the name of the copyright holder nor the names of its
|
||||
# contributors may be used to endorse or promote products derived from
|
||||
# this software without specific prior written permission.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# BSD 3-Clause License
|
||||
# BSD 2-Clause License
|
||||
#
|
||||
# Apprise - Push Notification Library.
|
||||
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
|
||||
|
@ -14,10 +14,6 @@
|
|||
# this list of conditions and the following disclaimer in the documentation
|
||||
# and/or other materials provided with the distribution.
|
||||
#
|
||||
# 3. Neither the name of the copyright holder nor the names of its
|
||||
# contributors may be used to endorse or promote products derived from
|
||||
# this software without specific prior written permission.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# BSD 3-Clause License
|
||||
# BSD 2-Clause License
|
||||
#
|
||||
# Apprise - Push Notification Library.
|
||||
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
|
||||
|
@ -14,10 +14,6 @@
|
|||
# this list of conditions and the following disclaimer in the documentation
|
||||
# and/or other materials provided with the distribution.
|
||||
#
|
||||
# 3. Neither the name of the copyright holder nor the names of its
|
||||
# contributors may be used to endorse or promote products derived from
|
||||
# this software without specific prior written permission.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
|
@ -47,6 +43,7 @@ from cryptography.hazmat.primitives import asymmetric
|
|||
from cryptography.exceptions import UnsupportedAlgorithm
|
||||
from datetime import datetime
|
||||
from datetime import timedelta
|
||||
from datetime import timezone
|
||||
from json.decoder import JSONDecodeError
|
||||
from urllib.parse import urlencode as _urlencode
|
||||
|
||||
|
@ -106,7 +103,7 @@ class GoogleOAuth:
|
|||
# Our keys we build using the provided content
|
||||
self.__refresh_token = None
|
||||
self.__access_token = None
|
||||
self.__access_token_expiry = datetime.utcnow()
|
||||
self.__access_token_expiry = datetime.now(timezone.utc)
|
||||
|
||||
def load(self, path):
|
||||
"""
|
||||
|
@ -117,7 +114,7 @@ class GoogleOAuth:
|
|||
self.content = None
|
||||
self.private_key = None
|
||||
self.__access_token = None
|
||||
self.__access_token_expiry = datetime.utcnow()
|
||||
self.__access_token_expiry = datetime.now(timezone.utc)
|
||||
|
||||
try:
|
||||
with open(path, mode="r", encoding=self.encoding) as fp:
|
||||
|
@ -199,7 +196,7 @@ class GoogleOAuth:
|
|||
'token with.')
|
||||
return None
|
||||
|
||||
if self.__access_token_expiry > datetime.utcnow():
|
||||
if self.__access_token_expiry > datetime.now(timezone.utc):
|
||||
# Return our no-expired key
|
||||
return self.__access_token
|
||||
|
||||
|
@ -209,7 +206,7 @@ class GoogleOAuth:
|
|||
key_identifier = self.content.get('private_key_id')
|
||||
|
||||
# Generate our Assertion
|
||||
now = datetime.utcnow()
|
||||
now = datetime.now(timezone.utc)
|
||||
expiry = now + self.access_token_lifetime_sec
|
||||
|
||||
payload = {
|
||||
|
@ -301,7 +298,7 @@ class GoogleOAuth:
|
|||
if 'expires_in' in response:
|
||||
delta = timedelta(seconds=int(response['expires_in']))
|
||||
self.__access_token_expiry = \
|
||||
delta + datetime.utcnow() - self.clock_skew
|
||||
delta + datetime.now(timezone.utc) - self.clock_skew
|
||||
|
||||
else:
|
||||
# Allow some grace before we expire
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# BSD 3-Clause License
|
||||
# BSD 2-Clause License
|
||||
#
|
||||
# Apprise - Push Notification Library.
|
||||
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
|
||||
|
@ -14,10 +14,6 @@
|
|||
# this list of conditions and the following disclaimer in the documentation
|
||||
# and/or other materials provided with the distribution.
|
||||
#
|
||||
# 3. Neither the name of the copyright holder nor the names of its
|
||||
# contributors may be used to endorse or promote products derived from
|
||||
# this software without specific prior written permission.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# BSD 3-Clause License
|
||||
# BSD 2-Clause License
|
||||
#
|
||||
# Apprise - Push Notification Library.
|
||||
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
|
||||
|
@ -14,10 +14,6 @@
|
|||
# this list of conditions and the following disclaimer in the documentation
|
||||
# and/or other materials provided with the distribution.
|
||||
#
|
||||
# 3. Neither the name of the copyright holder nor the names of its
|
||||
# contributors may be used to endorse or promote products derived from
|
||||
# this software without specific prior written permission.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# BSD 3-Clause License
|
||||
# BSD 2-Clause License
|
||||
#
|
||||
# Apprise - Push Notification Library.
|
||||
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
|
||||
|
@ -14,10 +14,6 @@
|
|||
# this list of conditions and the following disclaimer in the documentation
|
||||
# and/or other materials provided with the distribution.
|
||||
#
|
||||
# 3. Neither the name of the copyright holder nor the names of its
|
||||
# contributors may be used to endorse or promote products derived from
|
||||
# this software without specific prior written permission.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
|
@ -97,8 +93,8 @@ class NotifyFlock(NotifyBase):
|
|||
# Define object templates
|
||||
templates = (
|
||||
'{schema}://{token}',
|
||||
'{schema}://{user}@{token}',
|
||||
'{schema}://{user}@{token}/{targets}',
|
||||
'{schema}://{botname}@{token}',
|
||||
'{schema}://{botname}@{token}/{targets}',
|
||||
'{schema}://{token}/{targets}',
|
||||
)
|
||||
|
||||
|
@ -111,9 +107,10 @@ class NotifyFlock(NotifyBase):
|
|||
'private': True,
|
||||
'required': True,
|
||||
},
|
||||
'user': {
|
||||
'botname': {
|
||||
'name': _('Bot Name'),
|
||||
'type': 'string',
|
||||
'map_to': 'user',
|
||||
},
|
||||
'to_user': {
|
||||
'name': _('To User ID'),
|
||||
|
@ -334,6 +331,13 @@ class NotifyFlock(NotifyBase):
|
|||
params=NotifyFlock.urlencode(params),
|
||||
)
|
||||
|
||||
def __len__(self):
|
||||
"""
|
||||
Returns the number of targets associated with this notification
|
||||
"""
|
||||
targets = len(self.targets)
|
||||
return targets if targets > 0 else 1
|
||||
|
||||
@staticmethod
|
||||
def parse_url(url):
|
||||
"""
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# BSD 3-Clause License
|
||||
# BSD 2-Clause License
|
||||
#
|
||||
# Apprise - Push Notification Library.
|
||||
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
|
||||
|
@ -14,10 +14,6 @@
|
|||
# this list of conditions and the following disclaimer in the documentation
|
||||
# and/or other materials provided with the distribution.
|
||||
#
|
||||
# 3. Neither the name of the copyright holder nor the names of its
|
||||
# contributors may be used to endorse or promote products derived from
|
||||
# this software without specific prior written permission.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
|
@ -40,6 +36,16 @@ from ..common import NotifyType
|
|||
from ..AppriseLocale import gettext_lazy as _
|
||||
|
||||
|
||||
class FORMPayloadField:
|
||||
"""
|
||||
Identifies the fields available in the FORM Payload
|
||||
"""
|
||||
VERSION = 'version'
|
||||
TITLE = 'title'
|
||||
MESSAGE = 'message'
|
||||
MESSAGETYPE = 'type'
|
||||
|
||||
|
||||
# Defines the method to send the notification
|
||||
METHODS = (
|
||||
'POST',
|
||||
|
@ -89,6 +95,9 @@ class NotifyForm(NotifyBase):
|
|||
# A URL that takes you to the setup/help of the specific protocol
|
||||
setup_url = 'https://github.com/caronc/apprise/wiki/Notify_Custom_Form'
|
||||
|
||||
# Support attachments
|
||||
attachment_support = True
|
||||
|
||||
# Allows the user to specify the NotifyImageSize object
|
||||
image_size = NotifyImageSize.XY_128
|
||||
|
||||
|
@ -96,6 +105,12 @@ class NotifyForm(NotifyBase):
|
|||
# local anyway
|
||||
request_rate_per_sec = 0
|
||||
|
||||
# Define the FORM version to place in all payloads
|
||||
# Version: Major.Minor, Major is only updated if the entire schema is
|
||||
# changed. If just adding new items (or removing old ones, only increment
|
||||
# the Minor!
|
||||
form_version = '1.0'
|
||||
|
||||
# Define object templates
|
||||
templates = (
|
||||
'{schema}://{host}',
|
||||
|
@ -218,6 +233,18 @@ class NotifyForm(NotifyBase):
|
|||
self.attach_as += self.attach_as_count
|
||||
self.attach_multi_support = True
|
||||
|
||||
# A payload map allows users to over-ride the default mapping if
|
||||
# they're detected with the :overide=value. Normally this would
|
||||
# create a new key and assign it the value specified. However
|
||||
# if the key you specify is actually an internally mapped one,
|
||||
# then a re-mapping takes place using the value
|
||||
self.payload_map = {
|
||||
FORMPayloadField.VERSION: FORMPayloadField.VERSION,
|
||||
FORMPayloadField.TITLE: FORMPayloadField.TITLE,
|
||||
FORMPayloadField.MESSAGE: FORMPayloadField.MESSAGE,
|
||||
FORMPayloadField.MESSAGETYPE: FORMPayloadField.MESSAGETYPE,
|
||||
}
|
||||
|
||||
self.params = {}
|
||||
if params:
|
||||
# Store our extra headers
|
||||
|
@ -228,10 +255,20 @@ class NotifyForm(NotifyBase):
|
|||
# Store our extra headers
|
||||
self.headers.update(headers)
|
||||
|
||||
self.payload_overrides = {}
|
||||
self.payload_extras = {}
|
||||
if payload:
|
||||
# Store our extra payload entries
|
||||
self.payload_extras.update(payload)
|
||||
for key in list(self.payload_extras.keys()):
|
||||
# Any values set in the payload to alter a system related one
|
||||
# alters the system key. Hence :message=msg maps the 'message'
|
||||
# variable that otherwise already contains the payload to be
|
||||
# 'msg' instead (containing the payload)
|
||||
if key in self.payload_map:
|
||||
self.payload_map[key] = self.payload_extras[key]
|
||||
self.payload_overrides[key] = self.payload_extras[key]
|
||||
del self.payload_extras[key]
|
||||
|
||||
return
|
||||
|
||||
|
@ -257,6 +294,8 @@ class NotifyForm(NotifyBase):
|
|||
# Append our payload extra's into our parameters
|
||||
params.update(
|
||||
{':{}'.format(k): v for k, v in self.payload_extras.items()})
|
||||
params.update(
|
||||
{':{}'.format(k): v for k, v in self.payload_overrides.items()})
|
||||
|
||||
if self.attach_as != self.attach_as_default:
|
||||
# Provide Attach-As extension details
|
||||
|
@ -305,7 +344,7 @@ class NotifyForm(NotifyBase):
|
|||
|
||||
# Track our potential attachments
|
||||
files = []
|
||||
if attach:
|
||||
if attach and self.attachment_support:
|
||||
for no, attachment in enumerate(attach, start=1):
|
||||
# Perform some simple error checking
|
||||
if not attachment:
|
||||
|
@ -337,15 +376,18 @@ class NotifyForm(NotifyBase):
|
|||
'form:// Multi-Attachment Support not enabled')
|
||||
|
||||
# prepare Form Object
|
||||
payload = {
|
||||
# Version: Major.Minor, Major is only updated if the entire
|
||||
# schema is changed. If just adding new items (or removing
|
||||
# old ones, only increment the Minor!
|
||||
'version': '1.0',
|
||||
'title': title,
|
||||
'message': body,
|
||||
'type': notify_type,
|
||||
}
|
||||
payload = {}
|
||||
|
||||
for key, value in (
|
||||
(FORMPayloadField.VERSION, self.form_version),
|
||||
(FORMPayloadField.TITLE, title),
|
||||
(FORMPayloadField.MESSAGE, body),
|
||||
(FORMPayloadField.MESSAGETYPE, notify_type)):
|
||||
|
||||
if not self.payload_map[key]:
|
||||
# Do not store element in payload response
|
||||
continue
|
||||
payload[self.payload_map[key]] = value
|
||||
|
||||
# Apply any/all payload over-rides defined
|
||||
payload.update(self.payload_extras)
|
||||
|
|
|
@ -1,419 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# BSD 3-Clause License
|
||||
#
|
||||
# Apprise - Push Notification Library.
|
||||
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
|
||||
#
|
||||
# Redistribution and use in source and binary forms, with or without
|
||||
# modification, are permitted provided that the following conditions are met:
|
||||
#
|
||||
# 1. Redistributions of source code must retain the above copyright notice,
|
||||
# this list of conditions and the following disclaimer.
|
||||
#
|
||||
# 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
# this list of conditions and the following disclaimer in the documentation
|
||||
# and/or other materials provided with the distribution.
|
||||
#
|
||||
# 3. Neither the name of the copyright holder nor the names of its
|
||||
# contributors may be used to endorse or promote products derived from
|
||||
# this software without specific prior written permission.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
|
||||
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
||||
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
# POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
# Once you visit: https://developer.gitter.im/apps you'll get a personal
|
||||
# access token that will look something like this:
|
||||
# b5647881d563fm846dfbb2c27d1fe8f669b8f026
|
||||
|
||||
# Don't worry about generating an app; this token is all you need to form
|
||||
# you're URL with. The syntax is as follows:
|
||||
# gitter://{token}/{channel}
|
||||
|
||||
# Hence a URL might look like the following:
|
||||
# gitter://b5647881d563fm846dfbb2c27d1fe8f669b8f026/apprise
|
||||
|
||||
# Note: You must have joined the channel to send a message to it!
|
||||
|
||||
# Official API reference: https://developer.gitter.im/docs/user-resource
|
||||
|
||||
import re
|
||||
import requests
|
||||
from json import loads
|
||||
from json import dumps
|
||||
from datetime import datetime
|
||||
|
||||
from .NotifyBase import NotifyBase
|
||||
from ..common import NotifyImageSize
|
||||
from ..common import NotifyFormat
|
||||
from ..common import NotifyType
|
||||
from ..utils import parse_list
|
||||
from ..utils import parse_bool
|
||||
from ..utils import validate_regex
|
||||
from ..AppriseLocale import gettext_lazy as _
|
||||
|
||||
# API Gitter URL
|
||||
GITTER_API_URL = 'https://api.gitter.im/v1'
|
||||
|
||||
# Used to break path apart into list of targets
|
||||
TARGET_LIST_DELIM = re.compile(r'[ \t\r\n,\\/]+')
|
||||
|
||||
|
||||
class NotifyGitter(NotifyBase):
|
||||
"""
|
||||
A wrapper for Gitter Notifications
|
||||
"""
|
||||
|
||||
# The default descriptive name associated with the Notification
|
||||
service_name = 'Gitter'
|
||||
|
||||
# The services URL
|
||||
service_url = 'https://gitter.im/'
|
||||
|
||||
# All notification requests are secure
|
||||
secure_protocol = 'gitter'
|
||||
|
||||
# A URL that takes you to the setup/help of the specific protocol
|
||||
setup_url = 'https://github.com/caronc/apprise/wiki/Notify_gitter'
|
||||
|
||||
# Allows the user to specify the NotifyImageSize object
|
||||
image_size = NotifyImageSize.XY_32
|
||||
|
||||
# Gitter does not support a title
|
||||
title_maxlen = 0
|
||||
|
||||
# Gitter is kind enough to return how many more requests we're allowed to
|
||||
# continue to make within it's header response as:
|
||||
# X-RateLimit-Reset: The epoc time (in seconds) we can expect our
|
||||
# rate-limit to be reset.
|
||||
# X-RateLimit-Remaining: an integer identifying how many requests we're
|
||||
# still allow to make.
|
||||
request_rate_per_sec = 0
|
||||
|
||||
# For Tracking Purposes
|
||||
ratelimit_reset = datetime.utcnow()
|
||||
|
||||
# Default to 1
|
||||
ratelimit_remaining = 1
|
||||
|
||||
# Default Notification Format
|
||||
notify_format = NotifyFormat.MARKDOWN
|
||||
|
||||
# Define object templates
|
||||
templates = (
|
||||
'{schema}://{token}/{targets}/',
|
||||
)
|
||||
|
||||
# Define our template tokens
|
||||
template_tokens = dict(NotifyBase.template_tokens, **{
|
||||
'token': {
|
||||
'name': _('Token'),
|
||||
'type': 'string',
|
||||
'private': True,
|
||||
'required': True,
|
||||
'regex': (r'^[a-z0-9]{40}$', 'i'),
|
||||
},
|
||||
'targets': {
|
||||
'name': _('Rooms'),
|
||||
'type': 'list:string',
|
||||
},
|
||||
})
|
||||
|
||||
# Define our template arguments
|
||||
template_args = dict(NotifyBase.template_args, **{
|
||||
'image': {
|
||||
'name': _('Include Image'),
|
||||
'type': 'bool',
|
||||
'default': False,
|
||||
'map_to': 'include_image',
|
||||
},
|
||||
'to': {
|
||||
'alias_of': 'targets',
|
||||
},
|
||||
})
|
||||
|
||||
def __init__(self, token, targets, include_image=False, **kwargs):
|
||||
"""
|
||||
Initialize Gitter Object
|
||||
"""
|
||||
super().__init__(**kwargs)
|
||||
|
||||
# Secret Key (associated with project)
|
||||
self.token = validate_regex(
|
||||
token, *self.template_tokens['token']['regex'])
|
||||
if not self.token:
|
||||
msg = 'An invalid Gitter API Token ' \
|
||||
'({}) was specified.'.format(token)
|
||||
self.logger.warning(msg)
|
||||
raise TypeError(msg)
|
||||
|
||||
# Parse our targets
|
||||
self.targets = parse_list(targets)
|
||||
if not self.targets:
|
||||
msg = 'There are no valid Gitter targets to notify.'
|
||||
self.logger.warning(msg)
|
||||
raise TypeError(msg)
|
||||
|
||||
# Used to track maping of rooms to their numeric id lookup for
|
||||
# messaging
|
||||
self._room_mapping = None
|
||||
|
||||
# Track whether or not we want to send an image with our notification
|
||||
# or not.
|
||||
self.include_image = include_image
|
||||
|
||||
return
|
||||
|
||||
def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs):
|
||||
"""
|
||||
Perform Gitter Notification
|
||||
"""
|
||||
|
||||
# error tracking (used for function return)
|
||||
has_error = False
|
||||
|
||||
# Set up our image for display if configured to do so
|
||||
image_url = None if not self.include_image \
|
||||
else self.image_url(notify_type)
|
||||
|
||||
if image_url:
|
||||
body = '![alt]({})\n{}'.format(image_url, body)
|
||||
|
||||
if self._room_mapping is None:
|
||||
# Populate our room mapping
|
||||
self._room_mapping = {}
|
||||
postokay, response = self._fetch(url='rooms')
|
||||
if not postokay:
|
||||
return False
|
||||
|
||||
# Response generally looks like this:
|
||||
# [
|
||||
# {
|
||||
# noindex: False,
|
||||
# oneToOne: False,
|
||||
# avatarUrl: 'https://path/to/avatar/url',
|
||||
# url: '/apprise-notifications/community',
|
||||
# public: True,
|
||||
# tags: [],
|
||||
# lurk: False,
|
||||
# uri: 'apprise-notifications/community',
|
||||
# lastAccessTime: '2019-03-25T00:12:28.144Z',
|
||||
# topic: '',
|
||||
# roomMember: True,
|
||||
# groupId: '5c981cecd73408ce4fbbad2f',
|
||||
# githubType: 'REPO_CHANNEL',
|
||||
# unreadItems: 0,
|
||||
# mentions: 0,
|
||||
# security: 'PUBLIC',
|
||||
# userCount: 1,
|
||||
# id: '5c981cecd73408ce4fbbad31',
|
||||
# name: 'apprise/community'
|
||||
# }
|
||||
# ]
|
||||
for entry in response:
|
||||
self._room_mapping[entry['name'].lower().split('/')[0]] = {
|
||||
# The ID of the room
|
||||
'id': entry['id'],
|
||||
|
||||
# A descriptive name (useful for logging)
|
||||
'uri': entry['uri'],
|
||||
}
|
||||
|
||||
# Create a copy of the targets list
|
||||
targets = list(self.targets)
|
||||
while len(targets):
|
||||
target = targets.pop(0).lower()
|
||||
|
||||
if target not in self._room_mapping:
|
||||
self.logger.warning(
|
||||
'Failed to locate Gitter room {}'.format(target))
|
||||
|
||||
# Flag our error
|
||||
has_error = True
|
||||
continue
|
||||
|
||||
# prepare our payload
|
||||
payload = {
|
||||
'text': body,
|
||||
}
|
||||
|
||||
# Our Notification URL
|
||||
notify_url = 'rooms/{}/chatMessages'.format(
|
||||
self._room_mapping[target]['id'])
|
||||
|
||||
# Perform our query
|
||||
postokay, response = self._fetch(
|
||||
notify_url, payload=dumps(payload), method='POST')
|
||||
|
||||
if not postokay:
|
||||
# Flag our error
|
||||
has_error = True
|
||||
|
||||
return not has_error
|
||||
|
||||
def _fetch(self, url, payload=None, method='GET'):
|
||||
"""
|
||||
Wrapper to request object
|
||||
|
||||
"""
|
||||
|
||||
# Prepare our headers:
|
||||
headers = {
|
||||
'User-Agent': self.app_id,
|
||||
'Accept': 'application/json',
|
||||
'Authorization': 'Bearer ' + self.token,
|
||||
}
|
||||
if payload:
|
||||
# Only set our header payload if it's defined
|
||||
headers['Content-Type'] = 'application/json'
|
||||
|
||||
# Default content response object
|
||||
content = {}
|
||||
|
||||
# Update our URL
|
||||
url = '{}/{}'.format(GITTER_API_URL, url)
|
||||
|
||||
# Some Debug Logging
|
||||
self.logger.debug('Gitter {} URL: {} (cert_verify={})'.format(
|
||||
method,
|
||||
url, self.verify_certificate))
|
||||
if payload:
|
||||
self.logger.debug('Gitter Payload: {}' .format(payload))
|
||||
|
||||
# By default set wait to None
|
||||
wait = None
|
||||
|
||||
if self.ratelimit_remaining <= 0:
|
||||
# Determine how long we should wait for or if we should wait at
|
||||
# all. This isn't fool-proof because we can't be sure the client
|
||||
# time (calling this script) is completely synced up with the
|
||||
# Gitter server. One would hope we're on NTP and our clocks are
|
||||
# the same allowing this to role smoothly:
|
||||
|
||||
now = datetime.utcnow()
|
||||
if now < self.ratelimit_reset:
|
||||
# We need to throttle for the difference in seconds
|
||||
# We add 0.5 seconds to the end just to allow a grace
|
||||
# period.
|
||||
wait = (self.ratelimit_reset - now).total_seconds() + 0.5
|
||||
|
||||
# Always call throttle before any remote server i/o is made
|
||||
self.throttle(wait=wait)
|
||||
|
||||
# fetch function
|
||||
fn = requests.post if method == 'POST' else requests.get
|
||||
try:
|
||||
r = fn(
|
||||
url,
|
||||
data=payload,
|
||||
headers=headers,
|
||||
verify=self.verify_certificate,
|
||||
timeout=self.request_timeout,
|
||||
)
|
||||
|
||||
if r.status_code != requests.codes.ok:
|
||||
# We had a problem
|
||||
status_str = \
|
||||
NotifyGitter.http_response_code_lookup(r.status_code)
|
||||
|
||||
self.logger.warning(
|
||||
'Failed to send Gitter {} to {}: '
|
||||
'{}error={}.'.format(
|
||||
method,
|
||||
url,
|
||||
', ' if status_str else '',
|
||||
r.status_code))
|
||||
|
||||
self.logger.debug(
|
||||
'Response Details:\r\n{}'.format(r.content))
|
||||
|
||||
# Mark our failure
|
||||
return (False, content)
|
||||
|
||||
try:
|
||||
content = loads(r.content)
|
||||
|
||||
except (AttributeError, TypeError, ValueError):
|
||||
# ValueError = r.content is Unparsable
|
||||
# TypeError = r.content is None
|
||||
# AttributeError = r is None
|
||||
content = {}
|
||||
|
||||
try:
|
||||
self.ratelimit_remaining = \
|
||||
int(r.headers.get('X-RateLimit-Remaining'))
|
||||
self.ratelimit_reset = datetime.utcfromtimestamp(
|
||||
int(r.headers.get('X-RateLimit-Reset')))
|
||||
|
||||
except (TypeError, ValueError):
|
||||
# This is returned if we could not retrieve this information
|
||||
# gracefully accept this state and move on
|
||||
pass
|
||||
|
||||
except requests.RequestException as e:
|
||||
self.logger.warning(
|
||||
'Exception received when sending Gitter {} to {}: '.
|
||||
format(method, url))
|
||||
self.logger.debug('Socket Exception: %s' % str(e))
|
||||
|
||||
# Mark our failure
|
||||
return (False, content)
|
||||
|
||||
return (True, content)
|
||||
|
||||
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,
|
||||
token=self.pprint(self.token, privacy, safe=''),
|
||||
targets='/'.join(
|
||||
[NotifyGitter.quote(x, safe='') for x in self.targets]),
|
||||
params=NotifyGitter.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
|
||||
|
||||
results['token'] = NotifyGitter.unquote(results['host'])
|
||||
|
||||
# Get our entries; split_path() looks after unquoting content for us
|
||||
# by default
|
||||
results['targets'] = NotifyGitter.split_path(results['fullpath'])
|
||||
|
||||
# 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'] += NotifyGitter.parse_list(results['qsd']['to'])
|
||||
|
||||
# Include images with our message
|
||||
results['include_image'] = \
|
||||
parse_bool(results['qsd'].get('image', False))
|
||||
|
||||
return results
|
|
@ -1,5 +1,5 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# BSD 3-Clause License
|
||||
# BSD 2-Clause License
|
||||
#
|
||||
# Apprise - Push Notification Library.
|
||||
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
|
||||
|
@ -14,10 +14,6 @@
|
|||
# this list of conditions and the following disclaimer in the documentation
|
||||
# and/or other materials provided with the distribution.
|
||||
#
|
||||
# 3. Neither the name of the copyright holder nor the names of its
|
||||
# contributors may be used to endorse or promote products derived from
|
||||
# this software without specific prior written permission.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# BSD 3-Clause License
|
||||
# BSD 2-Clause License
|
||||
#
|
||||
# Apprise - Push Notification Library.
|
||||
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
|
||||
|
@ -14,10 +14,6 @@
|
|||
# this list of conditions and the following disclaimer in the documentation
|
||||
# and/or other materials provided with the distribution.
|
||||
#
|
||||
# 3. Neither the name of the copyright holder nor the names of its
|
||||
# contributors may be used to endorse or promote products derived from
|
||||
# this software without specific prior written permission.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# BSD 3-Clause License
|
||||
# BSD 2-Clause License
|
||||
#
|
||||
# Apprise - Push Notification Library.
|
||||
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
|
||||
|
@ -14,10 +14,6 @@
|
|||
# this list of conditions and the following disclaimer in the documentation
|
||||
# and/or other materials provided with the distribution.
|
||||
#
|
||||
# 3. Neither the name of the copyright holder nor the names of its
|
||||
# contributors may be used to endorse or promote products derived from
|
||||
# this software without specific prior written permission.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
|
@ -134,7 +130,6 @@ class NotifyGotify(NotifyBase):
|
|||
'type': 'string',
|
||||
'map_to': 'fullpath',
|
||||
'default': '/',
|
||||
'required': True,
|
||||
},
|
||||
'port': {
|
||||
'name': _('Port'),
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# BSD 3-Clause License
|
||||
# BSD 2-Clause License
|
||||
#
|
||||
# Apprise - Push Notification Library.
|
||||
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
|
||||
|
@ -14,10 +14,6 @@
|
|||
# this list of conditions and the following disclaimer in the documentation
|
||||
# and/or other materials provided with the distribution.
|
||||
#
|
||||
# 3. Neither the name of the copyright holder nor the names of its
|
||||
# contributors may be used to endorse or promote products derived from
|
||||
# this software without specific prior written permission.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# BSD 3-Clause License
|
||||
# BSD 2-Clause License
|
||||
#
|
||||
# Apprise - Push Notification Library.
|
||||
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
|
||||
|
@ -14,10 +14,6 @@
|
|||
# this list of conditions and the following disclaimer in the documentation
|
||||
# and/or other materials provided with the distribution.
|
||||
#
|
||||
# 3. Neither the name of the copyright holder nor the names of its
|
||||
# contributors may be used to endorse or promote products derived from
|
||||
# this software without specific prior written permission.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# BSD 3-Clause License
|
||||
# BSD 2-Clause License
|
||||
#
|
||||
# Apprise - Push Notification Library.
|
||||
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
|
||||
|
@ -14,10 +14,6 @@
|
|||
# this list of conditions and the following disclaimer in the documentation
|
||||
# and/or other materials provided with the distribution.
|
||||
#
|
||||
# 3. Neither the name of the copyright holder nor the names of its
|
||||
# contributors may be used to endorse or promote products derived from
|
||||
# this software without specific prior written permission.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# BSD 3-Clause License
|
||||
# BSD 2-Clause License
|
||||
#
|
||||
# Apprise - Push Notification Library.
|
||||
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
|
||||
|
@ -14,10 +14,6 @@
|
|||
# this list of conditions and the following disclaimer in the documentation
|
||||
# and/or other materials provided with the distribution.
|
||||
#
|
||||
# 3. Neither the name of the copyright holder nor the names of its
|
||||
# contributors may be used to endorse or promote products derived from
|
||||
# this software without specific prior written permission.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
|
@ -30,7 +26,6 @@
|
|||
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
# POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
#
|
||||
# For this plugin to work, you need to add the Maker applet to your profile
|
||||
# Simply visit https://ifttt.com/search and search for 'Webhooks'
|
||||
# Or if you're signed in, click here: https://ifttt.com/maker_webhooks
|
||||
|
@ -312,6 +307,12 @@ class NotifyIFTTT(NotifyBase):
|
|||
params=NotifyIFTTT.urlencode(params),
|
||||
)
|
||||
|
||||
def __len__(self):
|
||||
"""
|
||||
Returns the number of targets associated with this notification
|
||||
"""
|
||||
return len(self.events)
|
||||
|
||||
@staticmethod
|
||||
def parse_url(url):
|
||||
"""
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# BSD 3-Clause License
|
||||
# BSD 2-Clause License
|
||||
#
|
||||
# Apprise - Push Notification Library.
|
||||
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
|
||||
|
@ -14,10 +14,6 @@
|
|||
# this list of conditions and the following disclaimer in the documentation
|
||||
# and/or other materials provided with the distribution.
|
||||
#
|
||||
# 3. Neither the name of the copyright holder nor the names of its
|
||||
# contributors may be used to endorse or promote products derived from
|
||||
# this software without specific prior written permission.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
|
@ -41,6 +37,17 @@ from ..common import NotifyType
|
|||
from ..AppriseLocale import gettext_lazy as _
|
||||
|
||||
|
||||
class JSONPayloadField:
|
||||
"""
|
||||
Identifies the fields available in the JSON Payload
|
||||
"""
|
||||
VERSION = 'version'
|
||||
TITLE = 'title'
|
||||
MESSAGE = 'message'
|
||||
ATTACHMENTS = 'attachments'
|
||||
MESSAGETYPE = 'type'
|
||||
|
||||
|
||||
# Defines the method to send the notification
|
||||
METHODS = (
|
||||
'POST',
|
||||
|
@ -69,6 +76,9 @@ class NotifyJSON(NotifyBase):
|
|||
# A URL that takes you to the setup/help of the specific protocol
|
||||
setup_url = 'https://github.com/caronc/apprise/wiki/Notify_Custom_JSON'
|
||||
|
||||
# Support attachments
|
||||
attachment_support = True
|
||||
|
||||
# Allows the user to specify the NotifyImageSize object
|
||||
image_size = NotifyImageSize.XY_128
|
||||
|
||||
|
@ -76,6 +86,12 @@ class NotifyJSON(NotifyBase):
|
|||
# local anyway
|
||||
request_rate_per_sec = 0
|
||||
|
||||
# Define the JSON version to place in all payloads
|
||||
# Version: Major.Minor, Major is only updated if the entire schema is
|
||||
# changed. If just adding new items (or removing old ones, only increment
|
||||
# the Minor!
|
||||
json_version = '1.0'
|
||||
|
||||
# Define object templates
|
||||
templates = (
|
||||
'{schema}://{host}',
|
||||
|
@ -246,7 +262,7 @@ class NotifyJSON(NotifyBase):
|
|||
|
||||
# Track our potential attachments
|
||||
attachments = []
|
||||
if attach:
|
||||
if attach and self.attachment_support:
|
||||
for attachment in attach:
|
||||
# Perform some simple error checking
|
||||
if not attachment:
|
||||
|
@ -274,20 +290,30 @@ class NotifyJSON(NotifyBase):
|
|||
self.logger.debug('I/O Exception: %s' % str(e))
|
||||
return False
|
||||
|
||||
# prepare JSON Object
|
||||
# Prepare JSON Object
|
||||
payload = {
|
||||
# Version: Major.Minor, Major is only updated if the entire
|
||||
# schema is changed. If just adding new items (or removing
|
||||
# old ones, only increment the Minor!
|
||||
'version': '1.0',
|
||||
'title': title,
|
||||
'message': body,
|
||||
'attachments': attachments,
|
||||
'type': notify_type,
|
||||
JSONPayloadField.VERSION: self.json_version,
|
||||
JSONPayloadField.TITLE: title,
|
||||
JSONPayloadField.MESSAGE: body,
|
||||
JSONPayloadField.ATTACHMENTS: attachments,
|
||||
JSONPayloadField.MESSAGETYPE: notify_type,
|
||||
}
|
||||
|
||||
# Apply any/all payload over-rides defined
|
||||
payload.update(self.payload_extras)
|
||||
for key, value in self.payload_extras.items():
|
||||
|
||||
if key in payload:
|
||||
if not value:
|
||||
# Do not store element in payload response
|
||||
del payload[key]
|
||||
|
||||
else:
|
||||
# Re-map
|
||||
payload[value] = payload[key]
|
||||
del payload[key]
|
||||
|
||||
else:
|
||||
# Append entry
|
||||
payload[key] = value
|
||||
|
||||
auth = None
|
||||
if self.user:
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# BSD 3-Clause License
|
||||
# BSD 2-Clause License
|
||||
#
|
||||
# Apprise - Push Notification Library.
|
||||
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
|
||||
|
@ -14,10 +14,6 @@
|
|||
# this list of conditions and the following disclaimer in the documentation
|
||||
# and/or other materials provided with the distribution.
|
||||
#
|
||||
# 3. Neither the name of the copyright holder nor the names of its
|
||||
# contributors may be used to endorse or promote products derived from
|
||||
# this software without specific prior written permission.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
|
@ -174,7 +170,6 @@ class NotifyJoin(NotifyBase):
|
|||
'targets': {
|
||||
'name': _('Targets'),
|
||||
'type': 'list:string',
|
||||
'required': True,
|
||||
},
|
||||
})
|
||||
|
||||
|
@ -373,6 +368,12 @@ class NotifyJoin(NotifyBase):
|
|||
for x in self.targets]),
|
||||
params=NotifyJoin.urlencode(params))
|
||||
|
||||
def __len__(self):
|
||||
"""
|
||||
Returns the number of targets associated with this notification
|
||||
"""
|
||||
return len(self.targets)
|
||||
|
||||
@staticmethod
|
||||
def parse_url(url):
|
||||
"""
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# BSD 3-Clause License
|
||||
# BSD 2-Clause License
|
||||
#
|
||||
# Apprise - Push Notification Library.
|
||||
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
|
||||
|
@ -14,10 +14,6 @@
|
|||
# this list of conditions and the following disclaimer in the documentation
|
||||
# and/or other materials provided with the distribution.
|
||||
#
|
||||
# 3. Neither the name of the copyright holder nor the names of its
|
||||
# contributors may be used to endorse or promote products derived from
|
||||
# this software without specific prior written permission.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
|
@ -324,6 +320,12 @@ class NotifyKavenegar(NotifyBase):
|
|||
[NotifyKavenegar.quote(x, safe='') for x in self.targets]),
|
||||
params=NotifyKavenegar.urlencode(params))
|
||||
|
||||
def __len__(self):
|
||||
"""
|
||||
Returns the number of targets associated with this notification
|
||||
"""
|
||||
return len(self.targets)
|
||||
|
||||
@staticmethod
|
||||
def parse_url(url):
|
||||
"""
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# BSD 3-Clause License
|
||||
# BSD 2-Clause License
|
||||
#
|
||||
# Apprise - Push Notification Library.
|
||||
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
|
||||
|
@ -14,10 +14,6 @@
|
|||
# this list of conditions and the following disclaimer in the documentation
|
||||
# and/or other materials provided with the distribution.
|
||||
#
|
||||
# 3. Neither the name of the copyright holder nor the names of its
|
||||
# contributors may be used to endorse or promote products derived from
|
||||
# this software without specific prior written permission.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# BSD 3-Clause License
|
||||
# BSD 2-Clause License
|
||||
#
|
||||
# Apprise - Push Notification Library.
|
||||
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
|
||||
|
@ -14,10 +14,6 @@
|
|||
# this list of conditions and the following disclaimer in the documentation
|
||||
# and/or other materials provided with the distribution.
|
||||
#
|
||||
# 3. Neither the name of the copyright holder nor the names of its
|
||||
# contributors may be used to endorse or promote products derived from
|
||||
# this software without specific prior written permission.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
|
@ -370,6 +366,7 @@ class NotifyLametric(NotifyBase):
|
|||
|
||||
# Device Mode
|
||||
'{schema}://{apikey}@{host}',
|
||||
'{schema}://{user}:{apikey}@{host}',
|
||||
'{schema}://{apikey}@{host}:{port}',
|
||||
'{schema}://{user}:{apikey}@{host}:{port}',
|
||||
)
|
||||
|
@ -404,7 +401,6 @@ class NotifyLametric(NotifyBase):
|
|||
'host': {
|
||||
'name': _('Hostname'),
|
||||
'type': 'string',
|
||||
'required': True,
|
||||
},
|
||||
'port': {
|
||||
'name': _('Port'),
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# BSD 3-Clause License
|
||||
# BSD 2-Clause License
|
||||
#
|
||||
# Apprise - Push Notification Library.
|
||||
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
|
||||
|
@ -14,10 +14,6 @@
|
|||
# this list of conditions and the following disclaimer in the documentation
|
||||
# and/or other materials provided with the distribution.
|
||||
#
|
||||
# 3. Neither the name of the copyright holder nor the names of its
|
||||
# contributors may be used to endorse or promote products derived from
|
||||
# this software without specific prior written permission.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
|
@ -102,6 +98,7 @@ class NotifyLine(NotifyBase):
|
|||
'targets': {
|
||||
'name': _('Targets'),
|
||||
'type': 'list:string',
|
||||
'required': True
|
||||
},
|
||||
})
|
||||
|
||||
|
@ -267,6 +264,12 @@ class NotifyLine(NotifyBase):
|
|||
params=NotifyLine.urlencode(params),
|
||||
)
|
||||
|
||||
def __len__(self):
|
||||
"""
|
||||
Returns the number of targets associated with this notification
|
||||
"""
|
||||
return len(self.targets)
|
||||
|
||||
@staticmethod
|
||||
def parse_url(url):
|
||||
"""
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# BSD 3-Clause License
|
||||
# BSD 2-Clause License
|
||||
#
|
||||
# Apprise - Push Notification Library.
|
||||
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
|
||||
|
@ -14,10 +14,6 @@
|
|||
# this list of conditions and the following disclaimer in the documentation
|
||||
# and/or other materials provided with the distribution.
|
||||
#
|
||||
# 3. Neither the name of the copyright holder nor the names of its
|
||||
# contributors may be used to endorse or promote products derived from
|
||||
# this software without specific prior written permission.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
|
@ -427,6 +423,10 @@ class NotifyMQTT(NotifyBase):
|
|||
self.logger.debug('Socket Exception: %s' % str(e))
|
||||
return False
|
||||
|
||||
if not has_error:
|
||||
# Verbal notice
|
||||
self.logger.info('Sent MQTT notification')
|
||||
|
||||
return not has_error
|
||||
|
||||
def url(self, privacy=False, *args, **kwargs):
|
||||
|
@ -476,6 +476,12 @@ class NotifyMQTT(NotifyBase):
|
|||
params=NotifyMQTT.urlencode(params),
|
||||
)
|
||||
|
||||
def __len__(self):
|
||||
"""
|
||||
Returns the number of targets associated with this notification
|
||||
"""
|
||||
return len(self.topics)
|
||||
|
||||
@staticmethod
|
||||
def parse_url(url):
|
||||
"""
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# BSD 3-Clause License
|
||||
# BSD 2-Clause License
|
||||
#
|
||||
# Apprise - Push Notification Library.
|
||||
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
|
||||
|
@ -14,10 +14,6 @@
|
|||
# this list of conditions and the following disclaimer in the documentation
|
||||
# and/or other materials provided with the distribution.
|
||||
#
|
||||
# 3. Neither the name of the copyright holder nor the names of its
|
||||
# contributors may be used to endorse or promote products derived from
|
||||
# this software without specific prior written permission.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
|
@ -35,50 +31,31 @@
|
|||
# Get your (authkey) from the dashboard here:
|
||||
# - https://world.msg91.com/user/index.php#api
|
||||
#
|
||||
# Note: You will need to define a template for this to work
|
||||
#
|
||||
# Get details on the API used in this plugin here:
|
||||
# - https://world.msg91.com/apidoc/textsms/send-sms.php
|
||||
|
||||
# - https://docs.msg91.com/reference/send-sms
|
||||
import re
|
||||
import requests
|
||||
|
||||
from json import dumps
|
||||
from .NotifyBase import NotifyBase
|
||||
from ..common import NotifyType
|
||||
from ..utils import is_phone_no
|
||||
from ..utils import parse_phone_no
|
||||
from ..utils import parse_phone_no, parse_bool
|
||||
from ..utils import validate_regex
|
||||
from ..AppriseLocale import gettext_lazy as _
|
||||
|
||||
|
||||
class MSG91Route:
|
||||
class MSG91PayloadField:
|
||||
"""
|
||||
Transactional SMS Routes
|
||||
route=1 for promotional, route=4 for transactional SMS.
|
||||
Identifies the fields available in the JSON Payload
|
||||
"""
|
||||
PROMOTIONAL = 1
|
||||
TRANSACTIONAL = 4
|
||||
BODY = 'body'
|
||||
MESSAGETYPE = 'type'
|
||||
|
||||
|
||||
# Used for verification
|
||||
MSG91_ROUTES = (
|
||||
MSG91Route.PROMOTIONAL,
|
||||
MSG91Route.TRANSACTIONAL,
|
||||
)
|
||||
|
||||
|
||||
class MSG91Country:
|
||||
"""
|
||||
Optional value that can be specified on the MSG91 api
|
||||
"""
|
||||
INTERNATIONAL = 0
|
||||
USA = 1
|
||||
INDIA = 91
|
||||
|
||||
|
||||
# Used for verification
|
||||
MSG91_COUNTRIES = (
|
||||
MSG91Country.INTERNATIONAL,
|
||||
MSG91Country.USA,
|
||||
MSG91Country.INDIA,
|
||||
)
|
||||
# Add entries here that are reserved
|
||||
RESERVED_KEYWORDS = ('mobiles', )
|
||||
|
||||
|
||||
class NotifyMSG91(NotifyBase):
|
||||
|
@ -99,7 +76,7 @@ class NotifyMSG91(NotifyBase):
|
|||
setup_url = 'https://github.com/caronc/apprise/wiki/Notify_msg91'
|
||||
|
||||
# MSG91 uses the http protocol with JSON requests
|
||||
notify_url = 'https://world.msg91.com/api/sendhttp.php'
|
||||
notify_url = 'https://control.msg91.com/api/v5/flow/'
|
||||
|
||||
# The maximum length of the body
|
||||
body_maxlen = 160
|
||||
|
@ -108,14 +85,24 @@ class NotifyMSG91(NotifyBase):
|
|||
# cause any title (if defined) to get placed into the message body.
|
||||
title_maxlen = 0
|
||||
|
||||
# Our supported mappings and component keys
|
||||
component_key_re = re.compile(
|
||||
r'(?P<key>((?P<id>[a-z0-9_-])?|(?P<map>body|type)))', re.IGNORECASE)
|
||||
|
||||
# Define object templates
|
||||
templates = (
|
||||
'{schema}://{authkey}/{targets}',
|
||||
'{schema}://{sender}@{authkey}/{targets}',
|
||||
'{schema}://{template}@{authkey}/{targets}',
|
||||
)
|
||||
|
||||
# Define our template tokens
|
||||
template_tokens = dict(NotifyBase.template_tokens, **{
|
||||
'template': {
|
||||
'name': _('Template ID'),
|
||||
'type': 'string',
|
||||
'required': True,
|
||||
'private': True,
|
||||
'regex': (r'^[a-z0-9 _-]+$', 'i'),
|
||||
},
|
||||
'authkey': {
|
||||
'name': _('Authentication Key'),
|
||||
'type': 'string',
|
||||
|
@ -133,10 +120,7 @@ class NotifyMSG91(NotifyBase):
|
|||
'targets': {
|
||||
'name': _('Targets'),
|
||||
'type': 'list:string',
|
||||
},
|
||||
'sender': {
|
||||
'name': _('Sender ID'),
|
||||
'type': 'string',
|
||||
'required': True,
|
||||
},
|
||||
})
|
||||
|
||||
|
@ -145,21 +129,23 @@ class NotifyMSG91(NotifyBase):
|
|||
'to': {
|
||||
'alias_of': 'targets',
|
||||
},
|
||||
'route': {
|
||||
'name': _('Route'),
|
||||
'type': 'choice:int',
|
||||
'values': MSG91_ROUTES,
|
||||
'default': MSG91Route.TRANSACTIONAL,
|
||||
},
|
||||
'country': {
|
||||
'name': _('Country'),
|
||||
'type': 'choice:int',
|
||||
'values': MSG91_COUNTRIES,
|
||||
'short_url': {
|
||||
'name': _('Short URL'),
|
||||
'type': 'bool',
|
||||
'default': False,
|
||||
},
|
||||
})
|
||||
|
||||
def __init__(self, authkey, targets=None, sender=None, route=None,
|
||||
country=None, **kwargs):
|
||||
# Define any kwargs we're using
|
||||
template_kwargs = {
|
||||
'template_mapping': {
|
||||
'name': _('Template Mapping'),
|
||||
'prefix': ':',
|
||||
},
|
||||
}
|
||||
|
||||
def __init__(self, template, authkey, targets=None, short_url=None,
|
||||
template_mapping=None, **kwargs):
|
||||
"""
|
||||
Initialize MSG91 Object
|
||||
"""
|
||||
|
@ -174,39 +160,20 @@ class NotifyMSG91(NotifyBase):
|
|||
self.logger.warning(msg)
|
||||
raise TypeError(msg)
|
||||
|
||||
if route is None:
|
||||
self.route = self.template_args['route']['default']
|
||||
# Template ID
|
||||
self.template = validate_regex(
|
||||
template, *self.template_tokens['template']['regex'])
|
||||
if not self.template:
|
||||
msg = 'An invalid MSG91 Template ID ' \
|
||||
'({}) was specified.'.format(template)
|
||||
self.logger.warning(msg)
|
||||
raise TypeError(msg)
|
||||
|
||||
if short_url is None:
|
||||
self.short_url = self.template_args['short_url']['default']
|
||||
|
||||
else:
|
||||
try:
|
||||
self.route = int(route)
|
||||
if self.route not in MSG91_ROUTES:
|
||||
# Let outer except catch thi
|
||||
raise ValueError()
|
||||
|
||||
except (ValueError, TypeError):
|
||||
msg = 'The MSG91 route specified ({}) is invalid.'\
|
||||
.format(route)
|
||||
self.logger.warning(msg)
|
||||
raise TypeError(msg)
|
||||
|
||||
if country:
|
||||
try:
|
||||
self.country = int(country)
|
||||
if self.country not in MSG91_COUNTRIES:
|
||||
# Let outer except catch thi
|
||||
raise ValueError()
|
||||
|
||||
except (ValueError, TypeError):
|
||||
msg = 'The MSG91 country specified ({}) is invalid.'\
|
||||
.format(country)
|
||||
self.logger.warning(msg)
|
||||
raise TypeError(msg)
|
||||
else:
|
||||
self.country = country
|
||||
|
||||
# Store our sender
|
||||
self.sender = sender
|
||||
self.short_url = parse_bool(short_url)
|
||||
|
||||
# Parse our targets
|
||||
self.targets = list()
|
||||
|
@ -224,6 +191,11 @@ class NotifyMSG91(NotifyBase):
|
|||
# store valid phone number
|
||||
self.targets.append(result['full'])
|
||||
|
||||
self.template_mapping = {}
|
||||
if template_mapping:
|
||||
# Store our extra payload entries
|
||||
self.template_mapping.update(template_mapping)
|
||||
|
||||
return
|
||||
|
||||
def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs):
|
||||
|
@ -239,23 +211,55 @@ class NotifyMSG91(NotifyBase):
|
|||
# Prepare our headers
|
||||
headers = {
|
||||
'User-Agent': self.app_id,
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
'Content-Type': 'application/json',
|
||||
'authkey': self.authkey,
|
||||
}
|
||||
|
||||
# Base
|
||||
recipient_payload = {
|
||||
'mobiles': None,
|
||||
# Keyword Tokens
|
||||
MSG91PayloadField.BODY: body,
|
||||
MSG91PayloadField.MESSAGETYPE: notify_type,
|
||||
}
|
||||
|
||||
# Prepare Recipient Payload Object
|
||||
for key, value in self.template_mapping.items():
|
||||
|
||||
if key in RESERVED_KEYWORDS:
|
||||
self.logger.warning(
|
||||
'Ignoring MSG91 custom payload entry %s', key)
|
||||
continue
|
||||
|
||||
if key in recipient_payload:
|
||||
if not value:
|
||||
# Do not store element in payload response
|
||||
del recipient_payload[key]
|
||||
|
||||
else:
|
||||
# Re-map
|
||||
recipient_payload[value] = recipient_payload[key]
|
||||
del recipient_payload[key]
|
||||
|
||||
else:
|
||||
# Append entry
|
||||
recipient_payload[key] = value
|
||||
|
||||
# Prepare our recipients
|
||||
recipients = []
|
||||
for target in self.targets:
|
||||
recipient = recipient_payload.copy()
|
||||
recipient['mobiles'] = target
|
||||
recipients.append(recipient)
|
||||
|
||||
# Prepare our payload
|
||||
payload = {
|
||||
'sender': self.sender if self.sender else self.app_id,
|
||||
'authkey': self.authkey,
|
||||
'message': body,
|
||||
'response': 'json',
|
||||
'template_id': self.template,
|
||||
'short_url': 1 if self.short_url else 0,
|
||||
# target phone numbers are sent with a comma delimiter
|
||||
'mobiles': ','.join(self.targets),
|
||||
'route': str(self.route),
|
||||
'recipients': recipients,
|
||||
}
|
||||
|
||||
if self.country:
|
||||
payload['country'] = str(self.country)
|
||||
|
||||
# Some Debug Logging
|
||||
self.logger.debug('MSG91 POST URL: {} (cert_verify={})'.format(
|
||||
self.notify_url, self.verify_certificate))
|
||||
|
@ -267,7 +271,7 @@ class NotifyMSG91(NotifyBase):
|
|||
try:
|
||||
r = requests.post(
|
||||
self.notify_url,
|
||||
data=payload,
|
||||
data=dumps(payload),
|
||||
headers=headers,
|
||||
verify=self.verify_certificate,
|
||||
timeout=self.request_timeout,
|
||||
|
@ -313,22 +317,32 @@ class NotifyMSG91(NotifyBase):
|
|||
|
||||
# Define any URL parameters
|
||||
params = {
|
||||
'route': str(self.route),
|
||||
'short_url': str(self.short_url),
|
||||
}
|
||||
|
||||
# Extend our parameters
|
||||
params.update(self.url_parameters(privacy=privacy, *args, **kwargs))
|
||||
|
||||
if self.country:
|
||||
params['country'] = str(self.country)
|
||||
# Payload body extras prefixed with a ':' sign
|
||||
# Append our payload extras into our parameters
|
||||
params.update(
|
||||
{':{}'.format(k): v for k, v in self.template_mapping.items()})
|
||||
|
||||
return '{schema}://{authkey}/{targets}/?{params}'.format(
|
||||
return '{schema}://{template}@{authkey}/{targets}/?{params}'.format(
|
||||
schema=self.secure_protocol,
|
||||
template=self.pprint(self.template, privacy, safe=''),
|
||||
authkey=self.pprint(self.authkey, privacy, safe=''),
|
||||
targets='/'.join(
|
||||
[NotifyMSG91.quote(x, safe='') for x in self.targets]),
|
||||
params=NotifyMSG91.urlencode(params))
|
||||
|
||||
def __len__(self):
|
||||
"""
|
||||
Returns the number of targets associated with this notification
|
||||
"""
|
||||
targets = len(self.targets)
|
||||
return targets if targets > 0 else 1
|
||||
|
||||
@staticmethod
|
||||
def parse_url(url):
|
||||
"""
|
||||
|
@ -349,11 +363,11 @@ class NotifyMSG91(NotifyBase):
|
|||
# The hostname is our authentication key
|
||||
results['authkey'] = NotifyMSG91.unquote(results['host'])
|
||||
|
||||
if 'route' in results['qsd'] and len(results['qsd']['route']):
|
||||
results['route'] = results['qsd']['route']
|
||||
# The template id is kept in the user field
|
||||
results['template'] = NotifyMSG91.unquote(results['user'])
|
||||
|
||||
if 'country' in results['qsd'] and len(results['qsd']['country']):
|
||||
results['country'] = results['qsd']['country']
|
||||
if 'short_url' in results['qsd'] and len(results['qsd']['short_url']):
|
||||
results['short_url'] = parse_bool(results['qsd']['short_url'])
|
||||
|
||||
# Support the 'to' variable so that we can support targets this way too
|
||||
# The 'to' makes it easier to use yaml configuration
|
||||
|
@ -361,4 +375,10 @@ class NotifyMSG91(NotifyBase):
|
|||
results['targets'] += \
|
||||
NotifyMSG91.parse_phone_no(results['qsd']['to'])
|
||||
|
||||
# store any additional payload extra's defined
|
||||
results['template_mapping'] = {
|
||||
NotifyMSG91.unquote(x): NotifyMSG91.unquote(y)
|
||||
for x, y in results['qsd:'].items()
|
||||
}
|
||||
|
||||
return results
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# BSD 3-Clause License
|
||||
# BSD 2-Clause License
|
||||
#
|
||||
# Apprise - Push Notification Library.
|
||||
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
|
||||
|
@ -14,10 +14,6 @@
|
|||
# this list of conditions and the following disclaimer in the documentation
|
||||
# and/or other materials provided with the distribution.
|
||||
#
|
||||
# 3. Neither the name of the copyright holder nor the names of its
|
||||
# contributors may be used to endorse or promote products derived from
|
||||
# this software without specific prior written permission.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# BSD 3-Clause License
|
||||
# BSD 2-Clause License
|
||||
#
|
||||
# Apprise - Push Notification Library.
|
||||
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
|
||||
|
@ -14,10 +14,6 @@
|
|||
# this list of conditions and the following disclaimer in the documentation
|
||||
# and/or other materials provided with the distribution.
|
||||
#
|
||||
# 3. Neither the name of the copyright holder nor the names of its
|
||||
# contributors may be used to endorse or promote products derived from
|
||||
# this software without specific prior written permission.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
|
@ -197,8 +193,7 @@ class NotifyMacOSX(NotifyBase):
|
|||
self.logger.debug('MacOSX CMD: {}'.format(' '.join(cmd)))
|
||||
|
||||
# Send our notification
|
||||
output = subprocess.Popen(
|
||||
cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
|
||||
output = subprocess.Popen(cmd)
|
||||
|
||||
# Wait for process to complete
|
||||
output.wait()
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# BSD 3-Clause License
|
||||
# BSD 2-Clause License
|
||||
#
|
||||
# Apprise - Push Notification Library.
|
||||
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
|
||||
|
@ -14,10 +14,6 @@
|
|||
# this list of conditions and the following disclaimer in the documentation
|
||||
# and/or other materials provided with the distribution.
|
||||
#
|
||||
# 3. Neither the name of the copyright holder nor the names of its
|
||||
# contributors may be used to endorse or promote products derived from
|
||||
# this software without specific prior written permission.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
|
@ -121,6 +117,9 @@ class NotifyMailgun(NotifyBase):
|
|||
# A URL that takes you to the setup/help of the specific protocol
|
||||
setup_url = 'https://github.com/caronc/apprise/wiki/Notify_mailgun'
|
||||
|
||||
# Support attachments
|
||||
attachment_support = True
|
||||
|
||||
# Default Notify Format
|
||||
notify_format = NotifyFormat.HTML
|
||||
|
||||
|
@ -152,8 +151,13 @@ class NotifyMailgun(NotifyBase):
|
|||
'private': True,
|
||||
'required': True,
|
||||
},
|
||||
'target_email': {
|
||||
'name': _('Target Email'),
|
||||
'type': 'string',
|
||||
'map_to': 'targets',
|
||||
},
|
||||
'targets': {
|
||||
'name': _('Target Emails'),
|
||||
'name': _('Targets'),
|
||||
'type': 'list:string',
|
||||
},
|
||||
})
|
||||
|
@ -366,7 +370,7 @@ class NotifyMailgun(NotifyBase):
|
|||
# Track our potential files
|
||||
files = {}
|
||||
|
||||
if attach:
|
||||
if attach and self.attachment_support:
|
||||
for idx, attachment in enumerate(attach):
|
||||
# Perform some simple error checking
|
||||
if not attachment:
|
||||
|
@ -627,6 +631,20 @@ class NotifyMailgun(NotifyBase):
|
|||
safe='') for e in self.targets]),
|
||||
params=NotifyMailgun.urlencode(params))
|
||||
|
||||
def __len__(self):
|
||||
"""
|
||||
Returns the number of targets associated with this notification
|
||||
"""
|
||||
#
|
||||
# Factor batch into calculation
|
||||
#
|
||||
batch_size = 1 if not self.batch else self.default_batch_size
|
||||
targets = len(self.targets)
|
||||
if batch_size > 1:
|
||||
targets = int(targets / batch_size) + \
|
||||
(1 if targets % batch_size else 0)
|
||||
return targets if targets > 0 else 1
|
||||
|
||||
@staticmethod
|
||||
def parse_url(url):
|
||||
"""
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# BSD 3-Clause License
|
||||
# BSD 2-Clause License
|
||||
#
|
||||
# Apprise - Push Notification Library.
|
||||
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
|
||||
|
@ -14,10 +14,6 @@
|
|||
# this list of conditions and the following disclaimer in the documentation
|
||||
# and/or other materials provided with the distribution.
|
||||
#
|
||||
# 3. Neither the name of the copyright holder nor the names of its
|
||||
# contributors may be used to endorse or promote products derived from
|
||||
# this software without specific prior written permission.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
|
@ -35,6 +31,7 @@ import requests
|
|||
from copy import deepcopy
|
||||
from json import dumps, loads
|
||||
from datetime import datetime
|
||||
from datetime import timezone
|
||||
|
||||
from .NotifyBase import NotifyBase
|
||||
from ..URLBase import PrivacyMode
|
||||
|
@ -110,6 +107,10 @@ class NotifyMastodon(NotifyBase):
|
|||
# A URL that takes you to the setup/help of the specific protocol
|
||||
setup_url = 'https://github.com/caronc/apprise/wiki/Notify_mastodon'
|
||||
|
||||
# Support attachments
|
||||
attachment_support = True
|
||||
|
||||
# Allows the user to specify the NotifyImageSize object
|
||||
# Allows the user to specify the NotifyImageSize object; this is supported
|
||||
# through the webhook
|
||||
image_size = NotifyImageSize.XY_128
|
||||
|
@ -150,7 +151,7 @@ class NotifyMastodon(NotifyBase):
|
|||
request_rate_per_sec = 0
|
||||
|
||||
# For Tracking Purposes
|
||||
ratelimit_reset = datetime.utcnow()
|
||||
ratelimit_reset = datetime.now(timezone.utc).replace(tzinfo=None)
|
||||
|
||||
# Default to 1000; users can send up to 1000 DM's and 2400 toot a day
|
||||
# This value only get's adjusted if the server sets it that way
|
||||
|
@ -378,6 +379,13 @@ class NotifyMastodon(NotifyBase):
|
|||
params=NotifyMastodon.urlencode(params),
|
||||
)
|
||||
|
||||
def __len__(self):
|
||||
"""
|
||||
Returns the number of targets associated with this notification
|
||||
"""
|
||||
targets = len(self.targets)
|
||||
return targets if targets > 0 else 1
|
||||
|
||||
def send(self, body, title='', notify_type=NotifyType.INFO, attach=None,
|
||||
**kwargs):
|
||||
"""
|
||||
|
@ -406,11 +414,10 @@ class NotifyMastodon(NotifyBase):
|
|||
else:
|
||||
targets.add(myself)
|
||||
|
||||
if attach:
|
||||
if attach and self.attachment_support:
|
||||
# We need to upload our payload first so that we can source it
|
||||
# in remaining messages
|
||||
for attachment in attach:
|
||||
|
||||
# Perform some simple error checking
|
||||
if not attachment:
|
||||
# We could not access the attachment
|
||||
|
@ -570,7 +577,7 @@ class NotifyMastodon(NotifyBase):
|
|||
_payload = deepcopy(payload)
|
||||
_payload['media_ids'] = media_ids
|
||||
|
||||
if no:
|
||||
if no or not body:
|
||||
# strip text and replace it with the image representation
|
||||
_payload['status'] = \
|
||||
'{:02d}/{:02d}'.format(no + 1, len(batches))
|
||||
|
@ -827,7 +834,7 @@ class NotifyMastodon(NotifyBase):
|
|||
# Mastodon server. One would hope we're on NTP and our clocks are
|
||||
# the same allowing this to role smoothly:
|
||||
|
||||
now = datetime.utcnow()
|
||||
now = datetime.now(timezone.utc).replace(tzinfo=None)
|
||||
if now < self.ratelimit_reset:
|
||||
# We need to throttle for the difference in seconds
|
||||
# We add 0.5 seconds to the end just to allow a grace
|
||||
|
@ -885,8 +892,9 @@ class NotifyMastodon(NotifyBase):
|
|||
# Capture rate limiting if possible
|
||||
self.ratelimit_remaining = \
|
||||
int(r.headers.get('X-RateLimit-Remaining'))
|
||||
self.ratelimit_reset = datetime.utcfromtimestamp(
|
||||
int(r.headers.get('X-RateLimit-Limit')))
|
||||
self.ratelimit_reset = datetime.fromtimestamp(
|
||||
int(r.headers.get('X-RateLimit-Limit')), timezone.utc
|
||||
).replace(tzinfo=None)
|
||||
|
||||
except (TypeError, ValueError):
|
||||
# This is returned if we could not retrieve this information
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# BSD 3-Clause License
|
||||
# BSD 2-Clause License
|
||||
#
|
||||
# Apprise - Push Notification Library.
|
||||
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
|
||||
|
@ -14,10 +14,6 @@
|
|||
# this list of conditions and the following disclaimer in the documentation
|
||||
# and/or other materials provided with the distribution.
|
||||
#
|
||||
# 3. Neither the name of the copyright holder nor the names of its
|
||||
# contributors may be used to endorse or promote products derived from
|
||||
# this software without specific prior written permission.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
|
@ -53,8 +49,11 @@ from ..utils import validate_regex
|
|||
from ..AppriseLocale import gettext_lazy as _
|
||||
|
||||
# Define default path
|
||||
MATRIX_V2_API_PATH = '/_matrix/client/r0'
|
||||
MATRIX_V1_WEBHOOK_PATH = '/api/v1/matrix/hook'
|
||||
MATRIX_V2_API_PATH = '/_matrix/client/r0'
|
||||
MATRIX_V3_API_PATH = '/_matrix/client/v3'
|
||||
MATRIX_V3_MEDIA_PATH = '/_matrix/media/v3'
|
||||
MATRIX_V2_MEDIA_PATH = '/_matrix/media/r0'
|
||||
|
||||
# Extend HTTP Error Messages
|
||||
MATRIX_HTTP_ERROR_MAP = {
|
||||
|
@ -88,6 +87,21 @@ MATRIX_MESSAGE_TYPES = (
|
|||
)
|
||||
|
||||
|
||||
class MatrixVersion:
|
||||
# Version 2
|
||||
V2 = "2"
|
||||
|
||||
# Version 3
|
||||
V3 = "3"
|
||||
|
||||
|
||||
# webhook modes are placed into this list for validation purposes
|
||||
MATRIX_VERSIONS = (
|
||||
MatrixVersion.V2,
|
||||
MatrixVersion.V3,
|
||||
)
|
||||
|
||||
|
||||
class MatrixWebhookMode:
|
||||
# Webhook Mode is disabled
|
||||
DISABLED = "off"
|
||||
|
@ -128,6 +142,9 @@ class NotifyMatrix(NotifyBase):
|
|||
# The default secure protocol
|
||||
secure_protocol = 'matrixs'
|
||||
|
||||
# Support Attachments
|
||||
attachment_support = True
|
||||
|
||||
# A URL that takes you to the setup/help of the specific protocol
|
||||
setup_url = 'https://github.com/caronc/apprise/wiki/Notify_matrix'
|
||||
|
||||
|
@ -147,6 +164,9 @@ class NotifyMatrix(NotifyBase):
|
|||
# Throttle a wee-bit to avoid thrashing
|
||||
request_rate_per_sec = 0.5
|
||||
|
||||
# Our Matrix API Version
|
||||
matrix_api_version = '3'
|
||||
|
||||
# How many retry attempts we'll make in the event the server asks us to
|
||||
# throttle back.
|
||||
default_retries = 2
|
||||
|
@ -175,7 +195,6 @@ class NotifyMatrix(NotifyBase):
|
|||
'host': {
|
||||
'name': _('Hostname'),
|
||||
'type': 'string',
|
||||
'required': True,
|
||||
},
|
||||
'port': {
|
||||
'name': _('Port'),
|
||||
|
@ -194,6 +213,7 @@ class NotifyMatrix(NotifyBase):
|
|||
},
|
||||
'token': {
|
||||
'name': _('Access Token'),
|
||||
'private': True,
|
||||
'map_to': 'password',
|
||||
},
|
||||
'target_user': {
|
||||
|
@ -234,6 +254,12 @@ class NotifyMatrix(NotifyBase):
|
|||
'values': MATRIX_WEBHOOK_MODES,
|
||||
'default': MatrixWebhookMode.DISABLED,
|
||||
},
|
||||
'version': {
|
||||
'name': _('Matrix API Verion'),
|
||||
'type': 'choice:string',
|
||||
'values': MATRIX_VERSIONS,
|
||||
'default': MatrixVersion.V3,
|
||||
},
|
||||
'msgtype': {
|
||||
'name': _('Message Type'),
|
||||
'type': 'choice:string',
|
||||
|
@ -248,7 +274,7 @@ class NotifyMatrix(NotifyBase):
|
|||
},
|
||||
})
|
||||
|
||||
def __init__(self, targets=None, mode=None, msgtype=None,
|
||||
def __init__(self, targets=None, mode=None, msgtype=None, version=None,
|
||||
include_image=False, **kwargs):
|
||||
"""
|
||||
Initialize Matrix Object
|
||||
|
@ -282,6 +308,14 @@ class NotifyMatrix(NotifyBase):
|
|||
self.logger.warning(msg)
|
||||
raise TypeError(msg)
|
||||
|
||||
# Setup our version
|
||||
self.version = self.template_args['version']['default'] \
|
||||
if not isinstance(version, str) else version
|
||||
if self.version not in MATRIX_VERSIONS:
|
||||
msg = 'The version specified ({}) is invalid.'.format(version)
|
||||
self.logger.warning(msg)
|
||||
raise TypeError(msg)
|
||||
|
||||
# Setup our message type
|
||||
self.msgtype = self.template_args['msgtype']['default'] \
|
||||
if not isinstance(msgtype, str) else msgtype.lower()
|
||||
|
@ -521,7 +555,8 @@ class NotifyMatrix(NotifyBase):
|
|||
return payload
|
||||
|
||||
def _send_server_notification(self, body, title='',
|
||||
notify_type=NotifyType.INFO, **kwargs):
|
||||
notify_type=NotifyType.INFO, attach=None,
|
||||
**kwargs):
|
||||
"""
|
||||
Perform Direct Matrix Server Notification (no webhook)
|
||||
"""
|
||||
|
@ -548,6 +583,13 @@ class NotifyMatrix(NotifyBase):
|
|||
# Initiaize our error tracking
|
||||
has_error = False
|
||||
|
||||
attachments = None
|
||||
if attach and self.attachment_support:
|
||||
attachments = self._send_attachments(attach)
|
||||
if attachments is False:
|
||||
# take an early exit
|
||||
return False
|
||||
|
||||
while len(rooms) > 0:
|
||||
|
||||
# Get our room
|
||||
|
@ -568,23 +610,47 @@ class NotifyMatrix(NotifyBase):
|
|||
image_url = None if not self.include_image else \
|
||||
self.image_url(notify_type)
|
||||
|
||||
if image_url:
|
||||
# Define our payload
|
||||
image_payload = {
|
||||
'msgtype': 'm.image',
|
||||
'url': image_url,
|
||||
'body': '{}'.format(notify_type if not title else title),
|
||||
}
|
||||
# Build our path
|
||||
# Build our path
|
||||
if self.version == MatrixVersion.V3:
|
||||
path = '/rooms/{}/send/m.room.message/0'.format(
|
||||
NotifyMatrix.quote(room_id))
|
||||
|
||||
else:
|
||||
path = '/rooms/{}/send/m.room.message'.format(
|
||||
NotifyMatrix.quote(room_id))
|
||||
|
||||
# Post our content
|
||||
postokay, response = self._fetch(path, payload=image_payload)
|
||||
if not postokay:
|
||||
# Mark our failure
|
||||
has_error = True
|
||||
continue
|
||||
if self.version == MatrixVersion.V2:
|
||||
#
|
||||
# Attachments don't work beyond V2 at this time
|
||||
#
|
||||
if image_url:
|
||||
# Define our payload
|
||||
image_payload = {
|
||||
'msgtype': 'm.image',
|
||||
'url': image_url,
|
||||
'body': '{}'.format(
|
||||
notify_type if not title else title),
|
||||
}
|
||||
|
||||
# Post our content
|
||||
postokay, response = self._fetch(
|
||||
path, payload=image_payload)
|
||||
if not postokay:
|
||||
# Mark our failure
|
||||
has_error = True
|
||||
continue
|
||||
|
||||
if attachments:
|
||||
for attachment in attachments:
|
||||
attachment['room_id'] = room_id
|
||||
attachment['type'] = 'm.room.message'
|
||||
|
||||
postokay, response = self._fetch(
|
||||
path, payload=attachment)
|
||||
if not postokay:
|
||||
# Mark our failure
|
||||
has_error = True
|
||||
continue
|
||||
|
||||
# Define our payload
|
||||
payload = {
|
||||
|
@ -615,12 +681,10 @@ class NotifyMatrix(NotifyBase):
|
|||
)
|
||||
})
|
||||
|
||||
# Build our path
|
||||
path = '/rooms/{}/send/m.room.message'.format(
|
||||
NotifyMatrix.quote(room_id))
|
||||
|
||||
# Post our content
|
||||
postokay, response = self._fetch(path, payload=payload)
|
||||
method = 'PUT' if self.version == MatrixVersion.V3 else 'POST'
|
||||
postokay, response = self._fetch(
|
||||
path, payload=payload, method=method)
|
||||
if not postokay:
|
||||
# Notify our user
|
||||
self.logger.warning(
|
||||
|
@ -632,6 +696,62 @@ class NotifyMatrix(NotifyBase):
|
|||
|
||||
return not has_error
|
||||
|
||||
def _send_attachments(self, attach):
|
||||
"""
|
||||
Posts all of the provided attachments
|
||||
"""
|
||||
|
||||
payloads = []
|
||||
if self.version != MatrixVersion.V2:
|
||||
self.logger.warning(
|
||||
'Add ?v=2 to Apprise URL to support Attachments')
|
||||
return next((False for a in attach if not a), [])
|
||||
|
||||
for attachment in attach:
|
||||
if not attachment:
|
||||
# invalid attachment (bad file)
|
||||
return False
|
||||
|
||||
if not re.match(r'^image/', attachment.mimetype, re.I):
|
||||
# unsuppored at this time
|
||||
continue
|
||||
|
||||
postokay, response = \
|
||||
self._fetch('/upload', attachment=attachment)
|
||||
if not (postokay and isinstance(response, dict)):
|
||||
# Failed to perform upload
|
||||
return False
|
||||
|
||||
# If we get here, we'll have a response that looks like:
|
||||
# {
|
||||
# "content_uri": "mxc://example.com/a-unique-key"
|
||||
# }
|
||||
|
||||
if self.version == MatrixVersion.V3:
|
||||
# Prepare our payload
|
||||
payloads.append({
|
||||
"body": attachment.name,
|
||||
"info": {
|
||||
"mimetype": attachment.mimetype,
|
||||
"size": len(attachment),
|
||||
},
|
||||
"msgtype": "m.image",
|
||||
"url": response.get('content_uri'),
|
||||
})
|
||||
|
||||
else:
|
||||
# Prepare our payload
|
||||
payloads.append({
|
||||
"info": {
|
||||
"mimetype": attachment.mimetype,
|
||||
},
|
||||
"msgtype": "m.image",
|
||||
"body": "tta.webp",
|
||||
"url": response.get('content_uri'),
|
||||
})
|
||||
|
||||
return payloads
|
||||
|
||||
def _register(self):
|
||||
"""
|
||||
Register with the service if possible.
|
||||
|
@ -695,12 +815,23 @@ class NotifyMatrix(NotifyBase):
|
|||
'user/pass combo is missing.')
|
||||
return False
|
||||
|
||||
# Prepare our Registration Payload
|
||||
payload = {
|
||||
'type': 'm.login.password',
|
||||
'user': self.user,
|
||||
'password': self.password,
|
||||
}
|
||||
# Prepare our Authentication Payload
|
||||
if self.version == MatrixVersion.V3:
|
||||
payload = {
|
||||
'type': 'm.login.password',
|
||||
'identifier': {
|
||||
'type': 'm.id.user',
|
||||
'user': self.user,
|
||||
},
|
||||
'password': self.password,
|
||||
}
|
||||
|
||||
else:
|
||||
payload = {
|
||||
'type': 'm.login.password',
|
||||
'user': self.user,
|
||||
'password': self.password,
|
||||
}
|
||||
|
||||
# Build our URL
|
||||
postokay, response = self._fetch('/login', payload=payload)
|
||||
|
@ -970,7 +1101,8 @@ class NotifyMatrix(NotifyBase):
|
|||
|
||||
return None
|
||||
|
||||
def _fetch(self, path, payload=None, params=None, method='POST'):
|
||||
def _fetch(self, path, payload=None, params=None, attachment=None,
|
||||
method='POST'):
|
||||
"""
|
||||
Wrapper to request.post() to manage it's response better and make
|
||||
the send() function cleaner and easier to maintain.
|
||||
|
@ -983,6 +1115,7 @@ class NotifyMatrix(NotifyBase):
|
|||
headers = {
|
||||
'User-Agent': self.app_id,
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
}
|
||||
|
||||
if self.access_token is not None:
|
||||
|
@ -991,19 +1124,39 @@ class NotifyMatrix(NotifyBase):
|
|||
default_port = 443 if self.secure else 80
|
||||
|
||||
url = \
|
||||
'{schema}://{hostname}:{port}{matrix_api}{path}'.format(
|
||||
'{schema}://{hostname}{port}'.format(
|
||||
schema='https' if self.secure else 'http',
|
||||
hostname=self.host,
|
||||
port='' if self.port is None
|
||||
or self.port == default_port else self.port,
|
||||
matrix_api=MATRIX_V2_API_PATH,
|
||||
path=path)
|
||||
or self.port == default_port else f':{self.port}')
|
||||
|
||||
if path == '/upload':
|
||||
if self.version == MatrixVersion.V3:
|
||||
url += MATRIX_V3_MEDIA_PATH + path
|
||||
|
||||
else:
|
||||
url += MATRIX_V2_MEDIA_PATH + path
|
||||
|
||||
params = {'filename': attachment.name}
|
||||
with open(attachment.path, 'rb') as fp:
|
||||
payload = fp.read()
|
||||
|
||||
# Update our content type
|
||||
headers['Content-Type'] = attachment.mimetype
|
||||
|
||||
else:
|
||||
if self.version == MatrixVersion.V3:
|
||||
url += MATRIX_V3_API_PATH + path
|
||||
|
||||
else:
|
||||
url += MATRIX_V2_API_PATH + path
|
||||
|
||||
# Our response object
|
||||
response = {}
|
||||
|
||||
# fetch function
|
||||
fn = requests.post if method == 'POST' else requests.get
|
||||
fn = requests.post if method == 'POST' else (
|
||||
requests.put if method == 'PUT' else requests.get)
|
||||
|
||||
# Define how many attempts we'll make if we get caught in a throttle
|
||||
# event
|
||||
|
@ -1024,13 +1177,16 @@ class NotifyMatrix(NotifyBase):
|
|||
try:
|
||||
r = fn(
|
||||
url,
|
||||
data=dumps(payload),
|
||||
data=dumps(payload) if not attachment else payload,
|
||||
params=params,
|
||||
headers=headers,
|
||||
verify=self.verify_certificate,
|
||||
timeout=self.request_timeout,
|
||||
)
|
||||
|
||||
self.logger.debug(
|
||||
'Matrix Response: code=%d, %s' % (
|
||||
r.status_code, str(r.content)))
|
||||
response = loads(r.content)
|
||||
|
||||
if r.status_code == 429:
|
||||
|
@ -1094,6 +1250,13 @@ class NotifyMatrix(NotifyBase):
|
|||
# Return; we're done
|
||||
return (False, response)
|
||||
|
||||
except (OSError, IOError) as e:
|
||||
self.logger.warning(
|
||||
'An I/O error occurred while reading {}.'.format(
|
||||
attachment.name if attachment else 'unknown file'))
|
||||
self.logger.debug('I/O Exception: %s' % str(e))
|
||||
return (False, {})
|
||||
|
||||
return (True, response)
|
||||
|
||||
# If we get here, we ran out of retries
|
||||
|
@ -1160,6 +1323,7 @@ class NotifyMatrix(NotifyBase):
|
|||
params = {
|
||||
'image': 'yes' if self.include_image else 'no',
|
||||
'mode': self.mode,
|
||||
'version': self.version,
|
||||
'msgtype': self.msgtype,
|
||||
}
|
||||
|
||||
|
@ -1196,6 +1360,13 @@ class NotifyMatrix(NotifyBase):
|
|||
params=NotifyMatrix.urlencode(params),
|
||||
)
|
||||
|
||||
def __len__(self):
|
||||
"""
|
||||
Returns the number of targets associated with this notification
|
||||
"""
|
||||
targets = len(self.rooms)
|
||||
return targets if targets > 0 else 1
|
||||
|
||||
@staticmethod
|
||||
def parse_url(url):
|
||||
"""
|
||||
|
@ -1250,6 +1421,14 @@ class NotifyMatrix(NotifyBase):
|
|||
if 'token' in results['qsd'] and len(results['qsd']['token']):
|
||||
results['password'] = NotifyMatrix.unquote(results['qsd']['token'])
|
||||
|
||||
# Support the use of the version= or v= keyword
|
||||
if 'version' in results['qsd'] and len(results['qsd']['version']):
|
||||
results['version'] = \
|
||||
NotifyMatrix.unquote(results['qsd']['version'])
|
||||
|
||||
elif 'v' in results['qsd'] and len(results['qsd']['v']):
|
||||
results['version'] = NotifyMatrix.unquote(results['qsd']['v'])
|
||||
|
||||
return results
|
||||
|
||||
@staticmethod
|
||||
|
@ -1259,7 +1438,7 @@ class NotifyMatrix(NotifyBase):
|
|||
"""
|
||||
|
||||
result = re.match(
|
||||
r'^https?://webhooks\.t2bot\.io/api/v1/matrix/hook/'
|
||||
r'^https?://webhooks\.t2bot\.io/api/v[0-9]+/matrix/hook/'
|
||||
r'(?P<webhook_token>[A-Z0-9_-]+)/?'
|
||||
r'(?P<params>\?.+)?$', url, re.I)
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# BSD 3-Clause License
|
||||
# BSD 2-Clause License
|
||||
#
|
||||
# Apprise - Push Notification Library.
|
||||
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
|
||||
|
@ -14,10 +14,6 @@
|
|||
# this list of conditions and the following disclaimer in the documentation
|
||||
# and/or other materials provided with the distribution.
|
||||
#
|
||||
# 3. Neither the name of the copyright holder nor the names of its
|
||||
# contributors may be used to endorse or promote products derived from
|
||||
# this software without specific prior written permission.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
|
@ -91,11 +87,11 @@ class NotifyMattermost(NotifyBase):
|
|||
# Define object templates
|
||||
templates = (
|
||||
'{schema}://{host}/{token}',
|
||||
'{schema}://{host}/{token}:{port}',
|
||||
'{schema}://{host}:{port}/{token}',
|
||||
'{schema}://{host}/{fullpath}/{token}',
|
||||
'{schema}://{host}:{port}/{fullpath}/{token}',
|
||||
'{schema}://{botname}@{host}/{token}',
|
||||
'{schema}://{botname}@{host}:{port}/{token}',
|
||||
'{schema}://{host}/{fullpath}/{token}',
|
||||
'{schema}://{host}/{fullpath}{token}:{port}',
|
||||
'{schema}://{botname}@{host}/{fullpath}/{token}',
|
||||
'{schema}://{botname}@{host}:{port}/{fullpath}/{token}',
|
||||
)
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# BSD 3-Clause License
|
||||
# BSD 2-Clause License
|
||||
#
|
||||
# Apprise - Push Notification Library.
|
||||
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
|
||||
|
@ -14,10 +14,6 @@
|
|||
# this list of conditions and the following disclaimer in the documentation
|
||||
# and/or other materials provided with the distribution.
|
||||
#
|
||||
# 3. Neither the name of the copyright holder nor the names of its
|
||||
# contributors may be used to endorse or promote products derived from
|
||||
# this software without specific prior written permission.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
|
@ -311,6 +307,13 @@ class NotifyMessageBird(NotifyBase):
|
|||
[NotifyMessageBird.quote(x, safe='') for x in self.targets]),
|
||||
params=NotifyMessageBird.urlencode(params))
|
||||
|
||||
def __len__(self):
|
||||
"""
|
||||
Returns the number of targets associated with this notification
|
||||
"""
|
||||
targets = len(self.targets)
|
||||
return targets if targets > 0 else 1
|
||||
|
||||
@staticmethod
|
||||
def parse_url(url):
|
||||
"""
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# BSD 3-Clause License
|
||||
# BSD 2-Clause License
|
||||
#
|
||||
# Apprise - Push Notification Library.
|
||||
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
|
||||
|
@ -14,10 +14,6 @@
|
|||
# this list of conditions and the following disclaimer in the documentation
|
||||
# and/or other materials provided with the distribution.
|
||||
#
|
||||
# 3. Neither the name of the copyright holder nor the names of its
|
||||
# contributors may be used to endorse or promote products derived from
|
||||
# this software without specific prior written permission.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
|
@ -29,6 +25,7 @@
|
|||
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
# POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
# 1. visit https://misskey-hub.net/ and see what it's all about if you want.
|
||||
# Choose a service you want to create an account on from here:
|
||||
# https://misskey-hub.net/en/instances.html
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# BSD 3-Clause License
|
||||
# BSD 2-Clause License
|
||||
#
|
||||
# Apprise - Push Notification Library.
|
||||
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
|
||||
|
@ -14,10 +14,6 @@
|
|||
# this list of conditions and the following disclaimer in the documentation
|
||||
# and/or other materials provided with the distribution.
|
||||
#
|
||||
# 3. Neither the name of the copyright holder nor the names of its
|
||||
# contributors may be used to endorse or promote products derived from
|
||||
# this software without specific prior written permission.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
|
@ -67,6 +63,8 @@ class NotifyNextcloud(NotifyBase):
|
|||
|
||||
# Define object templates
|
||||
templates = (
|
||||
'{schema}://{host}/{targets}',
|
||||
'{schema}://{host}:{port}/{targets}',
|
||||
'{schema}://{user}:{password}@{host}/{targets}',
|
||||
'{schema}://{user}:{password}@{host}:{port}/{targets}',
|
||||
)
|
||||
|
@ -116,6 +114,10 @@ class NotifyNextcloud(NotifyBase):
|
|||
'min': 1,
|
||||
'default': 21,
|
||||
},
|
||||
'url_prefix': {
|
||||
'name': _('URL Prefix'),
|
||||
'type': 'string',
|
||||
},
|
||||
'to': {
|
||||
'alias_of': 'targets',
|
||||
},
|
||||
|
@ -129,17 +131,15 @@ class NotifyNextcloud(NotifyBase):
|
|||
},
|
||||
}
|
||||
|
||||
def __init__(self, targets=None, version=None, headers=None, **kwargs):
|
||||
def __init__(self, targets=None, version=None, headers=None,
|
||||
url_prefix=None, **kwargs):
|
||||
"""
|
||||
Initialize Nextcloud Object
|
||||
"""
|
||||
super().__init__(**kwargs)
|
||||
|
||||
# Store our targets
|
||||
self.targets = parse_list(targets)
|
||||
if len(self.targets) == 0:
|
||||
msg = 'At least one Nextcloud target user must be specified.'
|
||||
self.logger.warning(msg)
|
||||
raise TypeError(msg)
|
||||
|
||||
self.version = self.template_args['version']['default']
|
||||
if version is not None:
|
||||
|
@ -155,6 +155,10 @@ class NotifyNextcloud(NotifyBase):
|
|||
self.logger.warning(msg)
|
||||
raise TypeError(msg)
|
||||
|
||||
# Support URL Prefix
|
||||
self.url_prefix = '' if not url_prefix \
|
||||
else url_prefix.strip('/')
|
||||
|
||||
self.headers = {}
|
||||
if headers:
|
||||
# Store our extra headers
|
||||
|
@ -167,6 +171,11 @@ class NotifyNextcloud(NotifyBase):
|
|||
Perform Nextcloud Notification
|
||||
"""
|
||||
|
||||
if len(self.targets) == 0:
|
||||
# There were no services to notify
|
||||
self.logger.warning('There were no Nextcloud targets to notify.')
|
||||
return False
|
||||
|
||||
# Prepare our Header
|
||||
headers = {
|
||||
'User-Agent': self.app_id,
|
||||
|
@ -198,11 +207,11 @@ class NotifyNextcloud(NotifyBase):
|
|||
auth = (self.user, self.password)
|
||||
|
||||
# Nextcloud URL based on version used
|
||||
notify_url = '{schema}://{host}/ocs/v2.php/'\
|
||||
notify_url = '{schema}://{host}/{url_prefix}/ocs/v2.php/'\
|
||||
'apps/admin_notifications/' \
|
||||
'api/v1/notifications/{target}' \
|
||||
if self.version < 21 else \
|
||||
'{schema}://{host}/ocs/v2.php/'\
|
||||
'{schema}://{host}/{url_prefix}/ocs/v2.php/'\
|
||||
'apps/notifications/'\
|
||||
'api/v2/admin_notifications/{target}'
|
||||
|
||||
|
@ -210,6 +219,7 @@ class NotifyNextcloud(NotifyBase):
|
|||
schema='https' if self.secure else 'http',
|
||||
host=self.host if not isinstance(self.port, int)
|
||||
else '{}:{}'.format(self.host, self.port),
|
||||
url_prefix=self.url_prefix,
|
||||
target=target,
|
||||
)
|
||||
|
||||
|
@ -279,6 +289,9 @@ class NotifyNextcloud(NotifyBase):
|
|||
# Set our version
|
||||
params['version'] = str(self.version)
|
||||
|
||||
if self.url_prefix:
|
||||
params['url_prefix'] = self.url_prefix
|
||||
|
||||
# Extend our parameters
|
||||
params.update(self.url_parameters(privacy=privacy, *args, **kwargs))
|
||||
|
||||
|
@ -312,6 +325,13 @@ class NotifyNextcloud(NotifyBase):
|
|||
params=NotifyNextcloud.urlencode(params),
|
||||
)
|
||||
|
||||
def __len__(self):
|
||||
"""
|
||||
Returns the number of targets associated with this notification
|
||||
"""
|
||||
targets = len(self.targets)
|
||||
return targets if targets else 1
|
||||
|
||||
@staticmethod
|
||||
def parse_url(url):
|
||||
"""
|
||||
|
@ -339,6 +359,12 @@ class NotifyNextcloud(NotifyBase):
|
|||
results['version'] = \
|
||||
NotifyNextcloud.unquote(results['qsd']['version'])
|
||||
|
||||
# Support URL Prefixes
|
||||
if 'url_prefix' in results['qsd'] \
|
||||
and len(results['qsd']['url_prefix']):
|
||||
results['url_prefix'] = \
|
||||
NotifyNextcloud.unquote(results['qsd']['url_prefix'])
|
||||
|
||||
# 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'] = {
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# BSD 3-Clause License
|
||||
# BSD 2-Clause License
|
||||
#
|
||||
# Apprise - Push Notification Library.
|
||||
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
|
||||
|
@ -14,10 +14,6 @@
|
|||
# this list of conditions and the following disclaimer in the documentation
|
||||
# and/or other materials provided with the distribution.
|
||||
#
|
||||
# 3. Neither the name of the copyright holder nor the names of its
|
||||
# contributors may be used to endorse or promote products derived from
|
||||
# this software without specific prior written permission.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
|
@ -96,6 +92,11 @@ class NotifyNextcloudTalk(NotifyBase):
|
|||
'private': True,
|
||||
'required': True,
|
||||
},
|
||||
'target_room_id': {
|
||||
'name': _('Room ID'),
|
||||
'type': 'string',
|
||||
'map_to': 'targets',
|
||||
},
|
||||
'targets': {
|
||||
'name': _('Targets'),
|
||||
'type': 'list:string',
|
||||
|
@ -103,6 +104,14 @@ class NotifyNextcloudTalk(NotifyBase):
|
|||
},
|
||||
})
|
||||
|
||||
# Define our template arguments
|
||||
template_args = dict(NotifyBase.template_args, **{
|
||||
'url_prefix': {
|
||||
'name': _('URL Prefix'),
|
||||
'type': 'string',
|
||||
},
|
||||
})
|
||||
|
||||
# Define any kwargs we're using
|
||||
template_kwargs = {
|
||||
'headers': {
|
||||
|
@ -111,7 +120,7 @@ class NotifyNextcloudTalk(NotifyBase):
|
|||
},
|
||||
}
|
||||
|
||||
def __init__(self, targets=None, headers=None, **kwargs):
|
||||
def __init__(self, targets=None, headers=None, url_prefix=None, **kwargs):
|
||||
"""
|
||||
Initialize Nextcloud Talk Object
|
||||
"""
|
||||
|
@ -122,11 +131,12 @@ class NotifyNextcloudTalk(NotifyBase):
|
|||
self.logger.warning(msg)
|
||||
raise TypeError(msg)
|
||||
|
||||
# Store our targets
|
||||
self.targets = parse_list(targets)
|
||||
if len(self.targets) == 0:
|
||||
msg = 'At least one Nextcloud Talk Room ID must be specified.'
|
||||
self.logger.warning(msg)
|
||||
raise TypeError(msg)
|
||||
|
||||
# Support URL Prefix
|
||||
self.url_prefix = '' if not url_prefix \
|
||||
else url_prefix.strip('/')
|
||||
|
||||
self.headers = {}
|
||||
if headers:
|
||||
|
@ -140,6 +150,12 @@ class NotifyNextcloudTalk(NotifyBase):
|
|||
Perform Nextcloud Talk Notification
|
||||
"""
|
||||
|
||||
if len(self.targets) == 0:
|
||||
# There were no services to notify
|
||||
self.logger.warning(
|
||||
'There were no Nextcloud Talk targets to notify.')
|
||||
return False
|
||||
|
||||
# Prepare our Header
|
||||
headers = {
|
||||
'User-Agent': self.app_id,
|
||||
|
@ -171,13 +187,14 @@ class NotifyNextcloudTalk(NotifyBase):
|
|||
}
|
||||
|
||||
# Nextcloud Talk URL
|
||||
notify_url = '{schema}://{host}'\
|
||||
notify_url = '{schema}://{host}/{url_prefix}'\
|
||||
'/ocs/v2.php/apps/spreed/api/v1/chat/{target}'
|
||||
|
||||
notify_url = notify_url.format(
|
||||
schema='https' if self.secure else 'http',
|
||||
host=self.host if not isinstance(self.port, int)
|
||||
else '{}:{}'.format(self.host, self.port),
|
||||
url_prefix=self.url_prefix,
|
||||
target=target,
|
||||
)
|
||||
|
||||
|
@ -200,7 +217,8 @@ class NotifyNextcloudTalk(NotifyBase):
|
|||
verify=self.verify_certificate,
|
||||
timeout=self.request_timeout,
|
||||
)
|
||||
if r.status_code != requests.codes.created:
|
||||
if r.status_code not in (
|
||||
requests.codes.created, requests.codes.ok):
|
||||
# We had a problem
|
||||
status_str = \
|
||||
NotifyNextcloudTalk.http_response_code_lookup(
|
||||
|
@ -240,6 +258,14 @@ class NotifyNextcloudTalk(NotifyBase):
|
|||
Returns the URL built dynamically based on specified arguments.
|
||||
"""
|
||||
|
||||
# Our default set of parameters
|
||||
params = self.url_parameters(privacy=privacy, *args, **kwargs)
|
||||
|
||||
# Append our headers into our parameters
|
||||
params.update({'+{}'.format(k): v for k, v in self.headers.items()})
|
||||
if self.url_prefix:
|
||||
params['url_prefix'] = self.url_prefix
|
||||
|
||||
# Determine Authentication
|
||||
auth = '{user}:{password}@'.format(
|
||||
user=NotifyNextcloudTalk.quote(self.user, safe=''),
|
||||
|
@ -249,7 +275,7 @@ class NotifyNextcloudTalk(NotifyBase):
|
|||
|
||||
default_port = 443 if self.secure else 80
|
||||
|
||||
return '{schema}://{auth}{hostname}{port}/{targets}' \
|
||||
return '{schema}://{auth}{hostname}{port}/{targets}?{params}' \
|
||||
.format(
|
||||
schema=self.secure_protocol
|
||||
if self.secure else self.protocol,
|
||||
|
@ -261,8 +287,16 @@ class NotifyNextcloudTalk(NotifyBase):
|
|||
else ':{}'.format(self.port),
|
||||
targets='/'.join([NotifyNextcloudTalk.quote(x)
|
||||
for x in self.targets]),
|
||||
params=NotifyNextcloudTalk.urlencode(params),
|
||||
)
|
||||
|
||||
def __len__(self):
|
||||
"""
|
||||
Returns the number of targets associated with this notification
|
||||
"""
|
||||
targets = len(self.targets)
|
||||
return targets if targets else 1
|
||||
|
||||
@staticmethod
|
||||
def parse_url(url):
|
||||
"""
|
||||
|
@ -280,6 +314,12 @@ class NotifyNextcloudTalk(NotifyBase):
|
|||
results['targets'] = \
|
||||
NotifyNextcloudTalk.split_path(results['fullpath'])
|
||||
|
||||
# Support URL Prefixes
|
||||
if 'url_prefix' in results['qsd'] \
|
||||
and len(results['qsd']['url_prefix']):
|
||||
results['url_prefix'] = \
|
||||
NotifyNextcloudTalk.unquote(results['qsd']['url_prefix'])
|
||||
|
||||
# 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'] = {
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# BSD 3-Clause License
|
||||
# BSD 2-Clause License
|
||||
#
|
||||
# Apprise - Push Notification Library.
|
||||
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
|
||||
|
@ -14,10 +14,6 @@
|
|||
# this list of conditions and the following disclaimer in the documentation
|
||||
# and/or other materials provided with the distribution.
|
||||
#
|
||||
# 3. Neither the name of the copyright holder nor the names of its
|
||||
# contributors may be used to endorse or promote products derived from
|
||||
# this software without specific prior written permission.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
|
@ -112,12 +108,12 @@ class NotifyNotica(NotifyBase):
|
|||
'{schema}://{user}:{password}@{host}:{port}/{token}',
|
||||
|
||||
# Self-hosted notica servers (with custom path)
|
||||
'{schema}://{host}{path}{token}',
|
||||
'{schema}://{host}:{port}{path}{token}',
|
||||
'{schema}://{user}@{host}{path}{token}',
|
||||
'{schema}://{user}@{host}:{port}{path}{token}',
|
||||
'{schema}://{user}:{password}@{host}{path}{token}',
|
||||
'{schema}://{user}:{password}@{host}:{port}{path}{token}',
|
||||
'{schema}://{host}{path}/{token}',
|
||||
'{schema}://{host}:{port}/{path}/{token}',
|
||||
'{schema}://{user}@{host}/{path}/{token}',
|
||||
'{schema}://{user}@{host}:{port}{path}/{token}',
|
||||
'{schema}://{user}:{password}@{host}{path}/{token}',
|
||||
'{schema}://{user}:{password}@{host}:{port}/{path}/{token}',
|
||||
)
|
||||
|
||||
# Define our template tokens
|
||||
|
|
472
lib/apprise/plugins/NotifyNotifiarr.py
Normal file
472
lib/apprise/plugins/NotifyNotifiarr.py
Normal file
|
@ -0,0 +1,472 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# BSD 2-Clause License
|
||||
#
|
||||
# Apprise - Push Notification Library.
|
||||
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
|
||||
#
|
||||
# Redistribution and use in source and binary forms, with or without
|
||||
# modification, are permitted provided that the following conditions are met:
|
||||
#
|
||||
# 1. Redistributions of source code must retain the above copyright notice,
|
||||
# this list of conditions and the following disclaimer.
|
||||
#
|
||||
# 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
# this list of conditions and the following disclaimer in the documentation
|
||||
# and/or other materials provided with the distribution.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
|
||||
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
||||
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
# POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
import re
|
||||
import requests
|
||||
from json import dumps
|
||||
from itertools import chain
|
||||
|
||||
from .NotifyBase import NotifyBase
|
||||
from ..common import NotifyType
|
||||
from ..AppriseLocale import gettext_lazy as _
|
||||
from ..common import NotifyImageSize
|
||||
from ..utils import parse_list, parse_bool
|
||||
from ..utils import validate_regex
|
||||
|
||||
# Used to break path apart into list of channels
|
||||
CHANNEL_LIST_DELIM = re.compile(r'[ \t\r\n,#\\/]+')
|
||||
|
||||
CHANNEL_REGEX = re.compile(
|
||||
r'^\s*(\#|\%35)?(?P<channel>[0-9]+)', re.I)
|
||||
|
||||
# For API Details see:
|
||||
# https://notifiarr.wiki/Client/Installation
|
||||
|
||||
# Another good example:
|
||||
# https://notifiarr.wiki/en/Website/ \
|
||||
# Integrations/Passthrough#payload-example-1
|
||||
|
||||
|
||||
class NotifyNotifiarr(NotifyBase):
|
||||
"""
|
||||
A wrapper for Notifiarr Notifications
|
||||
"""
|
||||
|
||||
# The default descriptive name associated with the Notification
|
||||
service_name = 'Notifiarr'
|
||||
|
||||
# The services URL
|
||||
service_url = 'https://notifiarr.com/'
|
||||
|
||||
# The default secure protocol
|
||||
secure_protocol = 'notifiarr'
|
||||
|
||||
# A URL that takes you to the setup/help of the specific protocol
|
||||
setup_url = 'https://github.com/caronc/apprise/wiki/Notify_notifiarr'
|
||||
|
||||
# The Notification URL
|
||||
notify_url = 'https://notifiarr.com/api/v1/notification/apprise'
|
||||
|
||||
# Notifiarr Throttling (knowing in advance reduces 429 responses)
|
||||
# define('NOTIFICATION_LIMIT_SECOND_USER', 5);
|
||||
# define('NOTIFICATION_LIMIT_SECOND_PATRON', 15);
|
||||
|
||||
# Throttle requests ever so slightly
|
||||
request_rate_per_sec = 0.04
|
||||
|
||||
# Allows the user to specify the NotifyImageSize object
|
||||
image_size = NotifyImageSize.XY_256
|
||||
|
||||
# Define object templates
|
||||
templates = (
|
||||
'{schema}://{apikey}/{targets}',
|
||||
)
|
||||
|
||||
# Define our apikeys; these are the minimum apikeys required required to
|
||||
# be passed into this function (as arguments). The syntax appends any
|
||||
# previously defined in the base package and builds onto them
|
||||
template_tokens = dict(NotifyBase.template_tokens, **{
|
||||
'apikey': {
|
||||
'name': _('Token'),
|
||||
'type': 'string',
|
||||
'required': True,
|
||||
'private': True,
|
||||
},
|
||||
'target_channel': {
|
||||
'name': _('Target Channel'),
|
||||
'type': 'string',
|
||||
'prefix': '#',
|
||||
'map_to': 'targets',
|
||||
},
|
||||
'targets': {
|
||||
'name': _('Targets'),
|
||||
'type': 'list:string',
|
||||
'required': True,
|
||||
},
|
||||
})
|
||||
|
||||
# Define our template arguments
|
||||
template_args = dict(NotifyBase.template_args, **{
|
||||
'key': {
|
||||
'alias_of': 'apikey',
|
||||
},
|
||||
'apikey': {
|
||||
'alias_of': 'apikey',
|
||||
},
|
||||
'discord_user': {
|
||||
'name': _('Ping Discord User'),
|
||||
'type': 'int',
|
||||
},
|
||||
'discord_role': {
|
||||
'name': _('Ping Discord Role'),
|
||||
'type': 'int',
|
||||
},
|
||||
'event': {
|
||||
'name': _('Discord Event ID'),
|
||||
'type': 'int',
|
||||
},
|
||||
'image': {
|
||||
'name': _('Include Image'),
|
||||
'type': 'bool',
|
||||
'default': False,
|
||||
'map_to': 'include_image',
|
||||
},
|
||||
'source': {
|
||||
'name': _('Source'),
|
||||
'type': 'string',
|
||||
},
|
||||
'from': {
|
||||
'alias_of': 'source'
|
||||
},
|
||||
'to': {
|
||||
'alias_of': 'targets',
|
||||
},
|
||||
})
|
||||
|
||||
def __init__(self, apikey=None, include_image=None,
|
||||
discord_user=None, discord_role=None,
|
||||
event=None, targets=None, source=None, **kwargs):
|
||||
"""
|
||||
Initialize Notifiarr Object
|
||||
|
||||
headers can be a dictionary of key/value pairs that you want to
|
||||
additionally include as part of the server headers to post with
|
||||
|
||||
"""
|
||||
super().__init__(**kwargs)
|
||||
|
||||
self.apikey = apikey
|
||||
if not self.apikey:
|
||||
msg = 'An invalid Notifiarr APIKey ' \
|
||||
'({}) was specified.'.format(apikey)
|
||||
self.logger.warning(msg)
|
||||
raise TypeError(msg)
|
||||
|
||||
# Place a thumbnail image inline with the message body
|
||||
self.include_image = include_image \
|
||||
if isinstance(include_image, bool) \
|
||||
else self.template_args['image']['default']
|
||||
|
||||
# Set up our user if specified
|
||||
self.discord_user = 0
|
||||
if discord_user:
|
||||
try:
|
||||
self.discord_user = int(discord_user)
|
||||
|
||||
except (ValueError, TypeError):
|
||||
msg = 'An invalid Notifiarr User ID ' \
|
||||
'({}) was specified.'.format(discord_user)
|
||||
self.logger.warning(msg)
|
||||
raise TypeError(msg)
|
||||
|
||||
# Set up our role if specified
|
||||
self.discord_role = 0
|
||||
if discord_role:
|
||||
try:
|
||||
self.discord_role = int(discord_role)
|
||||
|
||||
except (ValueError, TypeError):
|
||||
msg = 'An invalid Notifiarr Role ID ' \
|
||||
'({}) was specified.'.format(discord_role)
|
||||
self.logger.warning(msg)
|
||||
raise TypeError(msg)
|
||||
|
||||
# Prepare our source (if set)
|
||||
self.source = validate_regex(source)
|
||||
|
||||
self.event = 0
|
||||
if event:
|
||||
try:
|
||||
self.event = int(event)
|
||||
|
||||
except (ValueError, TypeError):
|
||||
msg = 'An invalid Notifiarr Discord Event ID ' \
|
||||
'({}) was specified.'.format(event)
|
||||
self.logger.warning(msg)
|
||||
raise TypeError(msg)
|
||||
|
||||
# Prepare our targets
|
||||
self.targets = {
|
||||
'channels': [],
|
||||
'invalid': [],
|
||||
}
|
||||
|
||||
for target in parse_list(targets):
|
||||
result = CHANNEL_REGEX.match(target)
|
||||
if result:
|
||||
# Store role information
|
||||
self.targets['channels'].append(int(result.group('channel')))
|
||||
continue
|
||||
|
||||
self.logger.warning(
|
||||
'Dropped invalid channel '
|
||||
'({}) specified.'.format(target),
|
||||
)
|
||||
self.targets['invalid'].append(target)
|
||||
|
||||
return
|
||||
|
||||
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.source:
|
||||
params['source'] = self.source
|
||||
|
||||
if self.discord_user:
|
||||
params['discord_user'] = self.discord_user
|
||||
|
||||
if self.discord_role:
|
||||
params['discord_role'] = self.discord_role
|
||||
|
||||
if self.event:
|
||||
params['event'] = self.event
|
||||
|
||||
# Extend our parameters
|
||||
params.update(self.url_parameters(privacy=privacy, *args, **kwargs))
|
||||
|
||||
return '{schema}://{apikey}' \
|
||||
'/{targets}?{params}'.format(
|
||||
schema=self.secure_protocol,
|
||||
apikey=self.pprint(self.apikey, privacy, safe=''),
|
||||
targets='/'.join(
|
||||
[NotifyNotifiarr.quote(x, safe='+#@') for x in chain(
|
||||
# Channels
|
||||
['#{}'.format(x) for x in self.targets['channels']],
|
||||
# Pass along the same invalid entries as were provided
|
||||
self.targets['invalid'],
|
||||
)]),
|
||||
params=NotifyNotifiarr.urlencode(params),
|
||||
)
|
||||
|
||||
def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs):
|
||||
"""
|
||||
Perform Notifiarr Notification
|
||||
"""
|
||||
|
||||
if not self.targets['channels']:
|
||||
# There were no services to notify
|
||||
self.logger.warning(
|
||||
'There were no Notifiarr channels to notify.')
|
||||
return False
|
||||
|
||||
# No error to start with
|
||||
has_error = False
|
||||
|
||||
# Acquire image_url
|
||||
image_url = self.image_url(notify_type)
|
||||
|
||||
for idx, channel in enumerate(self.targets['channels']):
|
||||
# prepare Notifiarr Object
|
||||
payload = {
|
||||
'source': self.source if self.source else self.app_id,
|
||||
'type': notify_type,
|
||||
'notification': {
|
||||
'update': True if self.event else False,
|
||||
'name': self.app_id,
|
||||
'event': str(self.event)
|
||||
if self.event else "",
|
||||
},
|
||||
'discord': {
|
||||
'color': self.color(notify_type),
|
||||
'ping': {
|
||||
'pingUser': self.discord_user
|
||||
if not idx and self.discord_user else 0,
|
||||
'pingRole': self.discord_role
|
||||
if not idx and self.discord_role else 0,
|
||||
},
|
||||
'text': {
|
||||
'title': title,
|
||||
'content': '',
|
||||
'description': body,
|
||||
'footer': self.app_desc,
|
||||
},
|
||||
'ids': {
|
||||
'channel': channel,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if self.include_image and image_url:
|
||||
payload['discord']['text']['icon'] = image_url
|
||||
payload['discord']['images'] = {
|
||||
'thumbnail': image_url,
|
||||
}
|
||||
|
||||
if not self._send(payload):
|
||||
has_error = True
|
||||
|
||||
return not has_error
|
||||
|
||||
def _send(self, payload):
|
||||
"""
|
||||
Send notification
|
||||
"""
|
||||
self.logger.debug('Notifiarr POST URL: %s (cert_verify=%r)' % (
|
||||
self.notify_url, self.verify_certificate,
|
||||
))
|
||||
self.logger.debug('Notifiarr Payload: %s' % str(payload))
|
||||
|
||||
# Prepare HTTP Headers
|
||||
headers = {
|
||||
'User-Agent': self.app_id,
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'text/plain',
|
||||
'X-api-Key': self.apikey,
|
||||
}
|
||||
|
||||
# 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 < 200 or r.status_code >= 300:
|
||||
# We had a problem
|
||||
status_str = \
|
||||
NotifyNotifiarr.http_response_code_lookup(r.status_code)
|
||||
|
||||
self.logger.warning(
|
||||
'Failed to send Notifiarr %s notification: '
|
||||
'%serror=%s.',
|
||||
status_str,
|
||||
', ' if status_str else '',
|
||||
str(r.status_code))
|
||||
|
||||
self.logger.debug('Response Details:\r\n{}'.format(r.content))
|
||||
|
||||
# Return; we're done
|
||||
return False
|
||||
|
||||
else:
|
||||
self.logger.info('Sent Notifiarr notification.')
|
||||
|
||||
except requests.RequestException as e:
|
||||
self.logger.warning(
|
||||
'A Connection error occurred sending Notifiarr '
|
||||
'Chat notification to %s.' % self.host)
|
||||
self.logger.debug('Socket Exception: %s' % str(e))
|
||||
|
||||
# Return; we're done
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def __len__(self):
|
||||
"""
|
||||
Returns the number of targets associated with this notification
|
||||
"""
|
||||
targets = len(self.targets['channels']) + len(self.targets['invalid'])
|
||||
return targets if targets > 0 else 1
|
||||
|
||||
@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 channels
|
||||
results['targets'] = NotifyNotifiarr.split_path(results['fullpath'])
|
||||
|
||||
if 'discord_user' in results['qsd'] and \
|
||||
len(results['qsd']['discord_user']):
|
||||
results['discord_user'] = \
|
||||
NotifyNotifiarr.unquote(
|
||||
results['qsd']['discord_user'])
|
||||
|
||||
if 'discord_role' in results['qsd'] and \
|
||||
len(results['qsd']['discord_role']):
|
||||
results['discord_role'] = \
|
||||
NotifyNotifiarr.unquote(results['qsd']['discord_role'])
|
||||
|
||||
if 'event' in results['qsd'] and \
|
||||
len(results['qsd']['event']):
|
||||
results['event'] = \
|
||||
NotifyNotifiarr.unquote(results['qsd']['event'])
|
||||
|
||||
# Include images with our message
|
||||
results['include_image'] = \
|
||||
parse_bool(results['qsd'].get('image', False))
|
||||
|
||||
# Track if we need to extract the hostname as a target
|
||||
host_is_potential_target = False
|
||||
|
||||
if 'source' in results['qsd'] and len(results['qsd']['source']):
|
||||
results['source'] = \
|
||||
NotifyNotifiarr.unquote(results['qsd']['source'])
|
||||
|
||||
elif 'from' in results['qsd'] and len(results['qsd']['from']):
|
||||
results['source'] = \
|
||||
NotifyNotifiarr.unquote(results['qsd']['from'])
|
||||
|
||||
# Set our apikey if found as an argument
|
||||
if 'apikey' in results['qsd'] and len(results['qsd']['apikey']):
|
||||
results['apikey'] = \
|
||||
NotifyNotifiarr.unquote(results['qsd']['apikey'])
|
||||
|
||||
host_is_potential_target = True
|
||||
|
||||
elif 'key' in results['qsd'] and len(results['qsd']['key']):
|
||||
results['apikey'] = \
|
||||
NotifyNotifiarr.unquote(results['qsd']['key'])
|
||||
|
||||
host_is_potential_target = True
|
||||
|
||||
else:
|
||||
# Pop the first element (this is the api key)
|
||||
results['apikey'] = \
|
||||
NotifyNotifiarr.unquote(results['host'])
|
||||
|
||||
if host_is_potential_target is True and results['host']:
|
||||
results['targets'].append(NotifyNotifiarr.unquote(results['host']))
|
||||
|
||||
# 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, CHANNEL_LIST_DELIM.split(
|
||||
NotifyNotifiarr.unquote(results['qsd']['to'])))]
|
||||
|
||||
return results
|
|
@ -1,5 +1,5 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# BSD 3-Clause License
|
||||
# BSD 2-Clause License
|
||||
#
|
||||
# Apprise - Push Notification Library.
|
||||
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
|
||||
|
@ -14,10 +14,6 @@
|
|||
# this list of conditions and the following disclaimer in the documentation
|
||||
# and/or other materials provided with the distribution.
|
||||
#
|
||||
# 3. Neither the name of the copyright holder nor the names of its
|
||||
# contributors may be used to endorse or promote products derived from
|
||||
# this software without specific prior written permission.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# BSD 3-Clause License
|
||||
# BSD 2-Clause License
|
||||
#
|
||||
# Apprise - Push Notification Library.
|
||||
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
|
||||
|
@ -14,10 +14,6 @@
|
|||
# this list of conditions and the following disclaimer in the documentation
|
||||
# and/or other materials provided with the distribution.
|
||||
#
|
||||
# 3. Neither the name of the copyright holder nor the names of its
|
||||
# contributors may be used to endorse or promote products derived from
|
||||
# this software without specific prior written permission.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
|
@ -172,6 +168,9 @@ class NotifyNtfy(NotifyBase):
|
|||
# Default upstream/cloud host if none is defined
|
||||
cloud_notify_url = 'https://ntfy.sh'
|
||||
|
||||
# Support attachments
|
||||
attachment_support = True
|
||||
|
||||
# Allows the user to specify the NotifyImageSize object
|
||||
image_size = NotifyImageSize.XY_256
|
||||
|
||||
|
@ -405,14 +404,14 @@ class NotifyNtfy(NotifyBase):
|
|||
# Retrieve our topic
|
||||
topic = topics.pop()
|
||||
|
||||
if attach:
|
||||
if attach and self.attachment_support:
|
||||
# We need to upload our payload first so that we can source it
|
||||
# in remaining messages
|
||||
for no, attachment in enumerate(attach):
|
||||
|
||||
# First message only includes the text
|
||||
_body = body if not no else None
|
||||
_title = title if not no else None
|
||||
# First message only includes the text (if defined)
|
||||
_body = body if not no and body else None
|
||||
_title = title if not no and title else None
|
||||
|
||||
# Perform some simple error checking
|
||||
if not attachment:
|
||||
|
@ -453,10 +452,6 @@ class NotifyNtfy(NotifyBase):
|
|||
'User-Agent': self.app_id,
|
||||
}
|
||||
|
||||
# Some default values for our request object to which we'll update
|
||||
# depending on what our payload is
|
||||
files = None
|
||||
|
||||
# See https://ntfy.sh/docs/publish/#publish-as-json
|
||||
data = {}
|
||||
|
||||
|
@ -494,11 +489,23 @@ class NotifyNtfy(NotifyBase):
|
|||
data['topic'] = topic
|
||||
virt_payload = data
|
||||
|
||||
if self.attach:
|
||||
virt_payload['attach'] = self.attach
|
||||
|
||||
if self.filename:
|
||||
virt_payload['filename'] = self.filename
|
||||
|
||||
else:
|
||||
# Point our payload to our parameters
|
||||
virt_payload = params
|
||||
notify_url += '/{topic}'.format(topic=topic)
|
||||
|
||||
# Prepare our Header
|
||||
virt_payload['filename'] = attach.name
|
||||
|
||||
with open(attach.path, 'rb') as fp:
|
||||
data = fp.read()
|
||||
|
||||
if image_url:
|
||||
headers['X-Icon'] = image_url
|
||||
|
||||
|
@ -523,18 +530,6 @@ class NotifyNtfy(NotifyBase):
|
|||
if self.__tags:
|
||||
headers['X-Tags'] = ",".join(self.__tags)
|
||||
|
||||
if isinstance(attach, AttachBase):
|
||||
# Prepare our Header
|
||||
params['filename'] = attach.name
|
||||
|
||||
# prepare our files object
|
||||
files = {'file': (attach.name, open(attach.path, 'rb'))}
|
||||
|
||||
elif self.attach is not None:
|
||||
data['attach'] = self.attach
|
||||
if self.filename is not None:
|
||||
data['filename'] = self.filename
|
||||
|
||||
self.logger.debug('ntfy POST URL: %s (cert_verify=%r)' % (
|
||||
notify_url, self.verify_certificate,
|
||||
))
|
||||
|
@ -547,13 +542,15 @@ class NotifyNtfy(NotifyBase):
|
|||
# Default response type
|
||||
response = None
|
||||
|
||||
if not attach:
|
||||
data = dumps(data)
|
||||
|
||||
try:
|
||||
r = requests.post(
|
||||
notify_url,
|
||||
params=params if params else None,
|
||||
data=dumps(data) if data else None,
|
||||
data=data,
|
||||
headers=headers,
|
||||
files=files,
|
||||
auth=auth,
|
||||
verify=self.verify_certificate,
|
||||
timeout=self.request_timeout,
|
||||
|
@ -608,7 +605,6 @@ class NotifyNtfy(NotifyBase):
|
|||
notify_url) + 'notification.'
|
||||
)
|
||||
self.logger.debug('Socket Exception: %s' % str(e))
|
||||
return False, response
|
||||
|
||||
except (OSError, IOError) as e:
|
||||
self.logger.warning(
|
||||
|
@ -616,13 +612,8 @@ class NotifyNtfy(NotifyBase):
|
|||
attach.name if isinstance(attach, AttachBase)
|
||||
else virt_payload))
|
||||
self.logger.debug('I/O Exception: %s' % str(e))
|
||||
return False, response
|
||||
|
||||
finally:
|
||||
# Close our file (if it's open) stored in the second element
|
||||
# of our files tuple (index 1)
|
||||
if files:
|
||||
files['file'][1].close()
|
||||
return False, response
|
||||
|
||||
def url(self, privacy=False, *args, **kwargs):
|
||||
"""
|
||||
|
@ -698,6 +689,12 @@ class NotifyNtfy(NotifyBase):
|
|||
params=NotifyNtfy.urlencode(params)
|
||||
)
|
||||
|
||||
def __len__(self):
|
||||
"""
|
||||
Returns the number of targets associated with this notification
|
||||
"""
|
||||
return len(self.topics)
|
||||
|
||||
@staticmethod
|
||||
def parse_url(url):
|
||||
"""
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# BSD 3-Clause License
|
||||
# BSD 2-Clause License
|
||||
#
|
||||
# Apprise - Push Notification Library.
|
||||
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
|
||||
|
@ -14,10 +14,6 @@
|
|||
# this list of conditions and the following disclaimer in the documentation
|
||||
# and/or other materials provided with the distribution.
|
||||
#
|
||||
# 3. Neither the name of the copyright holder nor the names of its
|
||||
# contributors may be used to endorse or promote products derived from
|
||||
# this software without specific prior written permission.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
|
@ -148,8 +144,13 @@ class NotifyOffice365(NotifyBase):
|
|||
'private': True,
|
||||
'required': True,
|
||||
},
|
||||
'target_email': {
|
||||
'name': _('Target Email'),
|
||||
'type': 'string',
|
||||
'map_to': 'targets',
|
||||
},
|
||||
'targets': {
|
||||
'name': _('Target Emails'),
|
||||
'name': _('Targets'),
|
||||
'type': 'list:string',
|
||||
},
|
||||
})
|
||||
|
@ -596,6 +597,12 @@ class NotifyOffice365(NotifyBase):
|
|||
safe='') for e in self.targets]),
|
||||
params=NotifyOffice365.urlencode(params))
|
||||
|
||||
def __len__(self):
|
||||
"""
|
||||
Returns the number of targets associated with this notification
|
||||
"""
|
||||
return len(self.targets)
|
||||
|
||||
@staticmethod
|
||||
def parse_url(url):
|
||||
"""
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# BSD 3-Clause License
|
||||
# BSD 2-Clause License
|
||||
#
|
||||
# Apprise - Push Notification Library.
|
||||
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
|
||||
|
@ -14,10 +14,6 @@
|
|||
# this list of conditions and the following disclaimer in the documentation
|
||||
# and/or other materials provided with the distribution.
|
||||
#
|
||||
# 3. Neither the name of the copyright holder nor the names of its
|
||||
# contributors may be used to endorse or promote products derived from
|
||||
# this software without specific prior written permission.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
|
@ -51,7 +47,7 @@ from ..utils import is_email
|
|||
from ..AppriseLocale import gettext_lazy as _
|
||||
|
||||
|
||||
class OneSignalCategory(NotifyBase):
|
||||
class OneSignalCategory:
|
||||
"""
|
||||
We define the different category types that we can notify via OneSignal
|
||||
"""
|
||||
|
@ -92,7 +88,7 @@ class NotifyOneSignal(NotifyBase):
|
|||
image_size = NotifyImageSize.XY_72
|
||||
|
||||
# The maximum allowable batch sizes per message
|
||||
maximum_batch_size = 2000
|
||||
default_batch_size = 2000
|
||||
|
||||
# Define object templates
|
||||
templates = (
|
||||
|
@ -121,7 +117,7 @@ class NotifyOneSignal(NotifyBase):
|
|||
'private': True,
|
||||
'required': True,
|
||||
},
|
||||
'target_device': {
|
||||
'target_player': {
|
||||
'name': _('Target Player ID'),
|
||||
'type': 'string',
|
||||
'map_to': 'targets',
|
||||
|
@ -146,6 +142,7 @@ class NotifyOneSignal(NotifyBase):
|
|||
'targets': {
|
||||
'name': _('Targets'),
|
||||
'type': 'list:string',
|
||||
'required': True,
|
||||
},
|
||||
})
|
||||
|
||||
|
@ -204,7 +201,7 @@ class NotifyOneSignal(NotifyBase):
|
|||
raise TypeError(msg)
|
||||
|
||||
# Prepare Batch Mode Flag
|
||||
self.batch_size = self.maximum_batch_size if batch else 1
|
||||
self.batch_size = self.default_batch_size if batch else 1
|
||||
|
||||
# Place a thumbnail image inline with the message body
|
||||
self.include_image = include_image
|
||||
|
@ -432,6 +429,26 @@ class NotifyOneSignal(NotifyBase):
|
|||
params=NotifyOneSignal.urlencode(params),
|
||||
)
|
||||
|
||||
def __len__(self):
|
||||
"""
|
||||
Returns the number of targets associated with this notification
|
||||
"""
|
||||
#
|
||||
# Factor batch into calculation
|
||||
#
|
||||
if self.batch_size > 1:
|
||||
# Batches can only be sent by group (you can't combine groups into
|
||||
# a single batch)
|
||||
total_targets = 0
|
||||
for k, m in self.targets.items():
|
||||
targets = len(m)
|
||||
total_targets += int(targets / self.batch_size) + \
|
||||
(1 if targets % self.batch_size else 0)
|
||||
return total_targets
|
||||
|
||||
# Normal batch count; just count the targets
|
||||
return sum([len(m) for _, m in self.targets.items()])
|
||||
|
||||
@staticmethod
|
||||
def parse_url(url):
|
||||
"""
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# BSD 3-Clause License
|
||||
# BSD 2-Clause License
|
||||
#
|
||||
# Apprise - Push Notification Library.
|
||||
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
|
||||
|
@ -14,10 +14,6 @@
|
|||
# this list of conditions and the following disclaimer in the documentation
|
||||
# and/or other materials provided with the distribution.
|
||||
#
|
||||
# 3. Neither the name of the copyright holder nor the names of its
|
||||
# contributors may be used to endorse or promote products derived from
|
||||
# this software without specific prior written permission.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
|
@ -172,7 +168,7 @@ class NotifyOpsgenie(NotifyBase):
|
|||
opsgenie_default_region = OpsgenieRegion.US
|
||||
|
||||
# The maximum allowable targets within a notification
|
||||
maximum_batch_size = 50
|
||||
default_batch_size = 50
|
||||
|
||||
# Define object templates
|
||||
templates = (
|
||||
|
@ -308,7 +304,7 @@ class NotifyOpsgenie(NotifyBase):
|
|||
self.details.update(details)
|
||||
|
||||
# Prepare Batch Mode Flag
|
||||
self.batch_size = self.maximum_batch_size if batch else 1
|
||||
self.batch_size = self.default_batch_size if batch else 1
|
||||
|
||||
# Assign our tags (if defined)
|
||||
self.__tags = parse_list(tags)
|
||||
|
@ -536,6 +532,20 @@ class NotifyOpsgenie(NotifyBase):
|
|||
for x in self.targets]),
|
||||
params=NotifyOpsgenie.urlencode(params))
|
||||
|
||||
def __len__(self):
|
||||
"""
|
||||
Returns the number of targets associated with this notification
|
||||
"""
|
||||
#
|
||||
# Factor batch into calculation
|
||||
#
|
||||
targets = len(self.targets)
|
||||
if self.batch_size > 1:
|
||||
targets = int(targets / self.batch_size) + \
|
||||
(1 if targets % self.batch_size else 0)
|
||||
|
||||
return targets if targets > 0 else 1
|
||||
|
||||
@staticmethod
|
||||
def parse_url(url):
|
||||
"""
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# BSD 3-Clause License
|
||||
# BSD 2-Clause License
|
||||
#
|
||||
# Apprise - Push Notification Library.
|
||||
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
|
||||
|
@ -14,10 +14,6 @@
|
|||
# this list of conditions and the following disclaimer in the documentation
|
||||
# and/or other materials provided with the distribution.
|
||||
#
|
||||
# 3. Neither the name of the copyright holder nor the names of its
|
||||
# contributors may be used to endorse or promote products derived from
|
||||
# this software without specific prior written permission.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
|
@ -142,7 +138,7 @@ class NotifyPagerDuty(NotifyBase):
|
|||
},
|
||||
# Optional but triggers V2 API
|
||||
'integrationkey': {
|
||||
'name': _('Routing Key'),
|
||||
'name': _('Integration Key'),
|
||||
'type': 'string',
|
||||
'private': True,
|
||||
'required': True
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# BSD 3-Clause License
|
||||
# BSD 2-Clause License
|
||||
#
|
||||
# Apprise - Push Notification Library.
|
||||
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
|
||||
|
@ -14,10 +14,6 @@
|
|||
# this list of conditions and the following disclaimer in the documentation
|
||||
# and/or other materials provided with the distribution.
|
||||
#
|
||||
# 3. Neither the name of the copyright holder nor the names of its
|
||||
# contributors may be used to endorse or promote products derived from
|
||||
# this software without specific prior written permission.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# BSD 3-Clause License
|
||||
# BSD 2-Clause License
|
||||
#
|
||||
# Apprise - Push Notification Library.
|
||||
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
|
||||
|
@ -14,10 +14,6 @@
|
|||
# this list of conditions and the following disclaimer in the documentation
|
||||
# and/or other materials provided with the distribution.
|
||||
#
|
||||
# 3. Neither the name of the copyright holder nor the names of its
|
||||
# contributors may be used to endorse or promote products derived from
|
||||
# this software without specific prior written permission.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
|
@ -30,8 +26,6 @@
|
|||
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
# POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
# Official API reference: https://developer.gitter.im/docs/user-resource
|
||||
|
||||
import re
|
||||
import requests
|
||||
from json import dumps
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# BSD 3-Clause License
|
||||
# BSD 2-Clause License
|
||||
#
|
||||
# Apprise - Push Notification Library.
|
||||
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
|
||||
|
@ -14,10 +14,6 @@
|
|||
# this list of conditions and the following disclaimer in the documentation
|
||||
# and/or other materials provided with the distribution.
|
||||
#
|
||||
# 3. Neither the name of the copyright holder nor the names of its
|
||||
# contributors may be used to endorse or promote products derived from
|
||||
# this software without specific prior written permission.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
|
@ -93,6 +89,7 @@ class NotifyPopcornNotify(NotifyBase):
|
|||
'targets': {
|
||||
'name': _('Targets'),
|
||||
'type': 'list:string',
|
||||
'required': True,
|
||||
}
|
||||
})
|
||||
|
||||
|
@ -265,6 +262,21 @@ class NotifyPopcornNotify(NotifyBase):
|
|||
[NotifyPopcornNotify.quote(x, safe='') for x in self.targets]),
|
||||
params=NotifyPopcornNotify.urlencode(params))
|
||||
|
||||
def __len__(self):
|
||||
"""
|
||||
Returns the number of targets associated with this notification
|
||||
"""
|
||||
#
|
||||
# Factor batch into calculation
|
||||
#
|
||||
batch_size = 1 if not self.batch else self.default_batch_size
|
||||
targets = len(self.targets)
|
||||
if batch_size > 1:
|
||||
targets = int(targets / batch_size) + \
|
||||
(1 if targets % batch_size else 0)
|
||||
|
||||
return targets
|
||||
|
||||
@staticmethod
|
||||
def parse_url(url):
|
||||
"""
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# BSD 3-Clause License
|
||||
# BSD 2-Clause License
|
||||
#
|
||||
# Apprise - Push Notification Library.
|
||||
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
|
||||
|
@ -14,10 +14,6 @@
|
|||
# this list of conditions and the following disclaimer in the documentation
|
||||
# and/or other materials provided with the distribution.
|
||||
#
|
||||
# 3. Neither the name of the copyright holder nor the names of its
|
||||
# contributors may be used to endorse or promote products derived from
|
||||
# this software without specific prior written permission.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# BSD 3-Clause License
|
||||
# BSD 2-Clause License
|
||||
#
|
||||
# Apprise - Push Notification Library.
|
||||
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
|
||||
|
@ -14,10 +14,6 @@
|
|||
# this list of conditions and the following disclaimer in the documentation
|
||||
# and/or other materials provided with the distribution.
|
||||
#
|
||||
# 3. Neither the name of the copyright holder nor the names of its
|
||||
# contributors may be used to endorse or promote products derived from
|
||||
# this software without specific prior written permission.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
|
@ -75,6 +71,9 @@ class NotifyPushBullet(NotifyBase):
|
|||
# PushBullet uses the http protocol with JSON requests
|
||||
notify_url = 'https://api.pushbullet.com/v2/{}'
|
||||
|
||||
# Support attachments
|
||||
attachment_support = True
|
||||
|
||||
# Define object templates
|
||||
templates = (
|
||||
'{schema}://{accesstoken}',
|
||||
|
@ -150,7 +149,7 @@ class NotifyPushBullet(NotifyBase):
|
|||
# Build a list of our attachments
|
||||
attachments = []
|
||||
|
||||
if attach:
|
||||
if attach and self.attachment_support:
|
||||
# We need to upload our payload first so that we can source it
|
||||
# in remaining messages
|
||||
for attachment in attach:
|
||||
|
@ -261,14 +260,15 @@ class NotifyPushBullet(NotifyBase):
|
|||
"PushBullet recipient {} parsed as a device"
|
||||
.format(recipient))
|
||||
|
||||
okay, response = self._send(
|
||||
self.notify_url.format('pushes'), payload)
|
||||
if not okay:
|
||||
has_error = True
|
||||
continue
|
||||
if body:
|
||||
okay, response = self._send(
|
||||
self.notify_url.format('pushes'), payload)
|
||||
if not okay:
|
||||
has_error = True
|
||||
continue
|
||||
|
||||
self.logger.info(
|
||||
'Sent PushBullet notification to "%s".' % (recipient))
|
||||
self.logger.info(
|
||||
'Sent PushBullet notification to "%s".' % (recipient))
|
||||
|
||||
for attach_payload in attachments:
|
||||
# Send our attachments to our same user (already prepared as
|
||||
|
@ -406,6 +406,12 @@ class NotifyPushBullet(NotifyBase):
|
|||
targets=targets,
|
||||
params=NotifyPushBullet.urlencode(params))
|
||||
|
||||
def __len__(self):
|
||||
"""
|
||||
Returns the number of targets associated with this notification
|
||||
"""
|
||||
return len(self.targets)
|
||||
|
||||
@staticmethod
|
||||
def parse_url(url):
|
||||
"""
|
||||
|
|
218
lib/apprise/plugins/NotifyPushDeer.py
Normal file
218
lib/apprise/plugins/NotifyPushDeer.py
Normal file
|
@ -0,0 +1,218 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# BSD 2-Clause License
|
||||
#
|
||||
# Apprise - Push Notification Library.
|
||||
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
|
||||
#
|
||||
# Redistribution and use in source and binary forms, with or without
|
||||
# modification, are permitted provided that the following conditions are met:
|
||||
#
|
||||
# 1. Redistributions of source code must retain the above copyright notice,
|
||||
# this list of conditions and the following disclaimer.
|
||||
#
|
||||
# 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
# this list of conditions and the following disclaimer in the documentation
|
||||
# and/or other materials provided with the distribution.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
|
||||
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
||||
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
# POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
import requests
|
||||
|
||||
from ..common import NotifyType
|
||||
from .NotifyBase import NotifyBase
|
||||
from ..utils import validate_regex
|
||||
from ..AppriseLocale import gettext_lazy as _
|
||||
|
||||
# Syntax:
|
||||
# schan://{key}/
|
||||
|
||||
|
||||
class NotifyPushDeer(NotifyBase):
|
||||
"""
|
||||
A wrapper for PushDeer Notifications
|
||||
"""
|
||||
|
||||
# The default descriptive name associated with the Notification
|
||||
service_name = 'PushDeer'
|
||||
|
||||
# The services URL
|
||||
service_url = 'https://www.pushdeer.com/'
|
||||
|
||||
# Insecure Protocol Access
|
||||
protocol = 'pushdeer'
|
||||
|
||||
# Secure Protocol
|
||||
secure_protocol = 'pushdeers'
|
||||
|
||||
# A URL that takes you to the setup/help of the specific protocol
|
||||
setup_url = 'https://github.com/caronc/apprise/wiki/Notify_PushDeer'
|
||||
|
||||
# Default hostname
|
||||
default_hostname = 'api2.pushdeer.com'
|
||||
|
||||
# PushDeer API
|
||||
notify_url = '{schema}://{host}:{port}/message/push?pushkey={pushKey}'
|
||||
|
||||
# Define object templates
|
||||
templates = (
|
||||
'{schema}://{pushkey}',
|
||||
'{schema}://{host}/{pushkey}',
|
||||
'{schema}://{host}:{port}/{pushkey}',
|
||||
)
|
||||
|
||||
# Define our template tokens
|
||||
template_tokens = dict(NotifyBase.template_tokens, **{
|
||||
'host': {
|
||||
'name': _('Hostname'),
|
||||
'type': 'string',
|
||||
},
|
||||
'port': {
|
||||
'name': _('Port'),
|
||||
'type': 'int',
|
||||
'min': 1,
|
||||
'max': 65535,
|
||||
},
|
||||
'pushkey': {
|
||||
'name': _('Pushkey'),
|
||||
'type': 'string',
|
||||
'private': True,
|
||||
'required': True,
|
||||
'regex': (r'^[a-z0-9]+$', 'i'),
|
||||
},
|
||||
})
|
||||
|
||||
def __init__(self, pushkey, **kwargs):
|
||||
"""
|
||||
Initialize PushDeer Object
|
||||
"""
|
||||
super().__init__(**kwargs)
|
||||
|
||||
# PushKey (associated with project)
|
||||
self.push_key = validate_regex(
|
||||
pushkey, *self.template_tokens['pushkey']['regex'])
|
||||
if not self.push_key:
|
||||
msg = 'An invalid PushDeer API Pushkey ' \
|
||||
'({}) was specified.'.format(pushkey)
|
||||
self.logger.warning(msg)
|
||||
raise TypeError(msg)
|
||||
|
||||
def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs):
|
||||
"""
|
||||
Perform PushDeer Notification
|
||||
"""
|
||||
|
||||
# Prepare our persistent_notification.create payload
|
||||
payload = {
|
||||
'text': title if title else body,
|
||||
'type': 'text',
|
||||
'desp': body if title else '',
|
||||
}
|
||||
|
||||
# Set our schema
|
||||
schema = 'https' if self.secure else 'http'
|
||||
|
||||
# Set host
|
||||
host = self.default_hostname
|
||||
if self.host:
|
||||
host = self.host
|
||||
|
||||
# Set port
|
||||
port = 443 if self.secure else 80
|
||||
if self.port:
|
||||
port = self.port
|
||||
|
||||
# Our Notification URL
|
||||
notify_url = self.notify_url.format(
|
||||
schema=schema, host=host, port=port, pushKey=self.push_key)
|
||||
|
||||
# Some Debug Logging
|
||||
self.logger.debug('PushDeer URL: {} (cert_verify={})'.format(
|
||||
notify_url, self.verify_certificate))
|
||||
self.logger.debug('PushDeer Payload: {}'.format(payload))
|
||||
|
||||
# Always call throttle before any remote server i/o is made
|
||||
self.throttle()
|
||||
|
||||
try:
|
||||
r = requests.post(
|
||||
notify_url,
|
||||
data=payload,
|
||||
timeout=self.request_timeout,
|
||||
)
|
||||
|
||||
if r.status_code != requests.codes.ok:
|
||||
# We had a problem
|
||||
status_str = \
|
||||
NotifyPushDeer.http_response_code_lookup(
|
||||
r.status_code)
|
||||
|
||||
self.logger.warning(
|
||||
'Failed to send PushDeer notification: '
|
||||
'{}{}error={}.'.format(
|
||||
status_str,
|
||||
', ' if status_str else '',
|
||||
r.status_code))
|
||||
|
||||
self.logger.debug(
|
||||
'Response Details:\r\n{}'.format(r.content))
|
||||
return False
|
||||
|
||||
else:
|
||||
self.logger.info('Sent PushDeer notification.')
|
||||
|
||||
except requests.RequestException as e:
|
||||
self.logger.warning(
|
||||
'A Connection error occured sending PushDeer '
|
||||
'notification.'
|
||||
)
|
||||
self.logger.debug('Socket Exception: %s' % str(e))
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def url(self, privacy=False):
|
||||
"""
|
||||
Returns the URL built dynamically based on specified arguments.
|
||||
"""
|
||||
|
||||
if self.host:
|
||||
url = '{schema}://{host}{port}/{pushkey}'
|
||||
else:
|
||||
url = '{schema}://{pushkey}'
|
||||
|
||||
return url.format(
|
||||
schema=self.secure_protocol if self.secure else self.protocol,
|
||||
host=self.host,
|
||||
port='' if not self.port else ':{}'.format(self.port),
|
||||
pushkey=self.pprint(self.push_key, privacy, safe=''))
|
||||
|
||||
@staticmethod
|
||||
def parse_url(url):
|
||||
"""
|
||||
Parses the URL and returns enough arguments that can allow
|
||||
us to substantiate this object.
|
||||
"""
|
||||
results = NotifyBase.parse_url(url, verify_host=False)
|
||||
if not results:
|
||||
# We're done early as we couldn't parse the URL
|
||||
return results
|
||||
|
||||
fullpaths = NotifyPushDeer.split_path(results['fullpath'])
|
||||
|
||||
if len(fullpaths) == 0:
|
||||
results['pushkey'] = results['host']
|
||||
results['host'] = None
|
||||
else:
|
||||
results['pushkey'] = fullpaths.pop()
|
||||
|
||||
return results
|
221
lib/apprise/plugins/NotifyPushMe.py
Normal file
221
lib/apprise/plugins/NotifyPushMe.py
Normal file
|
@ -0,0 +1,221 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# BSD 2-Clause License
|
||||
#
|
||||
# Apprise - Push Notification Library.
|
||||
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
|
||||
#
|
||||
# Redistribution and use in source and binary forms, with or without
|
||||
# modification, are permitted provided that the following conditions are met:
|
||||
#
|
||||
# 1. Redistributions of source code must retain the above copyright notice,
|
||||
# this list of conditions and the following disclaimer.
|
||||
#
|
||||
# 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
# this list of conditions and the following disclaimer in the documentation
|
||||
# and/or other materials provided with the distribution.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
|
||||
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
||||
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
# POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
import requests
|
||||
|
||||
from .NotifyBase import NotifyBase
|
||||
from ..common import NotifyType
|
||||
from ..common import NotifyFormat
|
||||
from ..utils import validate_regex
|
||||
from ..utils import parse_bool
|
||||
from ..AppriseLocale import gettext_lazy as _
|
||||
|
||||
|
||||
class NotifyPushMe(NotifyBase):
|
||||
"""
|
||||
A wrapper for PushMe Notifications
|
||||
"""
|
||||
|
||||
# The default descriptive name associated with the Notification
|
||||
service_name = 'PushMe'
|
||||
|
||||
# The services URL
|
||||
service_url = 'https://push.i-i.me/'
|
||||
|
||||
# Insecure protocol (for those self hosted requests)
|
||||
protocol = 'pushme'
|
||||
|
||||
# A URL that takes you to the setup/help of the specific protocol
|
||||
setup_url = 'https://github.com/caronc/apprise/wiki/Notify_pushme'
|
||||
|
||||
# PushMe URL
|
||||
notify_url = 'https://push.i-i.me/'
|
||||
|
||||
# Define object templates
|
||||
templates = (
|
||||
'{schema}://{token}',
|
||||
)
|
||||
|
||||
# Define our template tokens
|
||||
template_tokens = dict(NotifyBase.template_tokens, **{
|
||||
'token': {
|
||||
'name': _('Token'),
|
||||
'type': 'string',
|
||||
'private': True,
|
||||
'required': True,
|
||||
},
|
||||
})
|
||||
|
||||
# Define our template arguments
|
||||
template_args = dict(NotifyBase.template_args, **{
|
||||
'token': {
|
||||
'alias_of': 'token',
|
||||
},
|
||||
'push_key': {
|
||||
'alias_of': 'token',
|
||||
},
|
||||
'status': {
|
||||
'name': _('Show Status'),
|
||||
'type': 'bool',
|
||||
'default': True,
|
||||
},
|
||||
})
|
||||
|
||||
def __init__(self, token, status=None, **kwargs):
|
||||
"""
|
||||
Initialize PushMe Object
|
||||
"""
|
||||
super().__init__(**kwargs)
|
||||
|
||||
# Token (associated with project)
|
||||
self.token = validate_regex(token)
|
||||
if not self.token:
|
||||
msg = 'An invalid PushMe Token ' \
|
||||
'({}) was specified.'.format(token)
|
||||
self.logger.warning(msg)
|
||||
raise TypeError(msg)
|
||||
|
||||
# Set Status type
|
||||
self.status = status
|
||||
|
||||
return
|
||||
|
||||
def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs):
|
||||
"""
|
||||
Perform PushMe Notification
|
||||
"""
|
||||
|
||||
headers = {
|
||||
'User-Agent': self.app_id,
|
||||
}
|
||||
|
||||
# Prepare our payload
|
||||
params = {
|
||||
'push_key': self.token,
|
||||
'title': title if not self.status
|
||||
else '{} {}'.format(self.asset.ascii(notify_type), title),
|
||||
'content': body,
|
||||
'type': 'markdown'
|
||||
if self.notify_format == NotifyFormat.MARKDOWN else 'text'
|
||||
}
|
||||
|
||||
self.logger.debug('PushMe POST URL: %s (cert_verify=%r)' % (
|
||||
self.notify_url, self.verify_certificate,
|
||||
))
|
||||
self.logger.debug('PushMe Payload: %s' % str(params))
|
||||
|
||||
# Always call throttle before any remote server i/o is made
|
||||
self.throttle()
|
||||
|
||||
try:
|
||||
r = requests.post(
|
||||
self.notify_url,
|
||||
params=params,
|
||||
headers=headers,
|
||||
verify=self.verify_certificate,
|
||||
timeout=self.request_timeout,
|
||||
)
|
||||
if r.status_code != requests.codes.ok:
|
||||
# We had a problem
|
||||
status_str = \
|
||||
NotifyPushMe.http_response_code_lookup(r.status_code)
|
||||
|
||||
self.logger.warning(
|
||||
'Failed to send PushMe 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 PushMe notification.')
|
||||
|
||||
except requests.RequestException as e:
|
||||
self.logger.warning(
|
||||
'A Connection error occurred sending PushMe notification.',
|
||||
)
|
||||
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 = {
|
||||
'status': 'yes' if self.status else 'no',
|
||||
}
|
||||
|
||||
# Extend our parameters
|
||||
params.update(self.url_parameters(privacy=privacy, *args, **kwargs))
|
||||
|
||||
# Official URLs are easy to assemble
|
||||
return '{schema}://{token}/?{params}'.format(
|
||||
schema=self.protocol,
|
||||
token=self.pprint(self.token, privacy, safe=''),
|
||||
params=NotifyPushMe.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
|
||||
|
||||
# Store our token using the host
|
||||
results['token'] = NotifyPushMe.unquote(results['host'])
|
||||
|
||||
# The 'token' makes it easier to use yaml configuration
|
||||
if 'token' in results['qsd'] and len(results['qsd']['token']):
|
||||
results['token'] = NotifyPushMe.unquote(results['qsd']['token'])
|
||||
|
||||
elif 'push_key' in results['qsd'] and len(results['qsd']['push_key']):
|
||||
# Support 'push_key' if specified
|
||||
results['token'] = NotifyPushMe.unquote(results['qsd']['push_key'])
|
||||
|
||||
# Get status switch
|
||||
results['status'] = \
|
||||
parse_bool(results['qsd'].get('status', True))
|
||||
|
||||
return results
|
|
@ -1,5 +1,5 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# BSD 3-Clause License
|
||||
# BSD 2-Clause License
|
||||
#
|
||||
# Apprise - Push Notification Library.
|
||||
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
|
||||
|
@ -14,10 +14,6 @@
|
|||
# this list of conditions and the following disclaimer in the documentation
|
||||
# and/or other materials provided with the distribution.
|
||||
#
|
||||
# 3. Neither the name of the copyright holder nor the names of its
|
||||
# contributors may be used to endorse or promote products derived from
|
||||
# this software without specific prior written permission.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
|
@ -336,6 +332,9 @@ class NotifyPushSafer(NotifyBase):
|
|||
# The default secure protocol
|
||||
secure_protocol = 'psafers'
|
||||
|
||||
# Support attachments
|
||||
attachment_support = True
|
||||
|
||||
# Number of requests to a allow per second
|
||||
request_rate_per_sec = 1.2
|
||||
|
||||
|
@ -546,7 +545,7 @@ class NotifyPushSafer(NotifyBase):
|
|||
# Initialize our list of attachments
|
||||
attachments = []
|
||||
|
||||
if attach:
|
||||
if attach and self.attachment_support:
|
||||
# We need to upload our payload first so that we can source it
|
||||
# in remaining messages
|
||||
for attachment in attach:
|
||||
|
@ -794,6 +793,12 @@ class NotifyPushSafer(NotifyBase):
|
|||
targets=targets,
|
||||
params=NotifyPushSafer.urlencode(params))
|
||||
|
||||
def __len__(self):
|
||||
"""
|
||||
Returns the number of targets associated with this notification
|
||||
"""
|
||||
return len(self.targets)
|
||||
|
||||
@staticmethod
|
||||
def parse_url(url):
|
||||
"""
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# BSD 3-Clause License
|
||||
# BSD 2-Clause License
|
||||
#
|
||||
# Apprise - Push Notification Library.
|
||||
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
|
||||
|
@ -14,10 +14,6 @@
|
|||
# this list of conditions and the following disclaimer in the documentation
|
||||
# and/or other materials provided with the distribution.
|
||||
#
|
||||
# 3. Neither the name of the copyright holder nor the names of its
|
||||
# contributors may be used to endorse or promote products derived from
|
||||
# this software without specific prior written permission.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
|
@ -329,6 +325,13 @@ class NotifyPushed(NotifyBase):
|
|||
)]),
|
||||
params=NotifyPushed.urlencode(params))
|
||||
|
||||
def __len__(self):
|
||||
"""
|
||||
Returns the number of targets associated with this notification
|
||||
"""
|
||||
targets = len(self.channels) + len(self.users)
|
||||
return targets if targets > 0 else 1
|
||||
|
||||
@staticmethod
|
||||
def parse_url(url):
|
||||
"""
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# BSD 3-Clause License
|
||||
# BSD 2-Clause License
|
||||
#
|
||||
# Apprise - Push Notification Library.
|
||||
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
|
||||
|
@ -14,10 +14,6 @@
|
|||
# this list of conditions and the following disclaimer in the documentation
|
||||
# and/or other materials provided with the distribution.
|
||||
#
|
||||
# 3. Neither the name of the copyright holder nor the names of its
|
||||
# contributors may be used to endorse or promote products derived from
|
||||
# this software without specific prior written permission.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# BSD 3-Clause License
|
||||
# BSD 2-Clause License
|
||||
#
|
||||
# Apprise - Push Notification Library.
|
||||
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
|
||||
|
@ -14,10 +14,6 @@
|
|||
# this list of conditions and the following disclaimer in the documentation
|
||||
# and/or other materials provided with the distribution.
|
||||
#
|
||||
# 3. Neither the name of the copyright holder nor the names of its
|
||||
# contributors may be used to endorse or promote products derived from
|
||||
# this software without specific prior written permission.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
|
@ -32,6 +28,7 @@
|
|||
|
||||
import re
|
||||
import requests
|
||||
from itertools import chain
|
||||
|
||||
from .NotifyBase import NotifyBase
|
||||
from ..common import NotifyType
|
||||
|
@ -46,7 +43,7 @@ from ..attachment.AttachBase import AttachBase
|
|||
PUSHOVER_SEND_TO_ALL = 'ALL_DEVICES'
|
||||
|
||||
# Used to detect a Device
|
||||
VALIDATE_DEVICE = re.compile(r'^[a-z0-9_]{1,25}$', re.I)
|
||||
VALIDATE_DEVICE = re.compile(r'^\s*(?P<device>[a-z0-9_-]{1,25})\s*$', re.I)
|
||||
|
||||
|
||||
# Priorities
|
||||
|
@ -164,6 +161,9 @@ class NotifyPushover(NotifyBase):
|
|||
# Pushover uses the http protocol with JSON requests
|
||||
notify_url = 'https://api.pushover.net/1/messages.json'
|
||||
|
||||
# Support attachments
|
||||
attachment_support = True
|
||||
|
||||
# The maximum allowable characters allowed in the body per message
|
||||
body_maxlen = 1024
|
||||
|
||||
|
@ -201,7 +201,7 @@ class NotifyPushover(NotifyBase):
|
|||
'target_device': {
|
||||
'name': _('Target Device'),
|
||||
'type': 'string',
|
||||
'regex': (r'^[a-z0-9_]{1,25}$', 'i'),
|
||||
'regex': (r'^[a-z0-9_-]{1,25}$', 'i'),
|
||||
'map_to': 'targets',
|
||||
},
|
||||
'targets': {
|
||||
|
@ -276,10 +276,30 @@ class NotifyPushover(NotifyBase):
|
|||
self.logger.warning(msg)
|
||||
raise TypeError(msg)
|
||||
|
||||
self.targets = parse_list(targets)
|
||||
if len(self.targets) == 0:
|
||||
# Track our valid devices
|
||||
targets = parse_list(targets)
|
||||
|
||||
# Track any invalid entries
|
||||
self.invalid_targets = list()
|
||||
|
||||
if len(targets) == 0:
|
||||
self.targets = (PUSHOVER_SEND_TO_ALL, )
|
||||
|
||||
else:
|
||||
self.targets = []
|
||||
for target in targets:
|
||||
result = VALIDATE_DEVICE.match(target)
|
||||
if result:
|
||||
# Store device information
|
||||
self.targets.append(result.group('device'))
|
||||
continue
|
||||
|
||||
self.logger.warning(
|
||||
'Dropped invalid Pushover device '
|
||||
'({}) specified.'.format(target),
|
||||
)
|
||||
self.invalid_targets.append(target)
|
||||
|
||||
# Setup supplemental url
|
||||
self.supplemental_url = supplemental_url
|
||||
self.supplemental_url_title = supplemental_url_title
|
||||
|
@ -288,9 +308,8 @@ class NotifyPushover(NotifyBase):
|
|||
self.sound = NotifyPushover.default_pushover_sound \
|
||||
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)
|
||||
msg = 'Using custom sound specified ({}). '.format(sound)
|
||||
self.logger.debug(msg)
|
||||
|
||||
# The Priority of the message
|
||||
self.priority = int(
|
||||
|
@ -338,77 +357,67 @@ class NotifyPushover(NotifyBase):
|
|||
Perform Pushover Notification
|
||||
"""
|
||||
|
||||
# error tracking (used for function return)
|
||||
has_error = False
|
||||
if not self.targets:
|
||||
# There were no services to notify
|
||||
self.logger.warning(
|
||||
'There were no Pushover targets to notify.')
|
||||
return False
|
||||
|
||||
# Create a copy of the devices list
|
||||
devices = list(self.targets)
|
||||
while len(devices):
|
||||
device = devices.pop(0)
|
||||
# prepare JSON Object
|
||||
payload = {
|
||||
'token': self.token,
|
||||
'user': self.user_key,
|
||||
'priority': str(self.priority),
|
||||
'title': title if title else self.app_desc,
|
||||
'message': body,
|
||||
'device': ','.join(self.targets),
|
||||
'sound': self.sound,
|
||||
}
|
||||
|
||||
if VALIDATE_DEVICE.match(device) is None:
|
||||
self.logger.warning(
|
||||
'The device specified (%s) is invalid.' % device,
|
||||
)
|
||||
if self.supplemental_url:
|
||||
payload['url'] = self.supplemental_url
|
||||
|
||||
# Mark our failure
|
||||
has_error = True
|
||||
continue
|
||||
if self.supplemental_url_title:
|
||||
payload['url_title'] = self.supplemental_url_title
|
||||
|
||||
# prepare JSON Object
|
||||
payload = {
|
||||
'token': self.token,
|
||||
'user': self.user_key,
|
||||
'priority': str(self.priority),
|
||||
'title': title if title else self.app_desc,
|
||||
'message': body,
|
||||
'device': device,
|
||||
'sound': self.sound,
|
||||
}
|
||||
if self.notify_format == NotifyFormat.HTML:
|
||||
# https://pushover.net/api#html
|
||||
payload['html'] = 1
|
||||
|
||||
if self.supplemental_url:
|
||||
payload['url'] = self.supplemental_url
|
||||
if self.supplemental_url_title:
|
||||
payload['url_title'] = self.supplemental_url_title
|
||||
elif self.notify_format == NotifyFormat.MARKDOWN:
|
||||
payload['message'] = convert_between(
|
||||
NotifyFormat.MARKDOWN, NotifyFormat.HTML, body)
|
||||
payload['html'] = 1
|
||||
|
||||
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})
|
||||
|
||||
if self.priority == PushoverPriority.EMERGENCY:
|
||||
payload.update({'retry': self.retry, 'expire': self.expire})
|
||||
|
||||
if attach:
|
||||
# Create a copy of our payload
|
||||
_payload = payload.copy()
|
||||
|
||||
# Send with attachments
|
||||
for attachment in attach:
|
||||
# Simple send
|
||||
if not self._send(_payload, attachment):
|
||||
# Mark our failure
|
||||
has_error = True
|
||||
# clean exit from our attachment loop
|
||||
break
|
||||
if attach and self.attachment_support:
|
||||
# Create a copy of our payload
|
||||
_payload = payload.copy()
|
||||
|
||||
# Send with attachments
|
||||
for no, attachment in enumerate(attach):
|
||||
if no or not body:
|
||||
# To handle multiple attachments, clean up our message
|
||||
_payload['title'] = '...'
|
||||
_payload['message'] = attachment.name
|
||||
# No need to alarm for each consecutive attachment uploaded
|
||||
# afterwards
|
||||
_payload['sound'] = PushoverSound.NONE
|
||||
|
||||
else:
|
||||
# Simple send
|
||||
if not self._send(payload):
|
||||
if not self._send(_payload, attachment):
|
||||
# Mark our failure
|
||||
has_error = True
|
||||
return False
|
||||
|
||||
return not has_error
|
||||
# Clear our title if previously set
|
||||
_payload['title'] = ''
|
||||
|
||||
# No need to alarm for each consecutive attachment uploaded
|
||||
# afterwards
|
||||
_payload['sound'] = PushoverSound.NONE
|
||||
|
||||
else:
|
||||
# Simple send
|
||||
return self._send(payload)
|
||||
|
||||
return True
|
||||
|
||||
def _send(self, payload, attach=None):
|
||||
"""
|
||||
|
@ -562,8 +571,9 @@ class NotifyPushover(NotifyBase):
|
|||
params.update(self.url_parameters(privacy=privacy, *args, **kwargs))
|
||||
|
||||
# Escape our devices
|
||||
devices = '/'.join([NotifyPushover.quote(x, safe='')
|
||||
for x in self.targets])
|
||||
devices = '/'.join(
|
||||
[NotifyPushover.quote(x, safe='')
|
||||
for x in chain(self.targets, self.invalid_targets)])
|
||||
|
||||
if devices == PUSHOVER_SEND_TO_ALL:
|
||||
# keyword is reserved for internal usage only; it's safe to remove
|
||||
|
|
384
lib/apprise/plugins/NotifyPushy.py
Normal file
384
lib/apprise/plugins/NotifyPushy.py
Normal file
|
@ -0,0 +1,384 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# BSD 2-Clause License
|
||||
#
|
||||
# Apprise - Push Notification Library.
|
||||
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
|
||||
#
|
||||
# Redistribution and use in source and binary forms, with or without
|
||||
# modification, are permitted provided that the following conditions are met:
|
||||
#
|
||||
# 1. Redistributions of source code must retain the above copyright notice,
|
||||
# this list of conditions and the following disclaimer.
|
||||
#
|
||||
# 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
# this list of conditions and the following disclaimer in the documentation
|
||||
# and/or other materials provided with the distribution.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
|
||||
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
||||
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
# POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
# API reference: https://pushy.me/docs/api/send-notifications
|
||||
import re
|
||||
import requests
|
||||
from itertools import chain
|
||||
|
||||
from json import dumps, loads
|
||||
from .NotifyBase import NotifyBase
|
||||
from ..common import NotifyType
|
||||
from ..utils import parse_list
|
||||
from ..utils import validate_regex
|
||||
from ..AppriseLocale import gettext_lazy as _
|
||||
|
||||
# Used to detect a Device and Topic
|
||||
VALIDATE_DEVICE = re.compile(r'^@(?P<device>[a-z0-9]+)$', re.I)
|
||||
VALIDATE_TOPIC = re.compile(r'^[#]?(?P<topic>[a-z0-9]+)$', re.I)
|
||||
|
||||
# Extend HTTP Error Messages
|
||||
PUSHY_HTTP_ERROR_MAP = {
|
||||
401: 'Unauthorized - Invalid Token.',
|
||||
}
|
||||
|
||||
|
||||
class NotifyPushy(NotifyBase):
|
||||
"""
|
||||
A wrapper for Pushy Notifications
|
||||
"""
|
||||
|
||||
# The default descriptive name associated with the Notification
|
||||
service_name = 'Pushy'
|
||||
|
||||
# The services URL
|
||||
service_url = 'https://pushy.me/'
|
||||
|
||||
# All Pushy requests are secure
|
||||
secure_protocol = 'pushy'
|
||||
|
||||
# A URL that takes you to the setup/help of the specific protocol
|
||||
setup_url = 'https://github.com/caronc/apprise/wiki/Notify_pushy'
|
||||
|
||||
# Pushy uses the http protocol with JSON requests
|
||||
notify_url = 'https://api.pushy.me/push?api_key={apikey}'
|
||||
|
||||
# The maximum allowable characters allowed in the body per message
|
||||
body_maxlen = 4096
|
||||
|
||||
# Define object templates
|
||||
templates = (
|
||||
'{schema}://{apikey}/{targets}',
|
||||
)
|
||||
|
||||
# Define our template tokens
|
||||
template_tokens = dict(NotifyBase.template_tokens, **{
|
||||
'apikey': {
|
||||
'name': _('Secret API Key'),
|
||||
'type': 'string',
|
||||
'private': True,
|
||||
'required': True,
|
||||
},
|
||||
'target_device': {
|
||||
'name': _('Target Device'),
|
||||
'type': 'string',
|
||||
'prefix': '@',
|
||||
'map_to': 'targets',
|
||||
},
|
||||
'target_topic': {
|
||||
'name': _('Target Topic'),
|
||||
'type': 'string',
|
||||
'prefix': '#',
|
||||
'map_to': 'targets',
|
||||
},
|
||||
'targets': {
|
||||
'name': _('Targets'),
|
||||
'type': 'list:string',
|
||||
'required': True,
|
||||
},
|
||||
})
|
||||
|
||||
# Define our template arguments
|
||||
template_args = dict(NotifyBase.template_args, **{
|
||||
'sound': {
|
||||
# Specify something like ping.aiff
|
||||
'name': _('Sound'),
|
||||
'type': 'string',
|
||||
},
|
||||
'badge': {
|
||||
'name': _('Badge'),
|
||||
'type': 'int',
|
||||
'min': 0,
|
||||
},
|
||||
'to': {
|
||||
'alias_of': 'targets',
|
||||
},
|
||||
'key': {
|
||||
'alias_of': 'apikey',
|
||||
},
|
||||
})
|
||||
|
||||
def __init__(self, apikey, targets=None, sound=None, badge=None, **kwargs):
|
||||
"""
|
||||
Initialize Pushy Object
|
||||
"""
|
||||
super().__init__(**kwargs)
|
||||
|
||||
# Access Token (associated with project)
|
||||
self.apikey = validate_regex(apikey)
|
||||
if not self.apikey:
|
||||
msg = 'An invalid Pushy Secret API Key ' \
|
||||
'({}) was specified.'.format(apikey)
|
||||
self.logger.warning(msg)
|
||||
raise TypeError(msg)
|
||||
|
||||
# Get our targets
|
||||
self.devices = []
|
||||
self.topics = []
|
||||
|
||||
for target in parse_list(targets):
|
||||
result = VALIDATE_TOPIC.match(target)
|
||||
if result:
|
||||
self.topics.append(result.group('topic'))
|
||||
continue
|
||||
|
||||
result = VALIDATE_DEVICE.match(target)
|
||||
if result:
|
||||
self.devices.append(result.group('device'))
|
||||
continue
|
||||
|
||||
self.logger.warning(
|
||||
'Dropped invalid topic/device '
|
||||
'({}) specified.'.format(target),
|
||||
)
|
||||
|
||||
# Setup our sound
|
||||
self.sound = sound
|
||||
|
||||
# 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 Pushy badge ({}) is not valid ', badge)
|
||||
|
||||
return
|
||||
|
||||
def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs):
|
||||
"""
|
||||
Perform Pushy Notification
|
||||
"""
|
||||
|
||||
if len(self.topics) + len(self.devices) == 0:
|
||||
# There were no services to notify
|
||||
self.logger.warning('There were no Pushy targets to notify.')
|
||||
return False
|
||||
|
||||
# error tracking (used for function return)
|
||||
has_error = False
|
||||
|
||||
# Default Header
|
||||
headers = {
|
||||
'User-Agent': self.app_id,
|
||||
'Content-Type': 'application/json',
|
||||
'Accepts': 'application/json',
|
||||
}
|
||||
|
||||
# Our URL
|
||||
notify_url = self.notify_url.format(apikey=self.apikey)
|
||||
|
||||
# Default content response object
|
||||
content = {}
|
||||
|
||||
# Create a copy of targets (topics and devices)
|
||||
targets = list(self.topics) + list(self.devices)
|
||||
while len(targets):
|
||||
target = targets.pop(0)
|
||||
|
||||
# prepare JSON Object
|
||||
payload = {
|
||||
# Mandatory fields
|
||||
'to': target,
|
||||
"data": {
|
||||
"message": body,
|
||||
},
|
||||
"notification": {
|
||||
'body': body,
|
||||
}
|
||||
}
|
||||
|
||||
# Optional payload items
|
||||
if title:
|
||||
payload['notification']['title'] = title
|
||||
|
||||
if self.sound:
|
||||
payload['notification']['sound'] = self.sound
|
||||
|
||||
if self.badge is not None:
|
||||
payload['notification']['badge'] = self.badge
|
||||
|
||||
self.logger.debug('Pushy POST URL: %s (cert_verify=%r)' % (
|
||||
notify_url, self.verify_certificate,
|
||||
))
|
||||
self.logger.debug('Pushy 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,
|
||||
)
|
||||
|
||||
# Sample response
|
||||
# See: https://pushy.me/docs/api/send-notifications
|
||||
# {
|
||||
# "success": true,
|
||||
# "id": "5ea9b214b47cad768a35f13a",
|
||||
# "info": {
|
||||
# "devices": 1
|
||||
# "failed": ['abc']
|
||||
# }
|
||||
# }
|
||||
try:
|
||||
content = loads(r.content)
|
||||
|
||||
except (AttributeError, TypeError, ValueError):
|
||||
# ValueError = r.content is Unparsable
|
||||
# TypeError = r.content is None
|
||||
# AttributeError = r is None
|
||||
content = {
|
||||
"success": False,
|
||||
"id": '',
|
||||
"info": {},
|
||||
}
|
||||
|
||||
if r.status_code != requests.codes.ok \
|
||||
or not content.get('success'):
|
||||
|
||||
# We had a problem
|
||||
status_str = \
|
||||
NotifyPushy.http_response_code_lookup(
|
||||
r.status_code, PUSHY_HTTP_ERROR_MAP)
|
||||
|
||||
self.logger.warning(
|
||||
'Failed to send Pushy notification to {}: '
|
||||
'{}{}error={}.'.format(
|
||||
target,
|
||||
status_str,
|
||||
', ' if status_str else '',
|
||||
r.status_code))
|
||||
|
||||
self.logger.debug(
|
||||
'Response Details:\r\n{}'.format(r.content))
|
||||
|
||||
has_error = True
|
||||
continue
|
||||
|
||||
else:
|
||||
self.logger.info(
|
||||
'Sent Pushy notification to %s.' % target)
|
||||
|
||||
except requests.RequestException as e:
|
||||
self.logger.warning(
|
||||
'A Connection error occurred sending Pushy:%s '
|
||||
'notification', target)
|
||||
self.logger.debug('Socket Exception: %s' % str(e))
|
||||
|
||||
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 = {}
|
||||
if self.sound:
|
||||
params['sound'] = self.sound
|
||||
|
||||
if self.badge is not None:
|
||||
params['badge'] = str(self.badge)
|
||||
|
||||
# Extend our parameters
|
||||
params.update(self.url_parameters(privacy=privacy, *args, **kwargs))
|
||||
|
||||
return '{schema}://{apikey}/{targets}/?{params}'.format(
|
||||
schema=self.secure_protocol,
|
||||
apikey=self.pprint(self.apikey, privacy, safe=''),
|
||||
targets='/'.join(
|
||||
[NotifyPushy.quote(x, safe='@#') for x in chain(
|
||||
# Topics are prefixed with a pound/hashtag symbol
|
||||
['#{}'.format(x) for x in self.topics],
|
||||
# Devices
|
||||
['@{}'.format(x) for x in self.devices],
|
||||
)]),
|
||||
params=NotifyPushy.urlencode(params))
|
||||
|
||||
def __len__(self):
|
||||
"""
|
||||
Returns the number of targets associated with this notification
|
||||
"""
|
||||
return len(self.topics) + len(self.devices)
|
||||
|
||||
@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
|
||||
|
||||
# Token
|
||||
results['apikey'] = NotifyPushy.unquote(results['host'])
|
||||
|
||||
# Retrieve all of our targets
|
||||
results['targets'] = NotifyPushy.split_path(results['fullpath'])
|
||||
|
||||
# Get the sound
|
||||
if 'sound' in results['qsd'] and len(results['qsd']['sound']):
|
||||
results['sound'] = \
|
||||
NotifyPushy.unquote(results['qsd']['sound'])
|
||||
|
||||
# Badge
|
||||
if 'badge' in results['qsd'] and results['qsd']['badge']:
|
||||
results['badge'] = NotifyPushy.unquote(
|
||||
results['qsd']['badge'].strip())
|
||||
|
||||
# Support key variable to store Secret API Key
|
||||
if 'key' in results['qsd'] and len(results['qsd']['key']):
|
||||
results['apikey'] = results['qsd']['key']
|
||||
|
||||
# The 'to' makes it easier to use yaml configuration
|
||||
if 'to' in results['qsd'] and len(results['qsd']['to']):
|
||||
results['targets'] += \
|
||||
NotifyPushy.parse_list(results['qsd']['to'])
|
||||
|
||||
return results
|
376
lib/apprise/plugins/NotifyRSyslog.py
Normal file
376
lib/apprise/plugins/NotifyRSyslog.py
Normal file
|
@ -0,0 +1,376 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# BSD 2-Clause License
|
||||
#
|
||||
# Apprise - Push Notification Library.
|
||||
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
|
||||
#
|
||||
# Redistribution and use in source and binary forms, with or without
|
||||
# modification, are permitted provided that the following conditions are met:
|
||||
#
|
||||
# 1. Redistributions of source code must retain the above copyright notice,
|
||||
# this list of conditions and the following disclaimer.
|
||||
#
|
||||
# 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
# this list of conditions and the following disclaimer in the documentation
|
||||
# and/or other materials provided with the distribution.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
|
||||
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
||||
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
# POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
import os
|
||||
import socket
|
||||
|
||||
from .NotifyBase import NotifyBase
|
||||
from ..common import NotifyType
|
||||
from ..utils import parse_bool
|
||||
from ..AppriseLocale import gettext_lazy as _
|
||||
|
||||
|
||||
class syslog:
|
||||
"""
|
||||
Extrapoloated information from the syslog library so that this plugin
|
||||
would not be dependent on it.
|
||||
"""
|
||||
# Notification Categories
|
||||
LOG_KERN = 0
|
||||
LOG_USER = 8
|
||||
LOG_MAIL = 16
|
||||
LOG_DAEMON = 24
|
||||
LOG_AUTH = 32
|
||||
LOG_SYSLOG = 40
|
||||
LOG_LPR = 48
|
||||
LOG_NEWS = 56
|
||||
LOG_UUCP = 64
|
||||
LOG_CRON = 72
|
||||
LOG_LOCAL0 = 128
|
||||
LOG_LOCAL1 = 136
|
||||
LOG_LOCAL2 = 144
|
||||
LOG_LOCAL3 = 152
|
||||
LOG_LOCAL4 = 160
|
||||
LOG_LOCAL5 = 168
|
||||
LOG_LOCAL6 = 176
|
||||
LOG_LOCAL7 = 184
|
||||
|
||||
# Notification Types
|
||||
LOG_INFO = 6
|
||||
LOG_NOTICE = 5
|
||||
LOG_WARNING = 4
|
||||
LOG_CRIT = 2
|
||||
|
||||
|
||||
class SyslogFacility:
|
||||
"""
|
||||
All of the supported facilities
|
||||
"""
|
||||
KERN = 'kern'
|
||||
USER = 'user'
|
||||
MAIL = 'mail'
|
||||
DAEMON = 'daemon'
|
||||
AUTH = 'auth'
|
||||
SYSLOG = 'syslog'
|
||||
LPR = 'lpr'
|
||||
NEWS = 'news'
|
||||
UUCP = 'uucp'
|
||||
CRON = 'cron'
|
||||
LOCAL0 = 'local0'
|
||||
LOCAL1 = 'local1'
|
||||
LOCAL2 = 'local2'
|
||||
LOCAL3 = 'local3'
|
||||
LOCAL4 = 'local4'
|
||||
LOCAL5 = 'local5'
|
||||
LOCAL6 = 'local6'
|
||||
LOCAL7 = 'local7'
|
||||
|
||||
|
||||
SYSLOG_FACILITY_MAP = {
|
||||
SyslogFacility.KERN: syslog.LOG_KERN,
|
||||
SyslogFacility.USER: syslog.LOG_USER,
|
||||
SyslogFacility.MAIL: syslog.LOG_MAIL,
|
||||
SyslogFacility.DAEMON: syslog.LOG_DAEMON,
|
||||
SyslogFacility.AUTH: syslog.LOG_AUTH,
|
||||
SyslogFacility.SYSLOG: syslog.LOG_SYSLOG,
|
||||
SyslogFacility.LPR: syslog.LOG_LPR,
|
||||
SyslogFacility.NEWS: syslog.LOG_NEWS,
|
||||
SyslogFacility.UUCP: syslog.LOG_UUCP,
|
||||
SyslogFacility.CRON: syslog.LOG_CRON,
|
||||
SyslogFacility.LOCAL0: syslog.LOG_LOCAL0,
|
||||
SyslogFacility.LOCAL1: syslog.LOG_LOCAL1,
|
||||
SyslogFacility.LOCAL2: syslog.LOG_LOCAL2,
|
||||
SyslogFacility.LOCAL3: syslog.LOG_LOCAL3,
|
||||
SyslogFacility.LOCAL4: syslog.LOG_LOCAL4,
|
||||
SyslogFacility.LOCAL5: syslog.LOG_LOCAL5,
|
||||
SyslogFacility.LOCAL6: syslog.LOG_LOCAL6,
|
||||
SyslogFacility.LOCAL7: syslog.LOG_LOCAL7,
|
||||
}
|
||||
|
||||
SYSLOG_FACILITY_RMAP = {
|
||||
syslog.LOG_KERN: SyslogFacility.KERN,
|
||||
syslog.LOG_USER: SyslogFacility.USER,
|
||||
syslog.LOG_MAIL: SyslogFacility.MAIL,
|
||||
syslog.LOG_DAEMON: SyslogFacility.DAEMON,
|
||||
syslog.LOG_AUTH: SyslogFacility.AUTH,
|
||||
syslog.LOG_SYSLOG: SyslogFacility.SYSLOG,
|
||||
syslog.LOG_LPR: SyslogFacility.LPR,
|
||||
syslog.LOG_NEWS: SyslogFacility.NEWS,
|
||||
syslog.LOG_UUCP: SyslogFacility.UUCP,
|
||||
syslog.LOG_CRON: SyslogFacility.CRON,
|
||||
syslog.LOG_LOCAL0: SyslogFacility.LOCAL0,
|
||||
syslog.LOG_LOCAL1: SyslogFacility.LOCAL1,
|
||||
syslog.LOG_LOCAL2: SyslogFacility.LOCAL2,
|
||||
syslog.LOG_LOCAL3: SyslogFacility.LOCAL3,
|
||||
syslog.LOG_LOCAL4: SyslogFacility.LOCAL4,
|
||||
syslog.LOG_LOCAL5: SyslogFacility.LOCAL5,
|
||||
syslog.LOG_LOCAL6: SyslogFacility.LOCAL6,
|
||||
syslog.LOG_LOCAL7: SyslogFacility.LOCAL7,
|
||||
}
|
||||
|
||||
# Used as a lookup when handling the Apprise -> Syslog Mapping
|
||||
SYSLOG_PUBLISH_MAP = {
|
||||
NotifyType.INFO: syslog.LOG_INFO,
|
||||
NotifyType.SUCCESS: syslog.LOG_NOTICE,
|
||||
NotifyType.FAILURE: syslog.LOG_CRIT,
|
||||
NotifyType.WARNING: syslog.LOG_WARNING,
|
||||
}
|
||||
|
||||
|
||||
class NotifyRSyslog(NotifyBase):
|
||||
"""
|
||||
A wrapper for Remote Syslog Notifications
|
||||
"""
|
||||
|
||||
# The default descriptive name associated with the Notification
|
||||
service_name = 'Remote Syslog'
|
||||
|
||||
# The services URL
|
||||
service_url = 'https://tools.ietf.org/html/rfc5424'
|
||||
|
||||
# The default protocol
|
||||
protocol = 'rsyslog'
|
||||
|
||||
# A URL that takes you to the setup/help of the specific protocol
|
||||
setup_url = 'https://github.com/caronc/apprise/wiki/Notify_rsyslog'
|
||||
|
||||
# Disable throttle rate for RSyslog requests
|
||||
request_rate_per_sec = 0
|
||||
|
||||
# Define object templates
|
||||
templates = (
|
||||
'{schema}://{host}',
|
||||
'{schema}://{host}:{port}',
|
||||
'{schema}://{host}/{facility}',
|
||||
'{schema}://{host}:{port}/{facility}',
|
||||
)
|
||||
|
||||
# Define our template tokens
|
||||
template_tokens = dict(NotifyBase.template_tokens, **{
|
||||
'facility': {
|
||||
'name': _('Facility'),
|
||||
'type': 'choice:string',
|
||||
'values': [k for k in SYSLOG_FACILITY_MAP.keys()],
|
||||
'default': SyslogFacility.USER,
|
||||
'required': True,
|
||||
},
|
||||
'host': {
|
||||
'name': _('Hostname'),
|
||||
'type': 'string',
|
||||
'required': True,
|
||||
},
|
||||
'port': {
|
||||
'name': _('Port'),
|
||||
'type': 'int',
|
||||
'min': 1,
|
||||
'max': 65535,
|
||||
'default': 514,
|
||||
},
|
||||
})
|
||||
|
||||
# Define our template arguments
|
||||
template_args = dict(NotifyBase.template_args, **{
|
||||
'facility': {
|
||||
# We map back to the same element defined in template_tokens
|
||||
'alias_of': 'facility',
|
||||
},
|
||||
'logpid': {
|
||||
'name': _('Log PID'),
|
||||
'type': 'bool',
|
||||
'default': True,
|
||||
'map_to': 'log_pid',
|
||||
},
|
||||
})
|
||||
|
||||
def __init__(self, facility=None, log_pid=True, **kwargs):
|
||||
"""
|
||||
Initialize RSyslog Object
|
||||
"""
|
||||
super().__init__(**kwargs)
|
||||
|
||||
if facility:
|
||||
try:
|
||||
self.facility = SYSLOG_FACILITY_MAP[facility]
|
||||
|
||||
except KeyError:
|
||||
msg = 'An invalid syslog facility ' \
|
||||
'({}) was specified.'.format(facility)
|
||||
self.logger.warning(msg)
|
||||
raise TypeError(msg)
|
||||
|
||||
else:
|
||||
self.facility = \
|
||||
SYSLOG_FACILITY_MAP[
|
||||
self.template_tokens['facility']['default']]
|
||||
|
||||
# Include PID with each message.
|
||||
self.log_pid = log_pid
|
||||
|
||||
return
|
||||
|
||||
def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs):
|
||||
"""
|
||||
Perform RSyslog Notification
|
||||
"""
|
||||
|
||||
if title:
|
||||
# Format title
|
||||
body = '{}: {}'.format(title, body)
|
||||
|
||||
# Always call throttle before any remote server i/o is made
|
||||
self.throttle()
|
||||
host = self.host
|
||||
port = self.port if self.port \
|
||||
else self.template_tokens['port']['default']
|
||||
|
||||
if self.log_pid:
|
||||
payload = '<%d>- %d - %s' % (
|
||||
SYSLOG_PUBLISH_MAP[notify_type] + self.facility * 8,
|
||||
os.getpid(), body)
|
||||
|
||||
else:
|
||||
payload = '<%d>- %s' % (
|
||||
SYSLOG_PUBLISH_MAP[notify_type] + self.facility * 8, body)
|
||||
|
||||
# send UDP packet to upstream server
|
||||
self.logger.debug(
|
||||
'RSyslog Host: %s:%d/%s',
|
||||
host, port, SYSLOG_FACILITY_RMAP[self.facility])
|
||||
self.logger.debug('RSyslog Payload: %s' % str(payload))
|
||||
|
||||
# our sent bytes
|
||||
sent = 0
|
||||
|
||||
try:
|
||||
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||
sock.settimeout(self.socket_connect_timeout)
|
||||
sent = sock.sendto(payload.encode('utf-8'), (host, port))
|
||||
sock.close()
|
||||
|
||||
except socket.gaierror as e:
|
||||
self.logger.warning(
|
||||
'A connection error occurred sending RSyslog '
|
||||
'notification to %s:%d/%s', host, port,
|
||||
SYSLOG_FACILITY_RMAP[self.facility]
|
||||
)
|
||||
self.logger.debug('Socket Exception: %s' % str(e))
|
||||
return False
|
||||
|
||||
except socket.timeout as e:
|
||||
self.logger.warning(
|
||||
'A connection timeout occurred sending RSyslog '
|
||||
'notification to %s:%d/%s', host, port,
|
||||
SYSLOG_FACILITY_RMAP[self.facility]
|
||||
)
|
||||
self.logger.debug('Socket Exception: %s' % str(e))
|
||||
return False
|
||||
|
||||
if sent < len(payload):
|
||||
self.logger.warning(
|
||||
'RSyslog sent %d byte(s) but intended to send %d byte(s)',
|
||||
sent, len(payload))
|
||||
return False
|
||||
|
||||
self.logger.info('Sent RSyslog notification.')
|
||||
|
||||
return True
|
||||
|
||||
def url(self, privacy=False, *args, **kwargs):
|
||||
"""
|
||||
Returns the URL built dynamically based on specified arguments.
|
||||
"""
|
||||
|
||||
# Define any URL parameters
|
||||
params = {
|
||||
'logpid': 'yes' if self.log_pid else 'no',
|
||||
}
|
||||
|
||||
# Extend our parameters
|
||||
params.update(self.url_parameters(privacy=privacy, *args, **kwargs))
|
||||
|
||||
return '{schema}://{hostname}{port}/{facility}/?{params}'.format(
|
||||
schema=self.protocol,
|
||||
hostname=NotifyRSyslog.quote(self.host, safe=''),
|
||||
port='' if self.port is None
|
||||
or self.port == self.template_tokens['port']['default']
|
||||
else ':{}'.format(self.port),
|
||||
facility=self.template_tokens['facility']['default']
|
||||
if self.facility not in SYSLOG_FACILITY_RMAP
|
||||
else SYSLOG_FACILITY_RMAP[self.facility],
|
||||
params=NotifyRSyslog.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
|
||||
|
||||
tokens = []
|
||||
|
||||
# Get our path values
|
||||
tokens.extend(NotifyRSyslog.split_path(results['fullpath']))
|
||||
|
||||
# Initialization
|
||||
facility = None
|
||||
|
||||
if tokens:
|
||||
# Store the last entry as the facility
|
||||
facility = tokens[-1].lower()
|
||||
|
||||
# However if specified on the URL, that will over-ride what was
|
||||
# identified
|
||||
if 'facility' in results['qsd'] and len(results['qsd']['facility']):
|
||||
facility = results['qsd']['facility'].lower()
|
||||
|
||||
if facility and facility not in SYSLOG_FACILITY_MAP:
|
||||
# Find first match; if no match is found we set the result
|
||||
# to the matching key. This allows us to throw a TypeError
|
||||
# during the __init__() call. The benifit of doing this
|
||||
# check here is if we do have a valid match, we can support
|
||||
# short form matches like 'u' which will match against user
|
||||
facility = next((f for f in SYSLOG_FACILITY_MAP.keys()
|
||||
if f.startswith(facility)), facility)
|
||||
|
||||
# Save facility if set
|
||||
if facility:
|
||||
results['facility'] = facility
|
||||
|
||||
# Include PID as part of the message logged
|
||||
results['log_pid'] = parse_bool(
|
||||
results['qsd'].get(
|
||||
'logpid',
|
||||
NotifyRSyslog.template_args['logpid']['default']))
|
||||
|
||||
return results
|
|
@ -1,5 +1,5 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# BSD 3-Clause License
|
||||
# BSD 2-Clause License
|
||||
#
|
||||
# Apprise - Push Notification Library.
|
||||
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
|
||||
|
@ -14,10 +14,6 @@
|
|||
# this list of conditions and the following disclaimer in the documentation
|
||||
# and/or other materials provided with the distribution.
|
||||
#
|
||||
# 3. Neither the name of the copyright holder nor the names of its
|
||||
# contributors may be used to endorse or promote products derived from
|
||||
# this software without specific prior written permission.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
|
@ -30,7 +26,6 @@
|
|||
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
# POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
#
|
||||
# 1. Visit https://www.reddit.com/prefs/apps and scroll to the bottom
|
||||
# 2. Click on the button that reads 'are you a developer? create an app...'
|
||||
# 3. Set the mode to `script`,
|
||||
|
@ -56,6 +51,7 @@ import requests
|
|||
from json import loads
|
||||
from datetime import timedelta
|
||||
from datetime import datetime
|
||||
from datetime import timezone
|
||||
|
||||
from .NotifyBase import NotifyBase
|
||||
from ..URLBase import PrivacyMode
|
||||
|
@ -133,12 +129,6 @@ class NotifyReddit(NotifyBase):
|
|||
# still allow to make.
|
||||
request_rate_per_sec = 0
|
||||
|
||||
# For Tracking Purposes
|
||||
ratelimit_reset = datetime.utcnow()
|
||||
|
||||
# Default to 1.0
|
||||
ratelimit_remaining = 1.0
|
||||
|
||||
# Taken right from google.auth.helpers:
|
||||
clock_skew = timedelta(seconds=10)
|
||||
|
||||
|
@ -185,6 +175,7 @@ class NotifyReddit(NotifyBase):
|
|||
'targets': {
|
||||
'name': _('Targets'),
|
||||
'type': 'list:string',
|
||||
'required': True,
|
||||
},
|
||||
})
|
||||
|
||||
|
@ -275,7 +266,7 @@ class NotifyReddit(NotifyBase):
|
|||
# Our keys we build using the provided content
|
||||
self.__refresh_token = None
|
||||
self.__access_token = None
|
||||
self.__access_token_expiry = datetime.utcnow()
|
||||
self.__access_token_expiry = datetime.now(timezone.utc)
|
||||
|
||||
self.kind = kind.strip().lower() \
|
||||
if isinstance(kind, str) \
|
||||
|
@ -324,6 +315,13 @@ class NotifyReddit(NotifyBase):
|
|||
if not self.subreddits:
|
||||
self.logger.warning(
|
||||
'No subreddits were identified to be notified')
|
||||
|
||||
# For Rate Limit Tracking Purposes
|
||||
self.ratelimit_reset = datetime.now(timezone.utc).replace(tzinfo=None)
|
||||
|
||||
# Default to 1.0
|
||||
self.ratelimit_remaining = 1.0
|
||||
|
||||
return
|
||||
|
||||
def url(self, privacy=False, *args, **kwargs):
|
||||
|
@ -367,6 +365,12 @@ class NotifyReddit(NotifyBase):
|
|||
params=NotifyReddit.urlencode(params),
|
||||
)
|
||||
|
||||
def __len__(self):
|
||||
"""
|
||||
Returns the number of targets associated with this notification
|
||||
"""
|
||||
return len(self.subreddits)
|
||||
|
||||
def login(self):
|
||||
"""
|
||||
A simple wrapper to authenticate with the Reddit Server
|
||||
|
@ -411,10 +415,10 @@ class NotifyReddit(NotifyBase):
|
|||
if 'expires_in' in response:
|
||||
delta = timedelta(seconds=int(response['expires_in']))
|
||||
self.__access_token_expiry = \
|
||||
delta + datetime.utcnow() - self.clock_skew
|
||||
delta + datetime.now(timezone.utc) - self.clock_skew
|
||||
else:
|
||||
self.__access_token_expiry = self.access_token_lifetime_sec + \
|
||||
datetime.utcnow() - self.clock_skew
|
||||
datetime.now(timezone.utc) - self.clock_skew
|
||||
|
||||
# The Refresh Token
|
||||
self.__refresh_token = response.get(
|
||||
|
@ -538,10 +542,10 @@ class NotifyReddit(NotifyBase):
|
|||
# Determine how long we should wait for or if we should wait at
|
||||
# all. This isn't fool-proof because we can't be sure the client
|
||||
# time (calling this script) is completely synced up with the
|
||||
# Gitter server. One would hope we're on NTP and our clocks are
|
||||
# Reddit server. One would hope we're on NTP and our clocks are
|
||||
# the same allowing this to role smoothly:
|
||||
|
||||
now = datetime.utcnow()
|
||||
now = datetime.now(timezone.utc).replace(tzinfo=None)
|
||||
if now < self.ratelimit_reset:
|
||||
# We need to throttle for the difference in seconds
|
||||
wait = abs(
|
||||
|
@ -665,8 +669,9 @@ class NotifyReddit(NotifyBase):
|
|||
self.ratelimit_remaining = \
|
||||
float(r.headers.get(
|
||||
'X-RateLimit-Remaining'))
|
||||
self.ratelimit_reset = datetime.utcfromtimestamp(
|
||||
int(r.headers.get('X-RateLimit-Reset')))
|
||||
self.ratelimit_reset = datetime.fromtimestamp(
|
||||
int(r.headers.get('X-RateLimit-Reset')), timezone.utc
|
||||
).replace(tzinfo=None)
|
||||
|
||||
except (TypeError, ValueError):
|
||||
# This is returned if we could not retrieve this information
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# BSD 3-Clause License
|
||||
# BSD 2-Clause License
|
||||
#
|
||||
# Apprise - Push Notification Library.
|
||||
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
|
||||
|
@ -14,10 +14,6 @@
|
|||
# this list of conditions and the following disclaimer in the documentation
|
||||
# and/or other materials provided with the distribution.
|
||||
#
|
||||
# 3. Neither the name of the copyright holder nor the names of its
|
||||
# contributors may be used to endorse or promote products derived from
|
||||
# this software without specific prior written permission.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
|
@ -348,6 +344,13 @@ class NotifyRocketChat(NotifyBase):
|
|||
params=NotifyRocketChat.urlencode(params),
|
||||
)
|
||||
|
||||
def __len__(self):
|
||||
"""
|
||||
Returns the number of targets associated with this notification
|
||||
"""
|
||||
targets = len(self.channels) + len(self.rooms) + len(self.users)
|
||||
return targets if targets > 0 else 1
|
||||
|
||||
def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs):
|
||||
"""
|
||||
wrapper to _send since we can alert more then one channel
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# BSD 3-Clause License
|
||||
# BSD 2-Clause License
|
||||
#
|
||||
# Apprise - Push Notification Library.
|
||||
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
|
||||
|
@ -14,10 +14,6 @@
|
|||
# this list of conditions and the following disclaimer in the documentation
|
||||
# and/or other materials provided with the distribution.
|
||||
#
|
||||
# 3. Neither the name of the copyright holder nor the names of its
|
||||
# contributors may be used to endorse or promote products derived from
|
||||
# this software without specific prior written permission.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
|
@ -91,7 +87,7 @@ class NotifyRyver(NotifyBase):
|
|||
# Define object templates
|
||||
templates = (
|
||||
'{schema}://{organization}/{token}',
|
||||
'{schema}://{user}@{organization}/{token}',
|
||||
'{schema}://{botname}@{organization}/{token}',
|
||||
)
|
||||
|
||||
# Define our template tokens
|
||||
|
@ -109,9 +105,10 @@ class NotifyRyver(NotifyBase):
|
|||
'private': True,
|
||||
'regex': (r'^[A-Z0-9]{15}$', 'i'),
|
||||
},
|
||||
'user': {
|
||||
'botname': {
|
||||
'name': _('Bot Name'),
|
||||
'type': 'string',
|
||||
'map_to': 'user',
|
||||
},
|
||||
})
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# BSD 3-Clause License
|
||||
# BSD 2-Clause License
|
||||
#
|
||||
# Apprise - Push Notification Library.
|
||||
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
|
||||
|
@ -14,10 +14,6 @@
|
|||
# this list of conditions and the following disclaimer in the documentation
|
||||
# and/or other materials provided with the distribution.
|
||||
#
|
||||
# 3. Neither the name of the copyright holder nor the names of its
|
||||
# contributors may be used to endorse or promote products derived from
|
||||
# this software without specific prior written permission.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
|
@ -89,6 +85,7 @@ import base64
|
|||
import requests
|
||||
from hashlib import sha256
|
||||
from datetime import datetime
|
||||
from datetime import timezone
|
||||
from collections import OrderedDict
|
||||
from xml.etree import ElementTree
|
||||
from email.mime.text import MIMEText
|
||||
|
@ -135,6 +132,9 @@ class NotifySES(NotifyBase):
|
|||
# A URL that takes you to the setup/help of the specific protocol
|
||||
setup_url = 'https://github.com/caronc/apprise/wiki/Notify_ses'
|
||||
|
||||
# Support attachments
|
||||
attachment_support = True
|
||||
|
||||
# AWS is pretty good for handling data load so request limits
|
||||
# can occur in much shorter bursts
|
||||
request_rate_per_sec = 2.5
|
||||
|
@ -156,6 +156,7 @@ class NotifySES(NotifyBase):
|
|||
'name': _('From Email'),
|
||||
'type': 'string',
|
||||
'map_to': 'from_addr',
|
||||
'required': True,
|
||||
},
|
||||
'access_key_id': {
|
||||
'name': _('Access Key ID'),
|
||||
|
@ -173,6 +174,7 @@ class NotifySES(NotifyBase):
|
|||
'name': _('Region'),
|
||||
'type': 'string',
|
||||
'regex': (r'^[a-z]{2}-[a-z-]+?-[0-9]+$', 'i'),
|
||||
'required': True,
|
||||
'map_to': 'region_name',
|
||||
},
|
||||
'targets': {
|
||||
|
@ -424,7 +426,8 @@ class NotifySES(NotifyBase):
|
|||
content = MIMEText(body, 'plain', 'utf-8')
|
||||
|
||||
# Create a Multipart container if there is an attachment
|
||||
base = MIMEMultipart() if attach else content
|
||||
base = MIMEMultipart() \
|
||||
if attach and self.attachment_support else content
|
||||
|
||||
# TODO: Deduplicate with `NotifyEmail`?
|
||||
base['Subject'] = Header(title, 'utf-8')
|
||||
|
@ -436,10 +439,11 @@ class NotifySES(NotifyBase):
|
|||
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")
|
||||
datetime.now(
|
||||
timezone.utc).strftime("%a, %d %b %Y %H:%M:%S +0000")
|
||||
base['X-Application'] = self.app_id
|
||||
|
||||
if attach:
|
||||
if attach and self.attachment_support:
|
||||
# First attach our body to our content as the first element
|
||||
base.attach(content)
|
||||
|
||||
|
@ -585,7 +589,7 @@ class NotifySES(NotifyBase):
|
|||
}
|
||||
|
||||
# Get a reference time (used for header construction)
|
||||
reference = datetime.utcnow()
|
||||
reference = datetime.now(timezone.utc)
|
||||
|
||||
# Provide Content-Length
|
||||
headers['Content-Length'] = str(len(payload))
|
||||
|
@ -816,6 +820,13 @@ class NotifySES(NotifyBase):
|
|||
params=NotifySES.urlencode(params),
|
||||
)
|
||||
|
||||
def __len__(self):
|
||||
"""
|
||||
Returns the number of targets associated with this notification
|
||||
"""
|
||||
targets = len(self.targets)
|
||||
return targets if targets > 0 else 1
|
||||
|
||||
@staticmethod
|
||||
def parse_url(url):
|
||||
"""
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# BSD 3-Clause License
|
||||
# BSD 2-Clause License
|
||||
#
|
||||
# Apprise - Push Notification Library.
|
||||
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
|
||||
|
@ -14,10 +14,6 @@
|
|||
# this list of conditions and the following disclaimer in the documentation
|
||||
# and/or other materials provided with the distribution.
|
||||
#
|
||||
# 3. Neither the name of the copyright holder nor the names of its
|
||||
# contributors may be used to endorse or promote products derived from
|
||||
# this software without specific prior written permission.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
|
@ -73,6 +69,22 @@ SMSEAGLE_PRIORITY_MAP = {
|
|||
}
|
||||
|
||||
|
||||
class SMSEagleCategory:
|
||||
"""
|
||||
We define the different category types that we can notify via SMS Eagle
|
||||
"""
|
||||
PHONE = 'phone'
|
||||
GROUP = 'group'
|
||||
CONTACT = 'contact'
|
||||
|
||||
|
||||
SMSEAGLE_CATEGORIES = (
|
||||
SMSEagleCategory.PHONE,
|
||||
SMSEagleCategory.GROUP,
|
||||
SMSEagleCategory.CONTACT,
|
||||
)
|
||||
|
||||
|
||||
class NotifySMSEagle(NotifyBase):
|
||||
"""
|
||||
A wrapper for SMSEagle Notifications
|
||||
|
@ -96,6 +108,9 @@ class NotifySMSEagle(NotifyBase):
|
|||
# The path we send our notification to
|
||||
notify_path = '/jsonrpc/sms'
|
||||
|
||||
# Support attachments
|
||||
attachment_support = True
|
||||
|
||||
# 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
|
||||
|
@ -129,6 +144,7 @@ class NotifySMSEagle(NotifyBase):
|
|||
'token': {
|
||||
'name': _('Access Token'),
|
||||
'type': 'string',
|
||||
'required': True,
|
||||
},
|
||||
'target_phone': {
|
||||
'name': _('Target Phone No'),
|
||||
|
@ -154,6 +170,7 @@ class NotifySMSEagle(NotifyBase):
|
|||
'targets': {
|
||||
'name': _('Targets'),
|
||||
'type': 'list:string',
|
||||
'required': True,
|
||||
}
|
||||
})
|
||||
|
||||
|
@ -322,7 +339,7 @@ class NotifySMSEagle(NotifyBase):
|
|||
has_error = False
|
||||
|
||||
attachments = []
|
||||
if attach:
|
||||
if attach and self.attachment_support:
|
||||
for attachment in attach:
|
||||
# Perform some simple error checking
|
||||
if not attachment:
|
||||
|
@ -403,15 +420,15 @@ class NotifySMSEagle(NotifyBase):
|
|||
batch_size = 1 if not self.batch else self.default_batch_size
|
||||
|
||||
notify_by = {
|
||||
'phone': {
|
||||
SMSEagleCategory.PHONE: {
|
||||
"method": "sms.send_sms",
|
||||
'target': 'to',
|
||||
},
|
||||
'group': {
|
||||
SMSEagleCategory.GROUP: {
|
||||
"method": "sms.send_togroup",
|
||||
'target': 'groupname',
|
||||
},
|
||||
'contact': {
|
||||
SMSEagleCategory.CONTACT: {
|
||||
"method": "sms.send_tocontact",
|
||||
'target': 'contactname',
|
||||
},
|
||||
|
@ -420,7 +437,7 @@ class NotifySMSEagle(NotifyBase):
|
|||
# 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'):
|
||||
for category in SMSEAGLE_CATEGORIES:
|
||||
# Create a copy of our template
|
||||
payload = {
|
||||
'method': notify_by[category]['method'],
|
||||
|
@ -596,6 +613,28 @@ class NotifySMSEagle(NotifyBase):
|
|||
params=NotifySMSEagle.urlencode(params),
|
||||
)
|
||||
|
||||
def __len__(self):
|
||||
"""
|
||||
Returns the number of targets associated with this notification
|
||||
"""
|
||||
#
|
||||
# Factor batch into calculation
|
||||
#
|
||||
batch_size = 1 if not self.batch else self.default_batch_size
|
||||
if batch_size > 1:
|
||||
# Batches can only be sent by group (you can't combine groups into
|
||||
# a single batch)
|
||||
total_targets = 0
|
||||
for c in SMSEAGLE_CATEGORIES:
|
||||
targets = len(getattr(self, f'target_{c}s'))
|
||||
total_targets += int(targets / batch_size) + \
|
||||
(1 if targets % batch_size else 0)
|
||||
return total_targets
|
||||
|
||||
# Normal batch count; just count the targets
|
||||
return len(self.target_phones) + len(self.target_contacts) + \
|
||||
len(self.target_groups)
|
||||
|
||||
@staticmethod
|
||||
def parse_url(url):
|
||||
"""
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue