Update Apprise 1.3.0 (6458ab0) → 1.6.0 (0c0d5da).

This commit is contained in:
JackDandy 2023-10-21 12:47:10 +01:00
parent 7995f92d91
commit f36bb0b179
129 changed files with 5607 additions and 2321 deletions

View file

@ -1,5 +1,6 @@
### 3.31.0 (2023-1x-xx xx:xx:00 UTC) ### 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 attr 22.2.0 (683d056) to 23.1.0 (67e4ff2)
* Update Beautiful Soup 4.12.2 to 4.12.2 (30c58a1) * Update Beautiful Soup 4.12.2 to 4.12.2 (30c58a1)
* Update diskcache 5.6.1 (4d30686) to 5.6.3 (323787f) * Update diskcache 5.6.1 (4d30686) to 5.6.3 (323787f)

View file

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# BSD 3-Clause License # BSD 2-Clause License
# #
# Apprise - Push Notification Library. # Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com> # Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation # this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution. # 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" # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
@ -31,8 +27,8 @@
# POSSIBILITY OF SUCH DAMAGE. # POSSIBILITY OF SUCH DAMAGE.
import asyncio import asyncio
import concurrent.futures as cf
import os import os
from functools import partial
from itertools import chain from itertools import chain
from . import common from . import common
from .conversion import convert_between from .conversion import convert_between
@ -376,7 +372,7 @@ class Apprise:
try: try:
# Process arguments and build synchronous and asynchronous calls # Process arguments and build synchronous and asynchronous calls
# (this step can throw internal errors). # (this step can throw internal errors).
sync_partials, async_cors = self._create_notify_calls( sequential_calls, parallel_calls = self._create_notify_calls(
body, title, body, title,
notify_type=notify_type, body_format=body_format, notify_type=notify_type, body_format=body_format,
tag=tag, match_always=match_always, attach=attach, tag=tag, match_always=match_always, attach=attach,
@ -387,49 +383,13 @@ class Apprise:
# No notifications sent, and there was an internal error. # No notifications sent, and there was an internal error.
return False return False
if not sync_partials and not async_cors: if not sequential_calls and not parallel_calls:
# Nothing to send # Nothing to send
return None return None
sync_result = Apprise._notify_all(*sync_partials) sequential_result = Apprise._notify_sequential(*sequential_calls)
parallel_result = Apprise._notify_parallel_threadpool(*parallel_calls)
if async_cors: return sequential_result and parallel_result
# 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
async def async_notify(self, *args, **kwargs): async def async_notify(self, *args, **kwargs):
""" """
@ -442,41 +402,42 @@ class Apprise:
try: try:
# Process arguments and build synchronous and asynchronous calls # Process arguments and build synchronous and asynchronous calls
# (this step can throw internal errors). # (this step can throw internal errors).
sync_partials, async_cors = self._create_notify_calls( sequential_calls, parallel_calls = self._create_notify_calls(
*args, **kwargs) *args, **kwargs)
except TypeError: except TypeError:
# No notifications sent, and there was an internal error. # No notifications sent, and there was an internal error.
return False return False
if not sync_partials and not async_cors: if not sequential_calls and not parallel_calls:
# Nothing to send # Nothing to send
return None return None
sync_result = Apprise._notify_all(*sync_partials) sequential_result = Apprise._notify_sequential(*sequential_calls)
async_result = await Apprise._async_notify_all(*async_cors) parallel_result = \
return sync_result and async_result await Apprise._notify_parallel_asyncio(*parallel_calls)
return sequential_result and parallel_result
def _create_notify_calls(self, *args, **kwargs): def _create_notify_calls(self, *args, **kwargs):
""" """
Creates notifications for all the plugins loaded. Creates notifications for all the plugins loaded.
Returns a list of synchronous calls (partial functions with no Returns a list of (server, notify() kwargs) tuples for plugins with
arguments required) for plugins with async disabled and a list of parallelism disabled and another list for plugins with parallelism
asynchronous calls (coroutines) for plugins with async enabled. enabled.
""" """
all_calls = list(self._create_notify_gen(*args, **kwargs)) all_calls = list(self._create_notify_gen(*args, **kwargs))
# Split into synchronous partials and asynchronous coroutines. # Split into sequential and parallel notify() calls.
sync_partials, async_cors = [], [] sequential, parallel = [], []
for notify in all_calls: for (server, notify_kwargs) in all_calls:
if asyncio.iscoroutine(notify): if server.asset.async_mode:
async_cors.append(notify) parallel.append((server, notify_kwargs))
else: 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='', def _create_notify_gen(self, body, title='',
notify_type=common.NotifyType.INFO, notify_type=common.NotifyType.INFO,
@ -493,7 +454,7 @@ class Apprise:
logger.error(msg) logger.error(msg)
raise TypeError(msg) raise TypeError(msg)
if not (title or body): if not (title or body or attach):
msg = "No message content specified to deliver" msg = "No message content specified to deliver"
logger.error(msg) logger.error(msg)
raise TypeError(msg) raise TypeError(msg)
@ -533,25 +494,29 @@ class Apprise:
# If our code reaches here, we either did not define a tag (it # 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 # 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 # determined we need to notify the service it's associated with
if server.notify_format not in conversion_body_map:
# Perform Conversion # First we need to generate a key we will use to determine if we
conversion_body_map[server.notify_format] = \ # need to build our data out. Entries without are merged with
convert_between( # the body at this stage.
body_format, server.notify_format, content=body) key = server.notify_format if server.title_maxlen > 0\
else f'_{server.notify_format}'
if key not in conversion_title_map:
# Prepare our title # Prepare our title
conversion_title_map[server.notify_format] = \ conversion_title_map[key] = '' if not title else title
'' if not title else title
# Tidy Title IF required (hence it will become part of the # Conversion of title only occurs for services where the title
# body) # is blended with the body (title_maxlen <= 0)
if server.title_maxlen <= 0 and \ if conversion_title_map[key] and server.title_maxlen <= 0:
conversion_title_map[server.notify_format]: conversion_title_map[key] = convert_between(
conversion_title_map[server.notify_format] = \
convert_between(
body_format, server.notify_format, body_format, server.notify_format,
content=conversion_title_map[server.notify_format]) content=conversion_title_map[key])
# Our body is always converted no matter what
conversion_body_map[key] = \
convert_between(
body_format, server.notify_format, content=body)
if interpret_escapes: if interpret_escapes:
# #
@ -561,13 +526,13 @@ class Apprise:
try: try:
# Added overhead required due to Python 3 Encoding Bug # Added overhead required due to Python 3 Encoding Bug
# identified here: https://bugs.python.org/issue21331 # identified here: https://bugs.python.org/issue21331
conversion_body_map[server.notify_format] = \ conversion_body_map[key] = \
conversion_body_map[server.notify_format]\ conversion_body_map[key]\
.encode('ascii', 'backslashreplace')\ .encode('ascii', 'backslashreplace')\
.decode('unicode-escape') .decode('unicode-escape')
conversion_title_map[server.notify_format] = \ conversion_title_map[key] = \
conversion_title_map[server.notify_format]\ conversion_title_map[key]\
.encode('ascii', 'backslashreplace')\ .encode('ascii', 'backslashreplace')\
.decode('unicode-escape') .decode('unicode-escape')
@ -578,29 +543,26 @@ class Apprise:
raise TypeError(msg) raise TypeError(msg)
kwargs = dict( kwargs = dict(
body=conversion_body_map[server.notify_format], body=conversion_body_map[key],
title=conversion_title_map[server.notify_format], title=conversion_title_map[key],
notify_type=notify_type, notify_type=notify_type,
attach=attach, attach=attach,
body_format=body_format body_format=body_format
) )
if server.asset.async_mode: yield (server, kwargs)
yield server.async_notify(**kwargs)
else:
yield partial(server.notify, **kwargs)
@staticmethod @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 success = True
for notify in partials: for (server, kwargs) in servers_kwargs:
try: try:
# Send notification # Send notification
result = notify() result = server.notify(**kwargs)
success = success and result success = success and result
except TypeError: except TypeError:
@ -616,14 +578,71 @@ class Apprise:
return success return success
@staticmethod @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 # 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) results = await asyncio.gather(*cors, return_exceptions=True)
if any(isinstance(status, Exception) if any(isinstance(status, Exception)
@ -665,6 +684,12 @@ class Apprise:
'setup_url': getattr(plugin, 'setup_url', None), 'setup_url': getattr(plugin, 'setup_url', None),
# Placeholder - populated below # Placeholder - populated below
'details': None, '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 # Differentiat between what is a custom loaded plugin and
# which is native. # which is native.
'category': getattr(plugin, 'category', None) 'category': getattr(plugin, 'category', None)
@ -790,6 +815,36 @@ class Apprise:
# If we reach here, then we indexed out of range # If we reach here, then we indexed out of range
raise IndexError('list index 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): def __bool__(self):
""" """
Allows the Apprise object to be wrapped in an 'if statement'. Allows the Apprise object to be wrapped in an 'if statement'.

View file

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# BSD 3-Clause License # BSD 2-Clause License
# #
# Apprise - Push Notification Library. # Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com> # Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation # this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution. # 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" # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE

View file

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# BSD 3-Clause License # BSD 2-Clause License
# #
# Apprise - Push Notification Library. # Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com> # Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation # this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution. # 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" # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE

View file

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# BSD 3-Clause License # BSD 2-Clause License
# #
# Apprise - Push Notification Library. # Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com> # Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation # this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution. # 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" # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE

View file

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# BSD 3-Clause License # BSD 2-Clause License
# #
# Apprise - Push Notification Library. # Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com> # Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation # this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution. # 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" # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
@ -33,14 +29,13 @@
import ctypes import ctypes
import locale import locale
import contextlib import contextlib
import os
import re
from os.path import join from os.path import join
from os.path import dirname from os.path import dirname
from os.path import abspath from os.path import abspath
from .logger import logger from .logger import logger
# Define our translation domain
DOMAIN = 'apprise'
LOCALE_DIR = abspath(join(dirname(__file__), 'i18n'))
# This gets toggled to True if we succeed # This gets toggled to True if we succeed
GETTEXT_LOADED = False GETTEXT_LOADED = False
@ -49,17 +44,220 @@ try:
# Initialize gettext # Initialize gettext
import gettext import gettext
# install() creates a _() in our builtins
gettext.install(DOMAIN, localedir=LOCALE_DIR)
# Toggle our flag # Toggle our flag
GETTEXT_LOADED = True GETTEXT_LOADED = True
except ImportError: except ImportError:
# gettext isn't available; no problem, just fall back to using # gettext isn't available; no problem; Use the library features without
# the library features without multi-language support. # multi-language support.
import builtins pass
builtins.__dict__['_'] = lambda x: x # pragma: no branch
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: class LazyTranslation:
@ -77,7 +275,7 @@ class LazyTranslation:
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
def __str__(self): def __str__(self):
return gettext.gettext(self.text) return LOCALE.gettext(self.text) if GETTEXT_LOADED else self.text
# Lazy translation handling # Lazy translation handling
@ -86,140 +284,3 @@ def gettext_lazy(text):
A dummy function that can be referenced A dummy function that can be referenced
""" """
return LazyTranslation(text=text) 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()

View file

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# BSD 3-Clause License # BSD 2-Clause License
# #
# Apprise - Push Notification Library. # Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com> # Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation # this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution. # 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" # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # 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)) self.verify_certificate = parse_bool(kwargs.get('verify', True))
# Secure Mode # 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.host = URLBase.unquote(kwargs.get('host'))
self.port = kwargs.get('port') self.port = kwargs.get('port')
@ -228,6 +231,11 @@ class URLBase:
# Always unquote the password if it exists # Always unquote the password if it exists
self.password = URLBase.unquote(self.password) 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 # Store our Timeout Variables
if 'rto' in kwargs: if 'rto' in kwargs:
try: try:
@ -307,7 +315,36 @@ class URLBase:
arguments provied. 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): def __contains__(self, tags):
""" """
@ -583,6 +620,33 @@ class URLBase:
""" """
return (self.socket_connect_timeout, self.socket_read_timeout) 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): def url_parameters(self, *args, **kwargs):
""" """
Provides a default set of args to work with. This can greatly Provides a default set of args to work with. This can greatly
@ -603,7 +667,8 @@ class URLBase:
} }
@staticmethod @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. """Parses the URL and returns it broken apart into a dictionary.
This is very specific and customized for Apprise. This is very specific and customized for Apprise.
@ -624,13 +689,13 @@ class URLBase:
results = parse_url( results = parse_url(
url, default_schema='unknown', verify_host=verify_host, 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: if not results:
# We're done; we failed to parse our url # We're done; we failed to parse our url
return results 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') results['secure'] = (results['schema'][-1] == 's')
# Support SSL Certificate 'verify' keyword. Default to being enabled # Support SSL Certificate 'verify' keyword. Default to being enabled
@ -650,6 +715,21 @@ class URLBase:
if 'user' in results['qsd']: if 'user' in results['qsd']:
results['user'] = results['qsd']['user'] 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 # Store our socket read timeout if specified
if 'rto' in results['qsd']: if 'rto' in results['qsd']:
results['rto'] = results['qsd']['rto'] results['rto'] = results['qsd']['rto']
@ -685,6 +765,15 @@ class URLBase:
return response 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): def schemas(self):
"""A simple function that returns a set of all schemas associated """A simple function that returns a set of all schemas associated
with this object based on the object.protocol and with this object based on the object.protocol and

View file

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# BSD 3-Clause License # BSD 2-Clause License
# #
# Apprise - Push Notification Library. # Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com> # Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation # this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution. # 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" # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
@ -31,7 +27,7 @@
# POSSIBILITY OF SUCH DAMAGE. # POSSIBILITY OF SUCH DAMAGE.
__title__ = 'Apprise' __title__ = 'Apprise'
__version__ = '1.3.0' __version__ = '1.6.0'
__author__ = 'Chris Caron' __author__ = 'Chris Caron'
__license__ = 'BSD' __license__ = 'BSD'
__copywrite__ = 'Copyright (C) 2023 Chris Caron <lead2gold@gmail.com>' __copywrite__ = 'Copyright (C) 2023 Chris Caron <lead2gold@gmail.com>'

View file

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# BSD 3-Clause License # BSD 2-Clause License
# #
# Apprise - Push Notification Library. # Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com> # Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation # this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution. # 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" # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # 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 # set to zero (0), then no check is performed
# 1 MB = 1048576 bytes # 1 MB = 1048576 bytes
# 5 MB = 5242880 bytes # 5 MB = 5242880 bytes
max_file_size = 5242880 # 1 GB = 1048576000 bytes
max_file_size = 1048576000
# By default all attachments types are inaccessible. # By default all attachments types are inaccessible.
# Developers of items identified in the attachment plugin directory # Developers of items identified in the attachment plugin directory

View file

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# BSD 3-Clause License # BSD 2-Clause License
# #
# Apprise - Push Notification Library. # Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com> # Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation # this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution. # 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" # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE

View file

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# BSD 3-Clause License # BSD 2-Clause License
# #
# Apprise - Push Notification Library. # Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com> # Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation # this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution. # 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" # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE

View file

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# BSD 3-Clause License # BSD 2-Clause License
# #
# Apprise - Push Notification Library. # Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com> # Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation # this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution. # 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" # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE

View file

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# BSD 3-Clause License # BSD 2-Clause License
# #
# Apprise - Push Notification Library. # Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com> # Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation # this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution. # 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" # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE

View file

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# BSD 3-Clause License # BSD 2-Clause License
# #
# Apprise - Push Notification Library. # Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com> # Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation # this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution. # 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" # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
@ -355,6 +351,77 @@ class ConfigBase(URLBase):
# missing and/or expired. # missing and/or expired.
return True 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 @staticmethod
def parse_url(url, verify_host=True): def parse_url(url, verify_host=True):
"""Parses the URL and returns it broken apart into a dictionary. """Parses the URL and returns it broken apart into a dictionary.
@ -533,6 +600,9 @@ class ConfigBase(URLBase):
# as additional configuration entries when loaded. # as additional configuration entries when loaded.
include <ConfigURL> include <ConfigURL>
# Assign tag contents to a group identifier
<Group(s)>=<Tag(s)>
""" """
# A list of loaded Notification Services # A list of loaded Notification Services
servers = list() servers = list()
@ -541,6 +611,12 @@ class ConfigBase(URLBase):
# the include keyword # the include keyword
configs = list() 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 # Prepare our Asset Object
asset = asset if isinstance(asset, AppriseAsset) else AppriseAsset() asset = asset if isinstance(asset, AppriseAsset) else AppriseAsset()
@ -548,7 +624,7 @@ class ConfigBase(URLBase):
valid_line_re = re.compile( valid_line_re = re.compile(
r'^\s*(?P<line>([;#]+(?P<comment>.*))|' r'^\s*(?P<line>([;#]+(?P<comment>.*))|'
r'(\s*(?P<tags>[a-z0-9, \t_-]+)\s*=|=)?\s*' 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) r'include\s+(?P<config>.+))?\s*$', re.I)
try: try:
@ -574,8 +650,13 @@ class ConfigBase(URLBase):
# otherwise. # otherwise.
return (list(), list()) return (list(), list())
url, config = result.group('url'), result.group('config') # Retrieve our line
if not (url or config): url, assign, config = \
result.group('url'), \
result.group('assign'), \
result.group('config')
if not (url or config or assign):
# Comment/empty line; do nothing # Comment/empty line; do nothing
continue continue
@ -595,6 +676,33 @@ class ConfigBase(URLBase):
loggable_url = url if not asset.secure_logging \ loggable_url = url if not asset.secure_logging \
else cwe312_url(url) else cwe312_url(url)
if 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 # Acquire our url tokens
results = plugins.url_to_dict( results = plugins.url_to_dict(
url, secure_logging=asset.secure_logging) url, secure_logging=asset.secure_logging)
@ -607,25 +715,57 @@ class ConfigBase(URLBase):
# Build a list of tags to associate with the newly added # Build a list of tags to associate with the newly added
# notifications if any were set # 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 # Set our Asset Object
results['asset'] = asset 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: try:
# Attempt to create an instance of our plugin using the # Attempt to create an instance of our plugin using the
# parsed URL information # parsed URL information
plugin = common.NOTIFY_SCHEMA_MAP[results['schema']](**results) plugin = common.NOTIFY_SCHEMA_MAP[
results['schema']](**results)
# Create log entry of loaded URL # Create log entry of loaded URL
ConfigBase.logger.debug( ConfigBase.logger.debug(
'Loaded URL: %s', plugin.url(privacy=asset.secure_logging)) 'Loaded URL: %s', plugin.url(
privacy=results['asset'].secure_logging))
except Exception as e: except Exception as e:
# the arguments are invalid or can not be used. # the arguments are invalid or can not be used.
ConfigBase.logger.warning( ConfigBase.logger.warning(
'Could not load URL {} on line {}.'.format( 'Could not load URL {} on line {}.'.format(
loggable_url, line)) entry['loggable_url'], entry['line']))
ConfigBase.logger.debug('Loading Exception: %s' % str(e)) ConfigBase.logger.debug('Loading Exception: %s' % str(e))
continue continue

View file

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# BSD 3-Clause License # BSD 2-Clause License
# #
# Apprise - Push Notification Library. # Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com> # Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation # this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution. # 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" # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE

View file

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# BSD 3-Clause License # BSD 2-Clause License
# #
# Apprise - Push Notification Library. # Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com> # Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation # this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution. # 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" # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE

View file

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# BSD 3-Clause License # BSD 2-Clause License
# #
# Apprise - Push Notification Library. # Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com> # Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation # this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution. # 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" # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE

View file

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# BSD 3-Clause License # BSD 2-Clause License
# #
# Apprise - Push Notification Library. # Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com> # Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation # this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution. # 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" # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE

View file

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# BSD 3-Clause License # BSD 2-Clause License
# #
# Apprise - Push Notification Library. # Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com> # Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation # this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution. # 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" # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE

View file

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# BSD 3-Clause License # BSD 2-Clause License
# #
# Apprise - Push Notification Library. # Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com> # Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation # this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution. # 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" # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # 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 # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) # 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 # 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. # POSSIBILITY OF SUCH DAMAGE.
from ..plugins.NotifyBase import NotifyBase from ..plugins.NotifyBase import NotifyBase

View file

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# BSD 3-Clause License # BSD 2-Clause License
# #
# Apprise - Push Notification Library. # Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com> # Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation # this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution. # 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" # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE

View file

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# BSD 3-Clause License # BSD 2-Clause License
# #
# Apprise - Push Notification Library. # Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com> # Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation # this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution. # 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" # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE

View file

@ -6,16 +6,16 @@
#, fuzzy #, fuzzy
msgid "" msgid ""
msgstr "" 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" "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" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n" "Language-Team: LANGUAGE <LL@li.org>\n"
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n" "Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\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." msgid "A local Gnome environment is required."
msgstr "" msgstr ""
@ -164,6 +164,9 @@ msgstr ""
msgid "Consumer Secret" msgid "Consumer Secret"
msgstr "" msgstr ""
msgid "Content Placement"
msgstr ""
msgid "Country" msgid "Country"
msgstr "" msgstr ""
@ -209,6 +212,9 @@ msgstr ""
msgid "Device Name" msgid "Device Name"
msgstr "" msgstr ""
msgid "Discord Event ID"
msgstr ""
msgid "Display Footer" msgid "Display Footer"
msgstr "" msgstr ""
@ -224,12 +230,6 @@ msgstr ""
msgid "Email Header" msgid "Email Header"
msgstr "" msgstr ""
msgid "Encrypted Password"
msgstr ""
msgid "Encrypted Salt"
msgstr ""
msgid "Entity" msgid "Entity"
msgstr "" msgstr ""
@ -272,6 +272,9 @@ msgstr ""
msgid "From Name" msgid "From Name"
msgstr "" msgstr ""
msgid "From Phone ID"
msgstr ""
msgid "From Phone No" msgid "From Phone No"
msgstr "" msgstr ""
@ -317,6 +320,9 @@ msgstr ""
msgid "Integration ID" msgid "Integration ID"
msgstr "" msgstr ""
msgid "Integration Key"
msgstr ""
msgid "Is Ad?" msgid "Is Ad?"
msgstr "" msgstr ""
@ -353,6 +359,9 @@ msgstr ""
msgid "Master Key" msgid "Master Key"
msgstr "" msgstr ""
msgid "Matrix API Verion"
msgstr ""
msgid "Memory" msgid "Memory"
msgstr "" msgstr ""
@ -433,6 +442,12 @@ msgstr ""
msgid "Payload Extras" msgid "Payload Extras"
msgstr "" msgstr ""
msgid "Ping Discord Role"
msgstr ""
msgid "Ping Discord User"
msgstr ""
msgid "Port" msgid "Port"
msgstr "" msgstr ""
@ -451,9 +466,15 @@ msgstr ""
msgid "Provider Key" msgid "Provider Key"
msgstr "" msgstr ""
msgid "Pushkey"
msgstr ""
msgid "QOS" msgid "QOS"
msgstr "" msgstr ""
msgid "Query Method"
msgstr ""
msgid "Region" msgid "Region"
msgstr "" msgstr ""
@ -475,24 +496,27 @@ msgstr ""
msgid "Retry" msgid "Retry"
msgstr "" msgstr ""
msgid "Rooms" msgid "Room ID"
msgstr ""
msgid "Route"
msgstr "" msgstr ""
msgid "Route Group" msgid "Route Group"
msgstr "" msgstr ""
msgid "Routing Key"
msgstr ""
msgid "SMTP Server" msgid "SMTP Server"
msgstr "" msgstr ""
msgid "Salt"
msgstr ""
msgid "Schema" msgid "Schema"
msgstr "" msgstr ""
msgid "Secret"
msgstr ""
msgid "Secret API Key"
msgstr ""
msgid "Secret Access Key" msgid "Secret Access Key"
msgstr "" msgstr ""
@ -520,6 +544,9 @@ msgstr ""
msgid "Severity" msgid "Severity"
msgstr "" msgstr ""
msgid "Short URL"
msgstr ""
msgid "Show Status" msgid "Show Status"
msgstr "" msgstr ""
@ -559,9 +586,6 @@ msgstr ""
msgid "Subtitle" msgid "Subtitle"
msgstr "" msgstr ""
msgid "Syslog Mode"
msgstr ""
msgid "Tags" msgid "Tags"
msgstr "" msgstr ""
@ -658,6 +682,15 @@ msgstr ""
msgid "Template Data" msgid "Template Data"
msgstr "" msgstr ""
msgid "Template ID"
msgstr ""
msgid "Template Mapping"
msgstr ""
msgid "Template Name"
msgstr ""
msgid "Template Path" msgid "Template Path"
msgstr "" msgstr ""
@ -706,12 +739,18 @@ msgstr ""
msgid "Topic" msgid "Topic"
msgstr "" msgstr ""
msgid "Topic Thread ID"
msgstr ""
msgid "Transmitter Groups" msgid "Transmitter Groups"
msgstr "" msgstr ""
msgid "URL" msgid "URL"
msgstr "" msgstr ""
msgid "URL Prefix"
msgstr ""
msgid "URL Title" msgid "URL Title"
msgstr "" msgstr ""
@ -796,3 +835,6 @@ msgstr ""
msgid "ttl" msgid "ttl"
msgstr "" msgstr ""
msgid "validity"
msgstr ""

View file

@ -5,7 +5,8 @@
# #
msgid "" msgid ""
msgstr "" 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" "Report-Msgid-Bugs-To: lead2gold@gmail.com\n"
"POT-Creation-Date: 2019-05-28 16:56-0400\n" "POT-Creation-Date: 2019-05-28 16:56-0400\n"
"PO-Revision-Date: 2019-05-24 20:00-0400\n" "PO-Revision-Date: 2019-05-24 20:00-0400\n"
@ -19,275 +20,271 @@ msgstr ""
"Generated-By: Babel 2.6.0\n" "Generated-By: Babel 2.6.0\n"
msgid "API Key" msgid "API Key"
msgstr "" msgstr "API Key"
msgid "Access Key" msgid "Access Key"
msgstr "" msgstr "Access Key"
msgid "Access Key ID" msgid "Access Key ID"
msgstr "" msgstr "Access Key ID"
msgid "Access Secret" msgid "Access Secret"
msgstr "" msgstr "Access Secret"
msgid "Access Token" msgid "Access Token"
msgstr "" msgstr "Access Token"
msgid "Account SID" msgid "Account SID"
msgstr "" msgstr "Account SID"
msgid "Add Tokens" msgid "Add Tokens"
msgstr "" msgstr "Add Tokens"
msgid "Application Key" msgid "Application Key"
msgstr "" msgstr "Application Key"
msgid "Application Secret" msgid "Application Secret"
msgstr "" msgstr "Application Secret"
msgid "Auth Token" msgid "Auth Token"
msgstr "" msgstr "Auth Token"
msgid "Authorization Token" msgid "Authorization Token"
msgstr "" msgstr "Authorization Token"
msgid "Avatar Image" msgid "Avatar Image"
msgstr "" msgstr "Avatar Image"
msgid "Bot Name" msgid "Bot Name"
msgstr "" msgstr "Bot Name"
msgid "Bot Token" msgid "Bot Token"
msgstr "" msgstr "Bot Token"
msgid "Channels" msgid "Channels"
msgstr "" msgstr "Channels"
msgid "Consumer Key" msgid "Consumer Key"
msgstr "" msgstr "Consumer Key"
msgid "Consumer Secret" msgid "Consumer Secret"
msgstr "" msgstr "Consumer Secret"
msgid "Detect Bot Owner" msgid "Detect Bot Owner"
msgstr "" msgstr "Detect Bot Owner"
msgid "Device ID" msgid "Device ID"
msgstr "" msgstr "Device ID"
msgid "Display Footer" msgid "Display Footer"
msgstr "" msgstr "Display Footer"
msgid "Domain" msgid "Domain"
msgstr "" msgstr "Domain"
msgid "Duration" msgid "Duration"
msgstr "" msgstr "Duration"
msgid "Events" msgid "Events"
msgstr "" msgstr "Events"
msgid "Footer Logo" msgid "Footer Logo"
msgstr "" msgstr "Footer Logo"
msgid "From Email" msgid "From Email"
msgstr "" msgstr "From Email"
msgid "From Name" msgid "From Name"
msgstr "" msgstr "From Name"
msgid "From Phone No" msgid "From Phone No"
msgstr "" msgstr "From Phone No"
msgid "Group" msgid "Group"
msgstr "" msgstr "Group"
msgid "HTTP Header" msgid "HTTP Header"
msgstr "" msgstr "HTTP Header"
msgid "Hostname" msgid "Hostname"
msgstr "" msgstr "Hostname"
msgid "Include Image" msgid "Include Image"
msgstr "" msgstr "Include Image"
msgid "Modal" msgid "Modal"
msgstr "" msgstr "Modal"
msgid "Notify Format" msgid "Notify Format"
msgstr "" msgstr "Notify Format"
msgid "Organization" msgid "Organization"
msgstr "" msgstr "Organization"
msgid "Overflow Mode" msgid "Overflow Mode"
msgstr "" msgstr "Overflow Mode"
msgid "Password" msgid "Password"
msgstr "" msgstr "Password"
msgid "Port" msgid "Port"
msgstr "" msgstr "Port"
msgid "Priority" msgid "Priority"
msgstr "" msgstr "Priority"
msgid "Provider Key" msgid "Provider Key"
msgstr "" msgstr "Provider Key"
msgid "Region" msgid "Region"
msgstr "" msgstr "Region"
msgid "Region Name" msgid "Region Name"
msgstr "" msgstr "Region Name"
msgid "Remove Tokens" msgid "Remove Tokens"
msgstr "" msgstr "Remove Tokens"
msgid "Rooms" msgid "Rooms"
msgstr "" msgstr "Rooms"
msgid "SMTP Server" msgid "SMTP Server"
msgstr "" msgstr "SMTP Server"
msgid "Schema" msgid "Schema"
msgstr "" msgstr "Schema"
msgid "Secret Access Key" msgid "Secret Access Key"
msgstr "" msgstr "Secret Access Key"
msgid "Secret Key" msgid "Secret Key"
msgstr "" msgstr "Secret Key"
msgid "Secure Mode" msgid "Secure Mode"
msgstr "" msgstr "Secure Mode"
msgid "Server Timeout" msgid "Server Timeout"
msgstr "" msgstr "Server Timeout"
msgid "Sound" msgid "Sound"
msgstr "" msgstr "Sound"
msgid "Source JID" msgid "Source JID"
msgstr "" msgstr "Source JID"
msgid "Target Channel" msgid "Target Channel"
msgstr "" msgstr "Target Channel"
msgid "Target Chat ID" msgid "Target Chat ID"
msgstr "" msgstr "Target Chat ID"
msgid "Target Device" msgid "Target Device"
msgstr "" msgstr "Target Device"
msgid "Target Device ID" msgid "Target Device ID"
msgstr "" msgstr "Target Device ID"
msgid "Target Email" msgid "Target Email"
msgstr "" msgstr "Target Email"
msgid "Target Emails" msgid "Target Emails"
msgstr "" msgstr "Target Emails"
msgid "Target Encoded ID" msgid "Target Encoded ID"
msgstr "" msgstr "Target Encoded ID"
msgid "Target JID" msgid "Target JID"
msgstr "" msgstr "Target JID"
msgid "Target Phone No" msgid "Target Phone No"
msgstr "" msgstr "Target Phone No"
msgid "Target Room Alias" msgid "Target Room Alias"
msgstr "" msgstr "Target Room Alias"
msgid "Target Room ID" msgid "Target Room ID"
msgstr "" msgstr "Target Room ID"
msgid "Target Short Code" msgid "Target Short Code"
msgstr "" msgstr "Target Short Code"
msgid "Target Tag ID" msgid "Target Tag ID"
msgstr "" msgstr "Target Tag ID"
msgid "Target Topic" msgid "Target Topic"
msgstr "" msgstr "Target Topic"
msgid "Target User" msgid "Target User"
msgstr "" msgstr "Target User"
msgid "Targets" msgid "Targets"
msgstr "" msgstr "Targets"
msgid "Text To Speech" msgid "Text To Speech"
msgstr "" msgstr "Text To Speech"
msgid "To Channel ID" msgid "To Channel ID"
msgstr "" msgstr "To Channel ID"
msgid "To Email" msgid "To Email"
msgstr "" msgstr "To Email"
msgid "To User ID" msgid "To User ID"
msgstr "" msgstr "To User ID"
msgid "Token" msgid "Token"
msgstr "" msgstr "Token"
msgid "Token A" msgid "Token A"
msgstr "" msgstr "Token A"
msgid "Token B" msgid "Token B"
msgstr "" msgstr "Token B"
msgid "Token C" msgid "Token C"
msgstr "" msgstr "Token C"
msgid "Urgency" msgid "Urgency"
msgstr "" msgstr "Urgency"
msgid "Use Avatar" msgid "Use Avatar"
msgstr "" msgstr "Use Avatar"
msgid "User" msgid "User"
msgstr "" msgstr "User"
msgid "User Key" msgid "User Key"
msgstr "" msgstr "User Key"
msgid "User Name" msgid "User Name"
msgstr "" msgstr "User Name"
msgid "Username" msgid "Username"
msgstr "" msgstr "Username"
msgid "Verify SSL" msgid "Verify SSL"
msgstr "" msgstr "Verify SSL"
msgid "Version" msgid "Version"
msgstr "" msgstr "Version"
msgid "Webhook" msgid "Webhook"
msgstr "" msgstr "Webhook"
msgid "Webhook ID" msgid "Webhook ID"
msgstr "" msgstr "Webhook ID"
msgid "Webhook Mode" msgid "Webhook Mode"
msgstr "" msgstr "Webhook Mode"
msgid "Webhook Token" msgid "Webhook Token"
msgstr "" msgstr "Webhook Token"
msgid "X-Axis" msgid "X-Axis"
msgstr "" msgstr "X-Axis"
msgid "XEP" msgid "XEP"
msgstr "" msgstr "XEP"
msgid "Y-Axis" msgid "Y-Axis"
msgstr "" msgstr "Y-Axis"
#~ msgid "Access Key Secret"
#~ msgstr ""

View file

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# BSD 3-Clause License # BSD 2-Clause License
# #
# Apprise - Push Notification Library. # Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com> # Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation # this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution. # 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" # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE

View file

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# BSD 3-Clause License # BSD 2-Clause License
# #
# Apprise - Push Notification Library. # Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com> # Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation # this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution. # 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" # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
@ -33,6 +29,7 @@
import re import re
import requests import requests
from json import dumps from json import dumps
import base64
from .NotifyBase import NotifyBase from .NotifyBase import NotifyBase
from ..URLBase import PrivacyMode from ..URLBase import PrivacyMode
@ -42,6 +39,20 @@ from ..utils import validate_regex
from ..AppriseLocale import gettext_lazy as _ 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): class NotifyAppriseAPI(NotifyBase):
""" """
A wrapper for Apprise (Persistent) API Notifications 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 # A URL that takes you to the setup/help of the specific protocol
setup_url = 'https://github.com/caronc/apprise/wiki/Notify_apprise_api' 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 # Depending on the number of transactions/notifications taking place, this
# could take a while. 30 seconds should be enough to perform the task # 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 # Disable throttle rate for Apprise API requests since they are normally
# local anyway # local anyway
@ -119,6 +133,12 @@ class NotifyAppriseAPI(NotifyBase):
'name': _('Tags'), 'name': _('Tags'),
'type': 'string', 'type': 'string',
}, },
'method': {
'name': _('Query Method'),
'type': 'choice:string',
'values': APPRISE_API_METHODS,
'default': APPRISE_API_METHODS[0],
},
'to': { 'to': {
'alias_of': 'token', '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 Initialize Apprise API Object
@ -142,10 +163,6 @@ class NotifyAppriseAPI(NotifyBase):
""" """
super().__init__(**kwargs) super().__init__(**kwargs)
self.fullpath = kwargs.get('fullpath')
if not isinstance(self.fullpath, str):
self.fullpath = '/'
self.token = validate_regex( self.token = validate_regex(
token, *self.template_tokens['token']['regex']) token, *self.template_tokens['token']['regex'])
if not self.token: if not self.token:
@ -154,6 +171,14 @@ class NotifyAppriseAPI(NotifyBase):
self.logger.warning(msg) self.logger.warning(msg)
raise TypeError(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 # Build list of tags
self.__tags = parse_list(tags) self.__tags = parse_list(tags)
@ -169,8 +194,13 @@ class NotifyAppriseAPI(NotifyBase):
Returns the URL built dynamically based on specified arguments. Returns the URL built dynamically based on specified arguments.
""" """
# Our URL parameters # Define any URL parameters
params = self.url_parameters(privacy=privacy, *args, **kwargs) params = {
'method': self.method,
}
# Extend our parameters
params.update(self.url_parameters(privacy=privacy, *args, **kwargs))
# Append our headers into our parameters # Append our headers into our parameters
params.update({'+{}'.format(k): v for k, v in self.headers.items()}) 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=''), token=self.pprint(self.token, privacy, safe=''),
params=NotifyAppriseAPI.urlencode(params)) 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 Perform Apprise API Notification
""" """
headers = {} # Prepare HTTP Headers
headers = {
'User-Agent': self.app_id,
}
# Apply any/all header over-rides defined # Apply any/all header over-rides defined
headers.update(self.headers) 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 # prepare Apprise API Object
payload = { payload = {
# Apprise API Payload # Apprise API Payload
@ -227,6 +303,14 @@ class NotifyAppriseAPI(NotifyBase):
'format': self.notify_format, '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: if self.__tags:
payload['tag'] = self.__tags payload['tag'] = self.__tags
@ -242,13 +326,13 @@ class NotifyAppriseAPI(NotifyBase):
url += ':%d' % self.port url += ':%d' % self.port
fullpath = self.fullpath.strip('/') fullpath = self.fullpath.strip('/')
url += '/{}/'.format(fullpath) if fullpath else '/' url += '{}'.format('/' + fullpath) if fullpath else ''
url += 'notify/{}'.format(self.token) url += '/notify/{}'.format(self.token)
# Some entries can not be over-ridden # Some entries can not be over-ridden
headers.update({ headers.update({
'User-Agent': self.app_id, # Our response to be in JSON format always
'Content-Type': 'application/json', 'Accept': 'application/json',
# Pass our Source UUID4 Identifier # Pass our Source UUID4 Identifier
'X-Apprise-ID': self.asset._uid, 'X-Apprise-ID': self.asset._uid,
# Pass our current recursion count to our upstream server # Pass our current recursion count to our upstream server
@ -266,9 +350,10 @@ class NotifyAppriseAPI(NotifyBase):
try: try:
r = requests.post( r = requests.post(
url, url,
data=dumps(payload), data=payload,
headers=headers, headers=headers,
auth=auth, auth=auth,
files=files if files else None,
verify=self.verify_certificate, verify=self.verify_certificate,
timeout=self.request_timeout, timeout=self.request_timeout,
) )
@ -290,7 +375,8 @@ class NotifyAppriseAPI(NotifyBase):
return False return False
else: else:
self.logger.info('Sent Apprise API notification.') self.logger.info(
'Sent Apprise API notification; method=%s.', self.method)
except requests.RequestException as e: except requests.RequestException as e:
self.logger.warning( self.logger.warning(
@ -301,6 +387,18 @@ class NotifyAppriseAPI(NotifyBase):
# Return; we're done # Return; we're done
return False 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 return True
@staticmethod @staticmethod
@ -377,4 +475,9 @@ class NotifyAppriseAPI(NotifyBase):
# re-assemble our full path # re-assemble our full path
results['fullpath'] = '/'.join(entries) 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 return results

View file

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# BSD 3-Clause License # BSD 2-Clause License
# #
# Apprise - Push Notification Library. # Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com> # Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation # this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution. # 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" # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
@ -127,10 +123,10 @@ class NotifyBark(NotifyBase):
# Define object templates # Define object templates
templates = ( templates = (
'{schema}://{host}/{targets}',
'{schema}://{host}:{port}/{targets}', '{schema}://{host}:{port}/{targets}',
'{schema}://{user}:{password}@{host}/{targets}', '{schema}://{user}:{password}@{host}/{targets}',
'{schema}://{user}:{password}@{host}:{port}/{targets}', '{schema}://{user}:{password}@{host}:{port}/{targets}',
'{schema}://{user}:{password}@{host}/{targets}',
) )
# Define our template arguments # Define our template arguments
@ -163,6 +159,7 @@ class NotifyBark(NotifyBase):
'targets': { 'targets': {
'name': _('Targets'), 'name': _('Targets'),
'type': 'list:string', 'type': 'list:string',
'required': True,
}, },
}) })
@ -280,7 +277,7 @@ class NotifyBark(NotifyBase):
# error tracking (used for function return) # error tracking (used for function return)
has_error = False has_error = False
if not len(self.targets): if not self.targets:
# We have nothing to notify; we're done # We have nothing to notify; we're done
self.logger.warning('There are no Bark devices to notify') self.logger.warning('There are no Bark devices to notify')
return False return False
@ -456,6 +453,12 @@ class NotifyBark(NotifyBase):
params=NotifyBark.urlencode(params), params=NotifyBark.urlencode(params),
) )
def __len__(self):
"""
Returns the number of targets associated with this notification
"""
return len(self.targets)
@staticmethod @staticmethod
def parse_url(url): def parse_url(url):
""" """

View file

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# BSD 3-Clause License # BSD 2-Clause License
# #
# Apprise - Push Notification Library. # Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com> # Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation # this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution. # 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" # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
@ -139,6 +135,18 @@ class NotifyBase(URLBase):
# Default Overflow Mode # Default Overflow Mode
overflow_mode = OverflowMode.UPSTREAM 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 # Default Title HTML Tagging
# When a title is specified for a notification service that doesn't accept # 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 # 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) the_cors = (do_send(**kwargs2) for kwargs2 in send_calls)
return all(await asyncio.gather(*the_cors)) 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, notify_type=NotifyType.INFO, overflow=None,
attach=None, body_format=None, **kwargs): attach=None, body_format=None, **kwargs):
""" """
@ -339,6 +347,28 @@ class NotifyBase(URLBase):
# bad attachments # bad attachments
raise 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 # Handle situations where the title is None
title = '' if not title else title title = '' if not title else title

View file

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# BSD 3-Clause License # BSD 2-Clause License
# #
# Apprise - Push Notification Library. # Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com> # Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation # this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution. # 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" # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
@ -46,6 +42,7 @@ except ImportError:
from .NotifyBase import NotifyBase from .NotifyBase import NotifyBase
from ..URLBase import PrivacyMode from ..URLBase import PrivacyMode
from ..utils import parse_bool from ..utils import parse_bool
from ..utils import parse_list
from ..utils import validate_regex from ..utils import validate_regex
from ..common import NotifyType from ..common import NotifyType
from ..common import NotifyImageSize 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 # 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 # boolean operator (and / or) that defines the criteria to match devices
# against those tags. # 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. # Device tokens are only referenced when developing.
# It's not likely you'll send a message directly to a device, but if you do; # 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': { 'to': {
'alias_of': 'targets', 'alias_of': 'targets',
}, },
'access': {
'alias_of': 'access_key',
},
'secret': {
'alias_of': 'secret_key',
},
}) })
def __init__(self, access, secret, targets=None, include_image=True, def __init__(self, access, secret, targets=None, include_image=True,
@ -160,7 +163,7 @@ class NotifyBoxcar(NotifyBase):
super().__init__(**kwargs) super().__init__(**kwargs)
# Initialize tag list # Initialize tag list
self.tags = list() self._tags = list()
# Initialize device_token list # Initialize device_token list
self.device_tokens = list() self.device_tokens = list()
@ -184,25 +187,23 @@ class NotifyBoxcar(NotifyBase):
raise TypeError(msg) raise TypeError(msg)
if not targets: if not targets:
self.tags.append(DEFAULT_TAG) self._tags.append(DEFAULT_TAG)
targets = [] targets = []
elif isinstance(targets, str):
targets = [x for x in filter(bool, TAGS_LIST_DELIM.split(
targets,
))]
# Validate targets and drop bad ones: # Validate targets and drop bad ones:
for target in targets: for target in parse_list(targets):
if IS_TAG.match(target): result = IS_TAG.match(target)
if result:
# store valid tag/alias # 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 # store valid device
self.device_tokens.append(target) self.device_tokens.append(target)
continue
else:
self.logger.warning( self.logger.warning(
'Dropped invalid tag/alias/device_token ' 'Dropped invalid tag/alias/device_token '
'({}) specified.'.format(target), '({}) specified.'.format(target),
@ -235,11 +236,10 @@ class NotifyBoxcar(NotifyBase):
if title: if title:
payload['aps']['@title'] = title payload['aps']['@title'] = title
if body:
payload['aps']['alert'] = body payload['aps']['alert'] = body
if self.tags: if self._tags:
payload['tags'] = {'or': self.tags} payload['tags'] = {'or': self._tags}
if self.device_tokens: if self.device_tokens:
payload['device_tokens'] = self.device_tokens payload['device_tokens'] = self.device_tokens
@ -341,10 +341,18 @@ class NotifyBoxcar(NotifyBase):
self.secret, privacy, mode=PrivacyMode.Secret, safe=''), self.secret, privacy, mode=PrivacyMode.Secret, safe=''),
targets='/'.join([ targets='/'.join([
NotifyBoxcar.quote(x, safe='') for x in chain( 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), 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 @staticmethod
def parse_url(url): def parse_url(url):
""" """
@ -374,6 +382,16 @@ class NotifyBoxcar(NotifyBase):
results['targets'] += \ results['targets'] += \
NotifyBoxcar.parse_list(results['qsd'].get('to')) 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 # Include images with our message
results['include_image'] = \ results['include_image'] = \
parse_bool(results['qsd'].get('image', True)) parse_bool(results['qsd'].get('image', True))

View file

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# BSD 3-Clause License # BSD 2-Clause License
# #
# Apprise - Push Notification Library. # Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com> # Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation # this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution. # 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" # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
@ -121,11 +117,13 @@ class NotifyBulkSMS(NotifyBase):
'user': { 'user': {
'name': _('User Name'), 'name': _('User Name'),
'type': 'string', 'type': 'string',
'required': True,
}, },
'password': { 'password': {
'name': _('Password'), 'name': _('Password'),
'type': 'string', 'type': 'string',
'private': True, 'private': True,
'required': True,
}, },
'target_phone': { 'target_phone': {
'name': _('Target Phone No'), 'name': _('Target Phone No'),
@ -144,6 +142,7 @@ class NotifyBulkSMS(NotifyBase):
'targets': { 'targets': {
'name': _('Targets'), 'name': _('Targets'),
'type': 'list:string', 'type': 'list:string',
'required': True,
}, },
}) })
@ -414,6 +413,24 @@ class NotifyBulkSMS(NotifyBase):
for x in self.groups])), for x in self.groups])),
params=NotifyBulkSMS.urlencode(params)) 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 @staticmethod
def parse_url(url): def parse_url(url):
""" """

View 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

View file

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# BSD 3-Clause License # BSD 2-Clause License
# #
# Apprise - Push Notification Library. # Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com> # Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation # this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution. # 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" # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
@ -288,6 +284,21 @@ class NotifyClickSend(NotifyBase):
params=NotifyClickSend.urlencode(params), 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 @staticmethod
def parse_url(url): def parse_url(url):
""" """

View file

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# BSD 3-Clause License # BSD 2-Clause License
# #
# Apprise - Push Notification Library. # Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com> # Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation # this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution. # 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" # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
@ -114,6 +110,7 @@ class NotifyD7Networks(NotifyBase):
'targets': { 'targets': {
'name': _('Targets'), 'name': _('Targets'),
'type': 'list:string', 'type': 'list:string',
'required': True,
}, },
}) })
@ -357,6 +354,15 @@ class NotifyD7Networks(NotifyBase):
[NotifyD7Networks.quote(x, safe='') for x in self.targets]), [NotifyD7Networks.quote(x, safe='') for x in self.targets]),
params=NotifyD7Networks.urlencode(params)) 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 @staticmethod
def parse_url(url): def parse_url(url):
""" """

View file

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# BSD 3-Clause License # BSD 2-Clause License
# #
# Apprise - Push Notification Library. # Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com> # Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation # this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution. # 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" # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE

View file

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# BSD 3-Clause License # BSD 2-Clause License
# #
# Apprise - Push Notification Library. # Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com> # Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation # this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution. # 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" # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
@ -350,6 +346,21 @@ class NotifyDapnet(NotifyBase):
params=NotifyDapnet.urlencode(params), 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 @staticmethod
def parse_url(url): def parse_url(url):
""" """

View file

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# BSD 3-Clause License # BSD 2-Clause License
# #
# Apprise - Push Notification Library. # Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com> # Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation # this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution. # 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" # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
@ -103,13 +99,18 @@ class NotifyDingTalk(NotifyBase):
'regex': (r'^[a-z0-9]+$', 'i'), 'regex': (r'^[a-z0-9]+$', 'i'),
}, },
'secret': { 'secret': {
'name': _('Token'), 'name': _('Secret'),
'type': 'string', 'type': 'string',
'private': True, 'private': True,
'regex': (r'^[a-z0-9]+$', 'i'), 'regex': (r'^[a-z0-9]+$', 'i'),
}, },
'targets': { 'target_phone_no': {
'name': _('Target Phone No'), 'name': _('Target Phone No'),
'type': 'string',
'map_to': 'targets',
},
'targets': {
'name': _('Targets'),
'type': 'list:string', 'type': 'list:string',
}, },
}) })
@ -309,6 +310,13 @@ class NotifyDingTalk(NotifyBase):
[NotifyDingTalk.quote(x, safe='') for x in self.targets]), [NotifyDingTalk.quote(x, safe='') for x in self.targets]),
args=NotifyDingTalk.urlencode(args)) 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 @staticmethod
def parse_url(url): def parse_url(url):
""" """

View file

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# BSD 3-Clause License # BSD 2-Clause License
# #
# Apprise - Push Notification Library. # Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com> # Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation # this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution. # 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" # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
@ -50,6 +46,9 @@
import re import re
import requests import requests
from json import dumps from json import dumps
from datetime import timedelta
from datetime import datetime
from datetime import timezone
from .NotifyBase import NotifyBase from .NotifyBase import NotifyBase
from ..common import NotifyImageSize from ..common import NotifyImageSize
@ -81,9 +80,23 @@ class NotifyDiscord(NotifyBase):
# Discord Webhook # Discord Webhook
notify_url = 'https://discord.com/api/webhooks' notify_url = 'https://discord.com/api/webhooks'
# Support attachments
attachment_support = True
# Allows the user to specify the NotifyImageSize object # Allows the user to specify the NotifyImageSize object
image_size = NotifyImageSize.XY_256 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 # The maximum allowable characters allowed in the body per message
body_maxlen = 2000 body_maxlen = 2000
@ -135,6 +148,13 @@ class NotifyDiscord(NotifyBase):
'name': _('Avatar URL'), 'name': _('Avatar URL'),
'type': 'string', 'type': 'string',
}, },
'href': {
'name': _('URL'),
'type': 'string',
},
'url': {
'alias_of': 'href',
},
# Send a message to the specified thread within a webhook's channel. # Send a message to the specified thread within a webhook's channel.
# The thread will automatically be unarchived. # The thread will automatically be unarchived.
'thread': { 'thread': {
@ -166,7 +186,8 @@ class NotifyDiscord(NotifyBase):
def __init__(self, webhook_id, webhook_token, tts=False, avatar=True, def __init__(self, webhook_id, webhook_token, tts=False, avatar=True,
footer=False, footer_logo=True, include_image=False, footer=False, footer_logo=True, include_image=False,
fields=True, avatar_url=None, thread=None, **kwargs): fields=True, avatar_url=None, href=None, thread=None,
**kwargs):
""" """
Initialize Discord Object Initialize Discord Object
@ -215,6 +236,15 @@ class NotifyDiscord(NotifyBase):
# dynamically generated avatar url images # dynamically generated avatar url images
self.avatar_url = avatar_url 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 return
def send(self, body, title='', notify_type=NotifyType.INFO, attach=None, def send(self, body, title='', notify_type=NotifyType.INFO, attach=None,
@ -235,6 +265,18 @@ class NotifyDiscord(NotifyBase):
# Acquire image_url # Acquire image_url
image_url = self.image_url(notify_type) image_url = self.image_url(notify_type)
if self.avatar and (image_url or self.avatar_url):
payload['avatar_url'] = \
self.avatar_url if self.avatar_url else image_url
if self.user:
# Optionally override the default username of the webhook
payload['username'] = self.user
# Associate our thread_id with our message
params = {'thread_id': self.thread_id} if self.thread_id else None
if body:
# our fields variable # our fields variable
fields = [] fields = []
@ -252,6 +294,9 @@ class NotifyDiscord(NotifyBase):
'color': self.color(notify_type, int), 'color': self.color(notify_type, int),
}] }]
if self.href:
payload['embeds'][0]['url'] = self.href
if self.footer: if self.footer:
# Acquire logo URL # Acquire logo URL
logo_url = self.image_url(notify_type, logo=True) logo_url = self.image_url(notify_type, logo=True)
@ -278,7 +323,8 @@ class NotifyDiscord(NotifyBase):
# Swap first entry for description # Swap first entry for description
payload['embeds'][0]['description'] = description payload['embeds'][0]['description'] = description
if fields: if fields:
# Apply our additional parsing for a better presentation # Apply our additional parsing for a better
# presentation
payload['embeds'][0]['fields'] = \ payload['embeds'][0]['fields'] = \
fields[:self.discord_max_fields] fields[:self.discord_max_fields]
@ -290,18 +336,7 @@ class NotifyDiscord(NotifyBase):
payload['content'] = \ payload['content'] = \
body if not title else "{}\r\n{}".format(title, body) body if not title else "{}\r\n{}".format(title, body)
if self.thread_id: if not self._send(payload, params=params):
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
if self.user:
# Optionally override the default username of the webhook
payload['username'] = self.user
if not self._send(payload):
# We failed to post our message # We failed to post our message
return False return False
@ -315,7 +350,7 @@ class NotifyDiscord(NotifyBase):
# We failed to post our message # We failed to post our message
return False return False
if attach: if attach and self.attachment_support:
# Update our payload; the idea is to preserve it's other detected # Update our payload; the idea is to preserve it's other detected
# and assigned values for re-use here too # and assigned values for re-use here too
payload.update({ payload.update({
@ -338,14 +373,15 @@ class NotifyDiscord(NotifyBase):
for attachment in attach: for attachment in attach:
self.logger.info( self.logger.info(
'Posting Discord Attachment {}'.format(attachment.name)) '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 # We failed to post our message
return False return False
# Otherwise return # Otherwise return
return True 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 Wrapper to the requests (post) object
""" """
@ -367,8 +403,25 @@ class NotifyDiscord(NotifyBase):
)) ))
self.logger.debug('Discord Payload: %s' % str(payload)) self.logger.debug('Discord Payload: %s' % str(payload))
# Always call throttle before any remote server i/o is made # By default set wait to None
self.throttle() 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 # Perform some simple error checking
if isinstance(attach, AttachBase): if isinstance(attach, AttachBase):
@ -396,12 +449,29 @@ class NotifyDiscord(NotifyBase):
r = requests.post( r = requests.post(
notify_url, notify_url,
params=params,
data=payload if files else dumps(payload), data=payload if files else dumps(payload),
headers=headers, headers=headers,
files=files, files=files,
verify=self.verify_certificate, verify=self.verify_certificate,
timeout=self.request_timeout, 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 ( if r.status_code not in (
requests.codes.ok, requests.codes.no_content): requests.codes.ok, requests.codes.no_content):
@ -409,6 +479,20 @@ class NotifyDiscord(NotifyBase):
status_str = \ status_str = \
NotifyBase.http_response_code_lookup(r.status_code) 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( self.logger.warning(
'Failed to send {}to Discord notification: ' 'Failed to send {}to Discord notification: '
'{}{}error={}.'.format( '{}{}error={}.'.format(
@ -466,6 +550,9 @@ class NotifyDiscord(NotifyBase):
if self.avatar_url: if self.avatar_url:
params['avatar_url'] = self.avatar_url params['avatar_url'] = self.avatar_url
if self.href:
params['href'] = self.href
if self.thread_id: if self.thread_id:
params['thread'] = self.thread_id params['thread'] = self.thread_id
@ -537,10 +624,23 @@ class NotifyDiscord(NotifyBase):
results['avatar_url'] = \ results['avatar_url'] = \
NotifyDiscord.unquote(results['qsd']['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 # Extract thread id if it was specified
if 'thread' in results['qsd']: if 'thread' in results['qsd']:
results['thread'] = \ results['thread'] = \
NotifyDiscord.unquote(results['qsd']['thread']) NotifyDiscord.unquote(results['qsd']['thread'])
# Markdown is implied
results['format'] = NotifyFormat.MARKDOWN
return results return results

View file

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# BSD 3-Clause License # BSD 2-Clause License
# #
# Apprise - Push Notification Library. # Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com> # Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation # this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution. # 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" # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # 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 socket import error as SocketError
from datetime import datetime from datetime import datetime
from datetime import timezone
from .NotifyBase import NotifyBase from .NotifyBase import NotifyBase
from ..URLBase import PrivacyMode from ..URLBase import PrivacyMode
@ -340,6 +337,9 @@ class NotifyEmail(NotifyBase):
# A URL that takes you to the setup/help of the specific protocol # A URL that takes you to the setup/help of the specific protocol
setup_url = 'https://github.com/caronc/apprise/wiki/Notify_email' setup_url = 'https://github.com/caronc/apprise/wiki/Notify_email'
# Support attachments
attachment_support = True
# Default Notify Format # Default Notify Format
notify_format = NotifyFormat.HTML notify_format = NotifyFormat.HTML
@ -384,8 +384,13 @@ class NotifyEmail(NotifyBase):
'min': 1, 'min': 1,
'max': 65535, 'max': 65535,
}, },
'target_email': {
'name': _('Target Email'),
'type': 'string',
'map_to': 'targets',
},
'targets': { 'targets': {
'name': _('Target Emails'), 'name': _('Targets'),
'type': 'list:string', 'type': 'list:string',
}, },
}) })
@ -764,7 +769,7 @@ class NotifyEmail(NotifyBase):
else: else:
base = MIMEText(body, 'plain', 'utf-8') base = MIMEText(body, 'plain', 'utf-8')
if attach: if attach and self.attachment_support:
mixed = MIMEMultipart("mixed") mixed = MIMEMultipart("mixed")
mixed.attach(base) mixed.attach(base)
# Now store our attachments # Now store our attachments
@ -805,7 +810,8 @@ class NotifyEmail(NotifyBase):
base['To'] = formataddr((to_name, to_addr), charset='utf-8') base['To'] = formataddr((to_name, to_addr), charset='utf-8')
base['Message-ID'] = make_msgid(domain=self.smtp_host) base['Message-ID'] = make_msgid(domain=self.smtp_host)
base['Date'] = \ 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 base['X-Application'] = self.app_id
if cc: if cc:
@ -999,6 +1005,13 @@ class NotifyEmail(NotifyBase):
params=NotifyEmail.urlencode(params), 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 @staticmethod
def parse_url(url): def parse_url(url):
""" """
@ -1023,6 +1036,10 @@ class NotifyEmail(NotifyBase):
# add one to ourselves # add one to ourselves
results['targets'] = NotifyEmail.split_path(results['fullpath']) 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 # Attempt to detect 'from' email address
if 'from' in results['qsd'] and len(results['qsd']['from']): if 'from' in results['qsd'] and len(results['qsd']['from']):
from_addr = NotifyEmail.unquote(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 # Extract from name to associate with from address
from_addr = NotifyEmail.unquote(results['qsd']['name']) 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 # Store SMTP Host if specified
if 'smtp' in results['qsd'] and len(results['qsd']['smtp']): if 'smtp' in results['qsd'] and len(results['qsd']['smtp']):
# Extract the smtp server # Extract the smtp server

View file

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# BSD 3-Clause License # BSD 2-Clause License
# #
# Apprise - Push Notification Library. # Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com> # Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation # this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution. # 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" # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE

View file

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# BSD 3-Clause License # BSD 2-Clause License
# #
# Apprise - Push Notification Library. # Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com> # Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation # this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution. # 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" # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE

View file

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# BSD 3-Clause License # BSD 2-Clause License
# #
# Apprise - Push Notification Library. # Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com> # Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation # this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution. # 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" # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
@ -157,7 +153,6 @@ class NotifyFCM(NotifyBase):
'project': { 'project': {
'name': _('Project ID'), 'name': _('Project ID'),
'type': 'string', 'type': 'string',
'required': True,
}, },
'target_device': { 'target_device': {
'name': _('Target Device'), 'name': _('Target Device'),
@ -173,6 +168,7 @@ class NotifyFCM(NotifyBase):
'targets': { 'targets': {
'name': _('Targets'), 'name': _('Targets'),
'type': 'list:string', 'type': 'list:string',
'required': True,
}, },
}) })
@ -555,6 +551,12 @@ class NotifyFCM(NotifyBase):
params=NotifyFCM.urlencode(params), params=NotifyFCM.urlencode(params),
) )
def __len__(self):
"""
Returns the number of targets associated with this notification
"""
return len(self.targets)
@staticmethod @staticmethod
def parse_url(url): def parse_url(url):
""" """

View file

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# BSD 3-Clause License # BSD 2-Clause License
# #
# Apprise - Push Notification Library. # Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com> # Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation # this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution. # 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" # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE

View file

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# BSD 3-Clause License # BSD 2-Clause License
# #
# Apprise - Push Notification Library. # Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com> # Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation # this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution. # 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" # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE

View file

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# BSD 3-Clause License # BSD 2-Clause License
# #
# Apprise - Push Notification Library. # Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com> # Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation # this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution. # 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" # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # 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 cryptography.exceptions import UnsupportedAlgorithm
from datetime import datetime from datetime import datetime
from datetime import timedelta from datetime import timedelta
from datetime import timezone
from json.decoder import JSONDecodeError from json.decoder import JSONDecodeError
from urllib.parse import urlencode as _urlencode from urllib.parse import urlencode as _urlencode
@ -106,7 +103,7 @@ class GoogleOAuth:
# Our keys we build using the provided content # Our keys we build using the provided content
self.__refresh_token = None self.__refresh_token = None
self.__access_token = None self.__access_token = None
self.__access_token_expiry = datetime.utcnow() self.__access_token_expiry = datetime.now(timezone.utc)
def load(self, path): def load(self, path):
""" """
@ -117,7 +114,7 @@ class GoogleOAuth:
self.content = None self.content = None
self.private_key = None self.private_key = None
self.__access_token = None self.__access_token = None
self.__access_token_expiry = datetime.utcnow() self.__access_token_expiry = datetime.now(timezone.utc)
try: try:
with open(path, mode="r", encoding=self.encoding) as fp: with open(path, mode="r", encoding=self.encoding) as fp:
@ -199,7 +196,7 @@ class GoogleOAuth:
'token with.') 'token with.')
return None return None
if self.__access_token_expiry > datetime.utcnow(): if self.__access_token_expiry > datetime.now(timezone.utc):
# Return our no-expired key # Return our no-expired key
return self.__access_token return self.__access_token
@ -209,7 +206,7 @@ class GoogleOAuth:
key_identifier = self.content.get('private_key_id') key_identifier = self.content.get('private_key_id')
# Generate our Assertion # Generate our Assertion
now = datetime.utcnow() now = datetime.now(timezone.utc)
expiry = now + self.access_token_lifetime_sec expiry = now + self.access_token_lifetime_sec
payload = { payload = {
@ -301,7 +298,7 @@ class GoogleOAuth:
if 'expires_in' in response: if 'expires_in' in response:
delta = timedelta(seconds=int(response['expires_in'])) delta = timedelta(seconds=int(response['expires_in']))
self.__access_token_expiry = \ self.__access_token_expiry = \
delta + datetime.utcnow() - self.clock_skew delta + datetime.now(timezone.utc) - self.clock_skew
else: else:
# Allow some grace before we expire # Allow some grace before we expire

View file

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# BSD 3-Clause License # BSD 2-Clause License
# #
# Apprise - Push Notification Library. # Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com> # Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation # this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution. # 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" # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE

View file

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# BSD 3-Clause License # BSD 2-Clause License
# #
# Apprise - Push Notification Library. # Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com> # Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation # this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution. # 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" # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE

View file

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# BSD 3-Clause License # BSD 2-Clause License
# #
# Apprise - Push Notification Library. # Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com> # Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation # this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution. # 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" # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
@ -97,8 +93,8 @@ class NotifyFlock(NotifyBase):
# Define object templates # Define object templates
templates = ( templates = (
'{schema}://{token}', '{schema}://{token}',
'{schema}://{user}@{token}', '{schema}://{botname}@{token}',
'{schema}://{user}@{token}/{targets}', '{schema}://{botname}@{token}/{targets}',
'{schema}://{token}/{targets}', '{schema}://{token}/{targets}',
) )
@ -111,9 +107,10 @@ class NotifyFlock(NotifyBase):
'private': True, 'private': True,
'required': True, 'required': True,
}, },
'user': { 'botname': {
'name': _('Bot Name'), 'name': _('Bot Name'),
'type': 'string', 'type': 'string',
'map_to': 'user',
}, },
'to_user': { 'to_user': {
'name': _('To User ID'), 'name': _('To User ID'),
@ -334,6 +331,13 @@ class NotifyFlock(NotifyBase):
params=NotifyFlock.urlencode(params), 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 @staticmethod
def parse_url(url): def parse_url(url):
""" """

View file

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# BSD 3-Clause License # BSD 2-Clause License
# #
# Apprise - Push Notification Library. # Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com> # Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation # this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution. # 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" # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
@ -40,6 +36,16 @@ from ..common import NotifyType
from ..AppriseLocale import gettext_lazy as _ 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 # Defines the method to send the notification
METHODS = ( METHODS = (
'POST', 'POST',
@ -89,6 +95,9 @@ class NotifyForm(NotifyBase):
# A URL that takes you to the setup/help of the specific protocol # A URL that takes you to the setup/help of the specific protocol
setup_url = 'https://github.com/caronc/apprise/wiki/Notify_Custom_Form' setup_url = 'https://github.com/caronc/apprise/wiki/Notify_Custom_Form'
# Support attachments
attachment_support = True
# Allows the user to specify the NotifyImageSize object # Allows the user to specify the NotifyImageSize object
image_size = NotifyImageSize.XY_128 image_size = NotifyImageSize.XY_128
@ -96,6 +105,12 @@ class NotifyForm(NotifyBase):
# local anyway # local anyway
request_rate_per_sec = 0 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 # Define object templates
templates = ( templates = (
'{schema}://{host}', '{schema}://{host}',
@ -218,6 +233,18 @@ class NotifyForm(NotifyBase):
self.attach_as += self.attach_as_count self.attach_as += self.attach_as_count
self.attach_multi_support = True 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 = {} self.params = {}
if params: if params:
# Store our extra headers # Store our extra headers
@ -228,10 +255,20 @@ class NotifyForm(NotifyBase):
# Store our extra headers # Store our extra headers
self.headers.update(headers) self.headers.update(headers)
self.payload_overrides = {}
self.payload_extras = {} self.payload_extras = {}
if payload: if payload:
# Store our extra payload entries # Store our extra payload entries
self.payload_extras.update(payload) 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 return
@ -257,6 +294,8 @@ class NotifyForm(NotifyBase):
# Append our payload extra's into our parameters # Append our payload extra's into our parameters
params.update( params.update(
{':{}'.format(k): v for k, v in self.payload_extras.items()}) {':{}'.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: if self.attach_as != self.attach_as_default:
# Provide Attach-As extension details # Provide Attach-As extension details
@ -305,7 +344,7 @@ class NotifyForm(NotifyBase):
# Track our potential attachments # Track our potential attachments
files = [] files = []
if attach: if attach and self.attachment_support:
for no, attachment in enumerate(attach, start=1): for no, attachment in enumerate(attach, start=1):
# Perform some simple error checking # Perform some simple error checking
if not attachment: if not attachment:
@ -337,15 +376,18 @@ class NotifyForm(NotifyBase):
'form:// Multi-Attachment Support not enabled') 'form:// Multi-Attachment Support not enabled')
# prepare Form Object # prepare Form Object
payload = { payload = {}
# Version: Major.Minor, Major is only updated if the entire
# schema is changed. If just adding new items (or removing for key, value in (
# old ones, only increment the Minor! (FORMPayloadField.VERSION, self.form_version),
'version': '1.0', (FORMPayloadField.TITLE, title),
'title': title, (FORMPayloadField.MESSAGE, body),
'message': body, (FORMPayloadField.MESSAGETYPE, notify_type)):
'type': 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 # Apply any/all payload over-rides defined
payload.update(self.payload_extras) payload.update(self.payload_extras)

View file

@ -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

View file

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# BSD 3-Clause License # BSD 2-Clause License
# #
# Apprise - Push Notification Library. # Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com> # Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation # this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution. # 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" # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE

View file

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# BSD 3-Clause License # BSD 2-Clause License
# #
# Apprise - Push Notification Library. # Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com> # Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation # this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution. # 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" # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE

View file

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# BSD 3-Clause License # BSD 2-Clause License
# #
# Apprise - Push Notification Library. # Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com> # Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation # this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution. # 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" # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
@ -134,7 +130,6 @@ class NotifyGotify(NotifyBase):
'type': 'string', 'type': 'string',
'map_to': 'fullpath', 'map_to': 'fullpath',
'default': '/', 'default': '/',
'required': True,
}, },
'port': { 'port': {
'name': _('Port'), 'name': _('Port'),

View file

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# BSD 3-Clause License # BSD 2-Clause License
# #
# Apprise - Push Notification Library. # Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com> # Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation # this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution. # 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" # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE

View file

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# BSD 3-Clause License # BSD 2-Clause License
# #
# Apprise - Push Notification Library. # Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com> # Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation # this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution. # 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" # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE

View file

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# BSD 3-Clause License # BSD 2-Clause License
# #
# Apprise - Push Notification Library. # Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com> # Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation # this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution. # 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" # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE

View file

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# BSD 3-Clause License # BSD 2-Clause License
# #
# Apprise - Push Notification Library. # Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com> # Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation # this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution. # 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" # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # 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 # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE. # POSSIBILITY OF SUCH DAMAGE.
#
# For this plugin to work, you need to add the Maker applet to your profile # 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' # Simply visit https://ifttt.com/search and search for 'Webhooks'
# Or if you're signed in, click here: https://ifttt.com/maker_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), params=NotifyIFTTT.urlencode(params),
) )
def __len__(self):
"""
Returns the number of targets associated with this notification
"""
return len(self.events)
@staticmethod @staticmethod
def parse_url(url): def parse_url(url):
""" """

View file

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# BSD 3-Clause License # BSD 2-Clause License
# #
# Apprise - Push Notification Library. # Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com> # Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation # this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution. # 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" # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
@ -41,6 +37,17 @@ from ..common import NotifyType
from ..AppriseLocale import gettext_lazy as _ 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 # Defines the method to send the notification
METHODS = ( METHODS = (
'POST', 'POST',
@ -69,6 +76,9 @@ class NotifyJSON(NotifyBase):
# A URL that takes you to the setup/help of the specific protocol # A URL that takes you to the setup/help of the specific protocol
setup_url = 'https://github.com/caronc/apprise/wiki/Notify_Custom_JSON' setup_url = 'https://github.com/caronc/apprise/wiki/Notify_Custom_JSON'
# Support attachments
attachment_support = True
# Allows the user to specify the NotifyImageSize object # Allows the user to specify the NotifyImageSize object
image_size = NotifyImageSize.XY_128 image_size = NotifyImageSize.XY_128
@ -76,6 +86,12 @@ class NotifyJSON(NotifyBase):
# local anyway # local anyway
request_rate_per_sec = 0 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 # Define object templates
templates = ( templates = (
'{schema}://{host}', '{schema}://{host}',
@ -246,7 +262,7 @@ class NotifyJSON(NotifyBase):
# Track our potential attachments # Track our potential attachments
attachments = [] attachments = []
if attach: if attach and self.attachment_support:
for attachment in attach: for attachment in attach:
# Perform some simple error checking # Perform some simple error checking
if not attachment: if not attachment:
@ -274,20 +290,30 @@ class NotifyJSON(NotifyBase):
self.logger.debug('I/O Exception: %s' % str(e)) self.logger.debug('I/O Exception: %s' % str(e))
return False return False
# prepare JSON Object # Prepare JSON Object
payload = { payload = {
# Version: Major.Minor, Major is only updated if the entire JSONPayloadField.VERSION: self.json_version,
# schema is changed. If just adding new items (or removing JSONPayloadField.TITLE: title,
# old ones, only increment the Minor! JSONPayloadField.MESSAGE: body,
'version': '1.0', JSONPayloadField.ATTACHMENTS: attachments,
'title': title, JSONPayloadField.MESSAGETYPE: notify_type,
'message': body,
'attachments': attachments,
'type': notify_type,
} }
# Apply any/all payload over-rides defined for key, value in self.payload_extras.items():
payload.update(self.payload_extras)
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 auth = None
if self.user: if self.user:

View file

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# BSD 3-Clause License # BSD 2-Clause License
# #
# Apprise - Push Notification Library. # Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com> # Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation # this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution. # 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" # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
@ -174,7 +170,6 @@ class NotifyJoin(NotifyBase):
'targets': { 'targets': {
'name': _('Targets'), 'name': _('Targets'),
'type': 'list:string', 'type': 'list:string',
'required': True,
}, },
}) })
@ -373,6 +368,12 @@ class NotifyJoin(NotifyBase):
for x in self.targets]), for x in self.targets]),
params=NotifyJoin.urlencode(params)) params=NotifyJoin.urlencode(params))
def __len__(self):
"""
Returns the number of targets associated with this notification
"""
return len(self.targets)
@staticmethod @staticmethod
def parse_url(url): def parse_url(url):
""" """

View file

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# BSD 3-Clause License # BSD 2-Clause License
# #
# Apprise - Push Notification Library. # Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com> # Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation # this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution. # 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" # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # 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]), [NotifyKavenegar.quote(x, safe='') for x in self.targets]),
params=NotifyKavenegar.urlencode(params)) params=NotifyKavenegar.urlencode(params))
def __len__(self):
"""
Returns the number of targets associated with this notification
"""
return len(self.targets)
@staticmethod @staticmethod
def parse_url(url): def parse_url(url):
""" """

View file

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# BSD 3-Clause License # BSD 2-Clause License
# #
# Apprise - Push Notification Library. # Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com> # Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation # this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution. # 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" # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE

View file

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# BSD 3-Clause License # BSD 2-Clause License
# #
# Apprise - Push Notification Library. # Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com> # Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation # this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution. # 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" # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
@ -370,6 +366,7 @@ class NotifyLametric(NotifyBase):
# Device Mode # Device Mode
'{schema}://{apikey}@{host}', '{schema}://{apikey}@{host}',
'{schema}://{user}:{apikey}@{host}',
'{schema}://{apikey}@{host}:{port}', '{schema}://{apikey}@{host}:{port}',
'{schema}://{user}:{apikey}@{host}:{port}', '{schema}://{user}:{apikey}@{host}:{port}',
) )
@ -404,7 +401,6 @@ class NotifyLametric(NotifyBase):
'host': { 'host': {
'name': _('Hostname'), 'name': _('Hostname'),
'type': 'string', 'type': 'string',
'required': True,
}, },
'port': { 'port': {
'name': _('Port'), 'name': _('Port'),

View file

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# BSD 3-Clause License # BSD 2-Clause License
# #
# Apprise - Push Notification Library. # Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com> # Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation # this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution. # 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" # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
@ -102,6 +98,7 @@ class NotifyLine(NotifyBase):
'targets': { 'targets': {
'name': _('Targets'), 'name': _('Targets'),
'type': 'list:string', 'type': 'list:string',
'required': True
}, },
}) })
@ -267,6 +264,12 @@ class NotifyLine(NotifyBase):
params=NotifyLine.urlencode(params), params=NotifyLine.urlencode(params),
) )
def __len__(self):
"""
Returns the number of targets associated with this notification
"""
return len(self.targets)
@staticmethod @staticmethod
def parse_url(url): def parse_url(url):
""" """

View file

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# BSD 3-Clause License # BSD 2-Clause License
# #
# Apprise - Push Notification Library. # Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com> # Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation # this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution. # 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" # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # 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)) self.logger.debug('Socket Exception: %s' % str(e))
return False return False
if not has_error:
# Verbal notice
self.logger.info('Sent MQTT notification')
return not has_error return not has_error
def url(self, privacy=False, *args, **kwargs): def url(self, privacy=False, *args, **kwargs):
@ -476,6 +476,12 @@ class NotifyMQTT(NotifyBase):
params=NotifyMQTT.urlencode(params), params=NotifyMQTT.urlencode(params),
) )
def __len__(self):
"""
Returns the number of targets associated with this notification
"""
return len(self.topics)
@staticmethod @staticmethod
def parse_url(url): def parse_url(url):
""" """

View file

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# BSD 3-Clause License # BSD 2-Clause License
# #
# Apprise - Push Notification Library. # Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com> # Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation # this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution. # 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" # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
@ -35,50 +31,31 @@
# Get your (authkey) from the dashboard here: # Get your (authkey) from the dashboard here:
# - https://world.msg91.com/user/index.php#api # - 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: # 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 import requests
from json import dumps
from .NotifyBase import NotifyBase from .NotifyBase import NotifyBase
from ..common import NotifyType from ..common import NotifyType
from ..utils import is_phone_no 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 ..utils import validate_regex
from ..AppriseLocale import gettext_lazy as _ from ..AppriseLocale import gettext_lazy as _
class MSG91Route: class MSG91PayloadField:
""" """
Transactional SMS Routes Identifies the fields available in the JSON Payload
route=1 for promotional, route=4 for transactional SMS.
""" """
PROMOTIONAL = 1 BODY = 'body'
TRANSACTIONAL = 4 MESSAGETYPE = 'type'
# Used for verification # Add entries here that are reserved
MSG91_ROUTES = ( RESERVED_KEYWORDS = ('mobiles', )
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,
)
class NotifyMSG91(NotifyBase): class NotifyMSG91(NotifyBase):
@ -99,7 +76,7 @@ class NotifyMSG91(NotifyBase):
setup_url = 'https://github.com/caronc/apprise/wiki/Notify_msg91' setup_url = 'https://github.com/caronc/apprise/wiki/Notify_msg91'
# MSG91 uses the http protocol with JSON requests # 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 # The maximum length of the body
body_maxlen = 160 body_maxlen = 160
@ -108,14 +85,24 @@ class NotifyMSG91(NotifyBase):
# cause any title (if defined) to get placed into the message body. # cause any title (if defined) to get placed into the message body.
title_maxlen = 0 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 # Define object templates
templates = ( templates = (
'{schema}://{authkey}/{targets}', '{schema}://{template}@{authkey}/{targets}',
'{schema}://{sender}@{authkey}/{targets}',
) )
# Define our template tokens # Define our template tokens
template_tokens = dict(NotifyBase.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': { 'authkey': {
'name': _('Authentication Key'), 'name': _('Authentication Key'),
'type': 'string', 'type': 'string',
@ -133,10 +120,7 @@ class NotifyMSG91(NotifyBase):
'targets': { 'targets': {
'name': _('Targets'), 'name': _('Targets'),
'type': 'list:string', 'type': 'list:string',
}, 'required': True,
'sender': {
'name': _('Sender ID'),
'type': 'string',
}, },
}) })
@ -145,21 +129,23 @@ class NotifyMSG91(NotifyBase):
'to': { 'to': {
'alias_of': 'targets', 'alias_of': 'targets',
}, },
'route': { 'short_url': {
'name': _('Route'), 'name': _('Short URL'),
'type': 'choice:int', 'type': 'bool',
'values': MSG91_ROUTES, 'default': False,
'default': MSG91Route.TRANSACTIONAL,
},
'country': {
'name': _('Country'),
'type': 'choice:int',
'values': MSG91_COUNTRIES,
}, },
}) })
def __init__(self, authkey, targets=None, sender=None, route=None, # Define any kwargs we're using
country=None, **kwargs): 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 Initialize MSG91 Object
""" """
@ -174,39 +160,20 @@ class NotifyMSG91(NotifyBase):
self.logger.warning(msg) self.logger.warning(msg)
raise TypeError(msg) raise TypeError(msg)
if route is None: # Template ID
self.route = self.template_args['route']['default'] self.template = validate_regex(
template, *self.template_tokens['template']['regex'])
else: if not self.template:
try: msg = 'An invalid MSG91 Template ID ' \
self.route = int(route) '({}) was specified.'.format(template)
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) self.logger.warning(msg)
raise TypeError(msg) raise TypeError(msg)
if country: if short_url is None:
try: self.short_url = self.template_args['short_url']['default']
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: else:
self.country = country self.short_url = parse_bool(short_url)
# Store our sender
self.sender = sender
# Parse our targets # Parse our targets
self.targets = list() self.targets = list()
@ -224,6 +191,11 @@ class NotifyMSG91(NotifyBase):
# store valid phone number # store valid phone number
self.targets.append(result['full']) self.targets.append(result['full'])
self.template_mapping = {}
if template_mapping:
# Store our extra payload entries
self.template_mapping.update(template_mapping)
return return
def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs): def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs):
@ -239,23 +211,55 @@ class NotifyMSG91(NotifyBase):
# Prepare our headers # Prepare our headers
headers = { headers = {
'User-Agent': self.app_id, '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 # Prepare our payload
payload = { payload = {
'sender': self.sender if self.sender else self.app_id, 'template_id': self.template,
'authkey': self.authkey, 'short_url': 1 if self.short_url else 0,
'message': body,
'response': 'json',
# target phone numbers are sent with a comma delimiter # target phone numbers are sent with a comma delimiter
'mobiles': ','.join(self.targets), 'recipients': recipients,
'route': str(self.route),
} }
if self.country:
payload['country'] = str(self.country)
# Some Debug Logging # Some Debug Logging
self.logger.debug('MSG91 POST URL: {} (cert_verify={})'.format( self.logger.debug('MSG91 POST URL: {} (cert_verify={})'.format(
self.notify_url, self.verify_certificate)) self.notify_url, self.verify_certificate))
@ -267,7 +271,7 @@ class NotifyMSG91(NotifyBase):
try: try:
r = requests.post( r = requests.post(
self.notify_url, self.notify_url,
data=payload, data=dumps(payload),
headers=headers, headers=headers,
verify=self.verify_certificate, verify=self.verify_certificate,
timeout=self.request_timeout, timeout=self.request_timeout,
@ -313,22 +317,32 @@ class NotifyMSG91(NotifyBase):
# Define any URL parameters # Define any URL parameters
params = { params = {
'route': str(self.route), 'short_url': str(self.short_url),
} }
# Extend our parameters # Extend our parameters
params.update(self.url_parameters(privacy=privacy, *args, **kwargs)) params.update(self.url_parameters(privacy=privacy, *args, **kwargs))
if self.country: # Payload body extras prefixed with a ':' sign
params['country'] = str(self.country) # 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, schema=self.secure_protocol,
template=self.pprint(self.template, privacy, safe=''),
authkey=self.pprint(self.authkey, privacy, safe=''), authkey=self.pprint(self.authkey, privacy, safe=''),
targets='/'.join( targets='/'.join(
[NotifyMSG91.quote(x, safe='') for x in self.targets]), [NotifyMSG91.quote(x, safe='') for x in self.targets]),
params=NotifyMSG91.urlencode(params)) 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 @staticmethod
def parse_url(url): def parse_url(url):
""" """
@ -349,11 +363,11 @@ class NotifyMSG91(NotifyBase):
# The hostname is our authentication key # The hostname is our authentication key
results['authkey'] = NotifyMSG91.unquote(results['host']) results['authkey'] = NotifyMSG91.unquote(results['host'])
if 'route' in results['qsd'] and len(results['qsd']['route']): # The template id is kept in the user field
results['route'] = results['qsd']['route'] results['template'] = NotifyMSG91.unquote(results['user'])
if 'country' in results['qsd'] and len(results['qsd']['country']): if 'short_url' in results['qsd'] and len(results['qsd']['short_url']):
results['country'] = results['qsd']['country'] results['short_url'] = parse_bool(results['qsd']['short_url'])
# Support the 'to' variable so that we can support targets this way too # Support the 'to' variable so that we can support targets this way too
# The 'to' makes it easier to use yaml configuration # The 'to' makes it easier to use yaml configuration
@ -361,4 +375,10 @@ class NotifyMSG91(NotifyBase):
results['targets'] += \ results['targets'] += \
NotifyMSG91.parse_phone_no(results['qsd']['to']) 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 return results

View file

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# BSD 3-Clause License # BSD 2-Clause License
# #
# Apprise - Push Notification Library. # Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com> # Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation # this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution. # 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" # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE

View file

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# BSD 3-Clause License # BSD 2-Clause License
# #
# Apprise - Push Notification Library. # Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com> # Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation # this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution. # 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" # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # 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))) self.logger.debug('MacOSX CMD: {}'.format(' '.join(cmd)))
# Send our notification # Send our notification
output = subprocess.Popen( output = subprocess.Popen(cmd)
cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
# Wait for process to complete # Wait for process to complete
output.wait() output.wait()

View file

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# BSD 3-Clause License # BSD 2-Clause License
# #
# Apprise - Push Notification Library. # Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com> # Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation # this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution. # 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" # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # 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 # A URL that takes you to the setup/help of the specific protocol
setup_url = 'https://github.com/caronc/apprise/wiki/Notify_mailgun' setup_url = 'https://github.com/caronc/apprise/wiki/Notify_mailgun'
# Support attachments
attachment_support = True
# Default Notify Format # Default Notify Format
notify_format = NotifyFormat.HTML notify_format = NotifyFormat.HTML
@ -152,8 +151,13 @@ class NotifyMailgun(NotifyBase):
'private': True, 'private': True,
'required': True, 'required': True,
}, },
'target_email': {
'name': _('Target Email'),
'type': 'string',
'map_to': 'targets',
},
'targets': { 'targets': {
'name': _('Target Emails'), 'name': _('Targets'),
'type': 'list:string', 'type': 'list:string',
}, },
}) })
@ -366,7 +370,7 @@ class NotifyMailgun(NotifyBase):
# Track our potential files # Track our potential files
files = {} files = {}
if attach: if attach and self.attachment_support:
for idx, attachment in enumerate(attach): for idx, attachment in enumerate(attach):
# Perform some simple error checking # Perform some simple error checking
if not attachment: if not attachment:
@ -627,6 +631,20 @@ class NotifyMailgun(NotifyBase):
safe='') for e in self.targets]), safe='') for e in self.targets]),
params=NotifyMailgun.urlencode(params)) 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 @staticmethod
def parse_url(url): def parse_url(url):
""" """

View file

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# BSD 3-Clause License # BSD 2-Clause License
# #
# Apprise - Push Notification Library. # Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com> # Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation # this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution. # 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" # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
@ -35,6 +31,7 @@ import requests
from copy import deepcopy from copy import deepcopy
from json import dumps, loads from json import dumps, loads
from datetime import datetime from datetime import datetime
from datetime import timezone
from .NotifyBase import NotifyBase from .NotifyBase import NotifyBase
from ..URLBase import PrivacyMode from ..URLBase import PrivacyMode
@ -110,6 +107,10 @@ class NotifyMastodon(NotifyBase):
# A URL that takes you to the setup/help of the specific protocol # A URL that takes you to the setup/help of the specific protocol
setup_url = 'https://github.com/caronc/apprise/wiki/Notify_mastodon' 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 # Allows the user to specify the NotifyImageSize object; this is supported
# through the webhook # through the webhook
image_size = NotifyImageSize.XY_128 image_size = NotifyImageSize.XY_128
@ -150,7 +151,7 @@ class NotifyMastodon(NotifyBase):
request_rate_per_sec = 0 request_rate_per_sec = 0
# For Tracking Purposes # 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 # 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 # This value only get's adjusted if the server sets it that way
@ -378,6 +379,13 @@ class NotifyMastodon(NotifyBase):
params=NotifyMastodon.urlencode(params), 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, def send(self, body, title='', notify_type=NotifyType.INFO, attach=None,
**kwargs): **kwargs):
""" """
@ -406,11 +414,10 @@ class NotifyMastodon(NotifyBase):
else: else:
targets.add(myself) targets.add(myself)
if attach: if attach and self.attachment_support:
# We need to upload our payload first so that we can source it # We need to upload our payload first so that we can source it
# in remaining messages # in remaining messages
for attachment in attach: for attachment in attach:
# Perform some simple error checking # Perform some simple error checking
if not attachment: if not attachment:
# We could not access the attachment # We could not access the attachment
@ -570,7 +577,7 @@ class NotifyMastodon(NotifyBase):
_payload = deepcopy(payload) _payload = deepcopy(payload)
_payload['media_ids'] = media_ids _payload['media_ids'] = media_ids
if no: if no or not body:
# strip text and replace it with the image representation # strip text and replace it with the image representation
_payload['status'] = \ _payload['status'] = \
'{:02d}/{:02d}'.format(no + 1, len(batches)) '{: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 # Mastodon server. One would hope we're on NTP and our clocks are
# the same allowing this to role smoothly: # the same allowing this to role smoothly:
now = datetime.utcnow() now = datetime.now(timezone.utc).replace(tzinfo=None)
if now < self.ratelimit_reset: if now < self.ratelimit_reset:
# We need to throttle for the difference in seconds # We need to throttle for the difference in seconds
# We add 0.5 seconds to the end just to allow a grace # 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 # Capture rate limiting if possible
self.ratelimit_remaining = \ self.ratelimit_remaining = \
int(r.headers.get('X-RateLimit-Remaining')) int(r.headers.get('X-RateLimit-Remaining'))
self.ratelimit_reset = datetime.utcfromtimestamp( self.ratelimit_reset = datetime.fromtimestamp(
int(r.headers.get('X-RateLimit-Limit'))) int(r.headers.get('X-RateLimit-Limit')), timezone.utc
).replace(tzinfo=None)
except (TypeError, ValueError): except (TypeError, ValueError):
# This is returned if we could not retrieve this information # This is returned if we could not retrieve this information

View file

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# BSD 3-Clause License # BSD 2-Clause License
# #
# Apprise - Push Notification Library. # Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com> # Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation # this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution. # 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" # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # 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 _ from ..AppriseLocale import gettext_lazy as _
# Define default path # Define default path
MATRIX_V2_API_PATH = '/_matrix/client/r0'
MATRIX_V1_WEBHOOK_PATH = '/api/v1/matrix/hook' 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 # Extend HTTP Error Messages
MATRIX_HTTP_ERROR_MAP = { 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: class MatrixWebhookMode:
# Webhook Mode is disabled # Webhook Mode is disabled
DISABLED = "off" DISABLED = "off"
@ -128,6 +142,9 @@ class NotifyMatrix(NotifyBase):
# The default secure protocol # The default secure protocol
secure_protocol = 'matrixs' secure_protocol = 'matrixs'
# Support Attachments
attachment_support = True
# A URL that takes you to the setup/help of the specific protocol # A URL that takes you to the setup/help of the specific protocol
setup_url = 'https://github.com/caronc/apprise/wiki/Notify_matrix' setup_url = 'https://github.com/caronc/apprise/wiki/Notify_matrix'
@ -147,6 +164,9 @@ class NotifyMatrix(NotifyBase):
# Throttle a wee-bit to avoid thrashing # Throttle a wee-bit to avoid thrashing
request_rate_per_sec = 0.5 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 # How many retry attempts we'll make in the event the server asks us to
# throttle back. # throttle back.
default_retries = 2 default_retries = 2
@ -175,7 +195,6 @@ class NotifyMatrix(NotifyBase):
'host': { 'host': {
'name': _('Hostname'), 'name': _('Hostname'),
'type': 'string', 'type': 'string',
'required': True,
}, },
'port': { 'port': {
'name': _('Port'), 'name': _('Port'),
@ -194,6 +213,7 @@ class NotifyMatrix(NotifyBase):
}, },
'token': { 'token': {
'name': _('Access Token'), 'name': _('Access Token'),
'private': True,
'map_to': 'password', 'map_to': 'password',
}, },
'target_user': { 'target_user': {
@ -234,6 +254,12 @@ class NotifyMatrix(NotifyBase):
'values': MATRIX_WEBHOOK_MODES, 'values': MATRIX_WEBHOOK_MODES,
'default': MatrixWebhookMode.DISABLED, 'default': MatrixWebhookMode.DISABLED,
}, },
'version': {
'name': _('Matrix API Verion'),
'type': 'choice:string',
'values': MATRIX_VERSIONS,
'default': MatrixVersion.V3,
},
'msgtype': { 'msgtype': {
'name': _('Message Type'), 'name': _('Message Type'),
'type': 'choice:string', '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): include_image=False, **kwargs):
""" """
Initialize Matrix Object Initialize Matrix Object
@ -282,6 +308,14 @@ class NotifyMatrix(NotifyBase):
self.logger.warning(msg) self.logger.warning(msg)
raise TypeError(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 # Setup our message type
self.msgtype = self.template_args['msgtype']['default'] \ self.msgtype = self.template_args['msgtype']['default'] \
if not isinstance(msgtype, str) else msgtype.lower() if not isinstance(msgtype, str) else msgtype.lower()
@ -521,7 +555,8 @@ class NotifyMatrix(NotifyBase):
return payload return payload
def _send_server_notification(self, body, title='', 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) Perform Direct Matrix Server Notification (no webhook)
""" """
@ -548,6 +583,13 @@ class NotifyMatrix(NotifyBase):
# Initiaize our error tracking # Initiaize our error tracking
has_error = False 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: while len(rooms) > 0:
# Get our room # Get our room
@ -568,19 +610,43 @@ class NotifyMatrix(NotifyBase):
image_url = None if not self.include_image else \ image_url = None if not self.include_image else \
self.image_url(notify_type) self.image_url(notify_type)
# 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))
if self.version == MatrixVersion.V2:
#
# Attachments don't work beyond V2 at this time
#
if image_url: if image_url:
# Define our payload # Define our payload
image_payload = { image_payload = {
'msgtype': 'm.image', 'msgtype': 'm.image',
'url': image_url, 'url': image_url,
'body': '{}'.format(notify_type if not title else title), 'body': '{}'.format(
notify_type if not title else title),
} }
# Build our path
path = '/rooms/{}/send/m.room.message'.format(
NotifyMatrix.quote(room_id))
# Post our content # Post our content
postokay, response = self._fetch(path, payload=image_payload) 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: if not postokay:
# Mark our failure # Mark our failure
has_error = True has_error = True
@ -615,12 +681,10 @@ class NotifyMatrix(NotifyBase):
) )
}) })
# Build our path
path = '/rooms/{}/send/m.room.message'.format(
NotifyMatrix.quote(room_id))
# Post our content # 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: if not postokay:
# Notify our user # Notify our user
self.logger.warning( self.logger.warning(
@ -632,6 +696,62 @@ class NotifyMatrix(NotifyBase):
return not has_error 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): def _register(self):
""" """
Register with the service if possible. Register with the service if possible.
@ -695,7 +815,18 @@ class NotifyMatrix(NotifyBase):
'user/pass combo is missing.') 'user/pass combo is missing.')
return False return False
# Prepare our Registration Payload # 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 = { payload = {
'type': 'm.login.password', 'type': 'm.login.password',
'user': self.user, 'user': self.user,
@ -970,7 +1101,8 @@ class NotifyMatrix(NotifyBase):
return None 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 Wrapper to request.post() to manage it's response better and make
the send() function cleaner and easier to maintain. the send() function cleaner and easier to maintain.
@ -983,6 +1115,7 @@ class NotifyMatrix(NotifyBase):
headers = { headers = {
'User-Agent': self.app_id, 'User-Agent': self.app_id,
'Content-Type': 'application/json', 'Content-Type': 'application/json',
'Accept': 'application/json',
} }
if self.access_token is not None: if self.access_token is not None:
@ -991,19 +1124,39 @@ class NotifyMatrix(NotifyBase):
default_port = 443 if self.secure else 80 default_port = 443 if self.secure else 80
url = \ url = \
'{schema}://{hostname}:{port}{matrix_api}{path}'.format( '{schema}://{hostname}{port}'.format(
schema='https' if self.secure else 'http', schema='https' if self.secure else 'http',
hostname=self.host, hostname=self.host,
port='' if self.port is None port='' if self.port is None
or self.port == default_port else self.port, or self.port == default_port else f':{self.port}')
matrix_api=MATRIX_V2_API_PATH,
path=path) 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 # Our response object
response = {} response = {}
# fetch function # 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 # Define how many attempts we'll make if we get caught in a throttle
# event # event
@ -1024,13 +1177,16 @@ class NotifyMatrix(NotifyBase):
try: try:
r = fn( r = fn(
url, url,
data=dumps(payload), data=dumps(payload) if not attachment else payload,
params=params, params=params,
headers=headers, headers=headers,
verify=self.verify_certificate, verify=self.verify_certificate,
timeout=self.request_timeout, timeout=self.request_timeout,
) )
self.logger.debug(
'Matrix Response: code=%d, %s' % (
r.status_code, str(r.content)))
response = loads(r.content) response = loads(r.content)
if r.status_code == 429: if r.status_code == 429:
@ -1094,6 +1250,13 @@ class NotifyMatrix(NotifyBase):
# Return; we're done # Return; we're done
return (False, response) 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) return (True, response)
# If we get here, we ran out of retries # If we get here, we ran out of retries
@ -1160,6 +1323,7 @@ class NotifyMatrix(NotifyBase):
params = { params = {
'image': 'yes' if self.include_image else 'no', 'image': 'yes' if self.include_image else 'no',
'mode': self.mode, 'mode': self.mode,
'version': self.version,
'msgtype': self.msgtype, 'msgtype': self.msgtype,
} }
@ -1196,6 +1360,13 @@ class NotifyMatrix(NotifyBase):
params=NotifyMatrix.urlencode(params), 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 @staticmethod
def parse_url(url): def parse_url(url):
""" """
@ -1250,6 +1421,14 @@ class NotifyMatrix(NotifyBase):
if 'token' in results['qsd'] and len(results['qsd']['token']): if 'token' in results['qsd'] and len(results['qsd']['token']):
results['password'] = NotifyMatrix.unquote(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 return results
@staticmethod @staticmethod
@ -1259,7 +1438,7 @@ class NotifyMatrix(NotifyBase):
""" """
result = re.match( 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<webhook_token>[A-Z0-9_-]+)/?'
r'(?P<params>\?.+)?$', url, re.I) r'(?P<params>\?.+)?$', url, re.I)

View file

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# BSD 3-Clause License # BSD 2-Clause License
# #
# Apprise - Push Notification Library. # Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com> # Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation # this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution. # 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" # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
@ -91,11 +87,11 @@ class NotifyMattermost(NotifyBase):
# Define object templates # Define object templates
templates = ( templates = (
'{schema}://{host}/{token}', '{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}/{token}',
'{schema}://{botname}@{host}:{port}/{token}', '{schema}://{botname}@{host}:{port}/{token}',
'{schema}://{host}/{fullpath}/{token}',
'{schema}://{host}/{fullpath}{token}:{port}',
'{schema}://{botname}@{host}/{fullpath}/{token}', '{schema}://{botname}@{host}/{fullpath}/{token}',
'{schema}://{botname}@{host}:{port}/{fullpath}/{token}', '{schema}://{botname}@{host}:{port}/{fullpath}/{token}',
) )

View file

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# BSD 3-Clause License # BSD 2-Clause License
# #
# Apprise - Push Notification Library. # Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com> # Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation # this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution. # 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" # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # 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]), [NotifyMessageBird.quote(x, safe='') for x in self.targets]),
params=NotifyMessageBird.urlencode(params)) 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 @staticmethod
def parse_url(url): def parse_url(url):
""" """

View file

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# BSD 3-Clause License # BSD 2-Clause License
# #
# Apprise - Push Notification Library. # Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com> # Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation # this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution. # 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" # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
@ -29,6 +25,7 @@
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) # 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 # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE. # POSSIBILITY OF SUCH DAMAGE.
# 1. visit https://misskey-hub.net/ and see what it's all about if you want. # 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: # Choose a service you want to create an account on from here:
# https://misskey-hub.net/en/instances.html # https://misskey-hub.net/en/instances.html

View file

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# BSD 3-Clause License # BSD 2-Clause License
# #
# Apprise - Push Notification Library. # Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com> # Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation # this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution. # 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" # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
@ -67,6 +63,8 @@ class NotifyNextcloud(NotifyBase):
# Define object templates # Define object templates
templates = ( templates = (
'{schema}://{host}/{targets}',
'{schema}://{host}:{port}/{targets}',
'{schema}://{user}:{password}@{host}/{targets}', '{schema}://{user}:{password}@{host}/{targets}',
'{schema}://{user}:{password}@{host}:{port}/{targets}', '{schema}://{user}:{password}@{host}:{port}/{targets}',
) )
@ -116,6 +114,10 @@ class NotifyNextcloud(NotifyBase):
'min': 1, 'min': 1,
'default': 21, 'default': 21,
}, },
'url_prefix': {
'name': _('URL Prefix'),
'type': 'string',
},
'to': { 'to': {
'alias_of': 'targets', '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 Initialize Nextcloud Object
""" """
super().__init__(**kwargs) super().__init__(**kwargs)
# Store our targets
self.targets = parse_list(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'] self.version = self.template_args['version']['default']
if version is not None: if version is not None:
@ -155,6 +155,10 @@ class NotifyNextcloud(NotifyBase):
self.logger.warning(msg) self.logger.warning(msg)
raise TypeError(msg) raise TypeError(msg)
# Support URL Prefix
self.url_prefix = '' if not url_prefix \
else url_prefix.strip('/')
self.headers = {} self.headers = {}
if headers: if headers:
# Store our extra headers # Store our extra headers
@ -167,6 +171,11 @@ class NotifyNextcloud(NotifyBase):
Perform Nextcloud Notification 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 # Prepare our Header
headers = { headers = {
'User-Agent': self.app_id, 'User-Agent': self.app_id,
@ -198,11 +207,11 @@ class NotifyNextcloud(NotifyBase):
auth = (self.user, self.password) auth = (self.user, self.password)
# Nextcloud URL based on version used # 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/' \ 'apps/admin_notifications/' \
'api/v1/notifications/{target}' \ 'api/v1/notifications/{target}' \
if self.version < 21 else \ if self.version < 21 else \
'{schema}://{host}/ocs/v2.php/'\ '{schema}://{host}/{url_prefix}/ocs/v2.php/'\
'apps/notifications/'\ 'apps/notifications/'\
'api/v2/admin_notifications/{target}' 'api/v2/admin_notifications/{target}'
@ -210,6 +219,7 @@ class NotifyNextcloud(NotifyBase):
schema='https' if self.secure else 'http', schema='https' if self.secure else 'http',
host=self.host if not isinstance(self.port, int) host=self.host if not isinstance(self.port, int)
else '{}:{}'.format(self.host, self.port), else '{}:{}'.format(self.host, self.port),
url_prefix=self.url_prefix,
target=target, target=target,
) )
@ -279,6 +289,9 @@ class NotifyNextcloud(NotifyBase):
# Set our version # Set our version
params['version'] = str(self.version) params['version'] = str(self.version)
if self.url_prefix:
params['url_prefix'] = self.url_prefix
# Extend our parameters # Extend our parameters
params.update(self.url_parameters(privacy=privacy, *args, **kwargs)) params.update(self.url_parameters(privacy=privacy, *args, **kwargs))
@ -312,6 +325,13 @@ class NotifyNextcloud(NotifyBase):
params=NotifyNextcloud.urlencode(params), 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 @staticmethod
def parse_url(url): def parse_url(url):
""" """
@ -339,6 +359,12 @@ class NotifyNextcloud(NotifyBase):
results['version'] = \ results['version'] = \
NotifyNextcloud.unquote(results['qsd']['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 # 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 # to to our returned result set and tidy entries by unquoting them
results['headers'] = { results['headers'] = {

View file

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# BSD 3-Clause License # BSD 2-Clause License
# #
# Apprise - Push Notification Library. # Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com> # Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation # this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution. # 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" # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
@ -96,6 +92,11 @@ class NotifyNextcloudTalk(NotifyBase):
'private': True, 'private': True,
'required': True, 'required': True,
}, },
'target_room_id': {
'name': _('Room ID'),
'type': 'string',
'map_to': 'targets',
},
'targets': { 'targets': {
'name': _('Targets'), 'name': _('Targets'),
'type': 'list:string', '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 # Define any kwargs we're using
template_kwargs = { template_kwargs = {
'headers': { '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 Initialize Nextcloud Talk Object
""" """
@ -122,11 +131,12 @@ class NotifyNextcloudTalk(NotifyBase):
self.logger.warning(msg) self.logger.warning(msg)
raise TypeError(msg) raise TypeError(msg)
# Store our targets
self.targets = parse_list(targets) self.targets = parse_list(targets)
if len(self.targets) == 0:
msg = 'At least one Nextcloud Talk Room ID must be specified.' # Support URL Prefix
self.logger.warning(msg) self.url_prefix = '' if not url_prefix \
raise TypeError(msg) else url_prefix.strip('/')
self.headers = {} self.headers = {}
if headers: if headers:
@ -140,6 +150,12 @@ class NotifyNextcloudTalk(NotifyBase):
Perform Nextcloud Talk Notification 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 # Prepare our Header
headers = { headers = {
'User-Agent': self.app_id, 'User-Agent': self.app_id,
@ -171,13 +187,14 @@ class NotifyNextcloudTalk(NotifyBase):
} }
# Nextcloud Talk URL # Nextcloud Talk URL
notify_url = '{schema}://{host}'\ notify_url = '{schema}://{host}/{url_prefix}'\
'/ocs/v2.php/apps/spreed/api/v1/chat/{target}' '/ocs/v2.php/apps/spreed/api/v1/chat/{target}'
notify_url = notify_url.format( notify_url = notify_url.format(
schema='https' if self.secure else 'http', schema='https' if self.secure else 'http',
host=self.host if not isinstance(self.port, int) host=self.host if not isinstance(self.port, int)
else '{}:{}'.format(self.host, self.port), else '{}:{}'.format(self.host, self.port),
url_prefix=self.url_prefix,
target=target, target=target,
) )
@ -200,7 +217,8 @@ class NotifyNextcloudTalk(NotifyBase):
verify=self.verify_certificate, verify=self.verify_certificate,
timeout=self.request_timeout, 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 # We had a problem
status_str = \ status_str = \
NotifyNextcloudTalk.http_response_code_lookup( NotifyNextcloudTalk.http_response_code_lookup(
@ -240,6 +258,14 @@ class NotifyNextcloudTalk(NotifyBase):
Returns the URL built dynamically based on specified arguments. 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 # Determine Authentication
auth = '{user}:{password}@'.format( auth = '{user}:{password}@'.format(
user=NotifyNextcloudTalk.quote(self.user, safe=''), user=NotifyNextcloudTalk.quote(self.user, safe=''),
@ -249,7 +275,7 @@ class NotifyNextcloudTalk(NotifyBase):
default_port = 443 if self.secure else 80 default_port = 443 if self.secure else 80
return '{schema}://{auth}{hostname}{port}/{targets}' \ return '{schema}://{auth}{hostname}{port}/{targets}?{params}' \
.format( .format(
schema=self.secure_protocol schema=self.secure_protocol
if self.secure else self.protocol, if self.secure else self.protocol,
@ -261,8 +287,16 @@ class NotifyNextcloudTalk(NotifyBase):
else ':{}'.format(self.port), else ':{}'.format(self.port),
targets='/'.join([NotifyNextcloudTalk.quote(x) targets='/'.join([NotifyNextcloudTalk.quote(x)
for x in self.targets]), 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 @staticmethod
def parse_url(url): def parse_url(url):
""" """
@ -280,6 +314,12 @@ class NotifyNextcloudTalk(NotifyBase):
results['targets'] = \ results['targets'] = \
NotifyNextcloudTalk.split_path(results['fullpath']) 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 # 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 # to to our returned result set and tidy entries by unquoting them
results['headers'] = { results['headers'] = {

View file

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# BSD 3-Clause License # BSD 2-Clause License
# #
# Apprise - Push Notification Library. # Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com> # Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation # this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution. # 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" # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
@ -112,12 +108,12 @@ class NotifyNotica(NotifyBase):
'{schema}://{user}:{password}@{host}:{port}/{token}', '{schema}://{user}:{password}@{host}:{port}/{token}',
# Self-hosted notica servers (with custom path) # Self-hosted notica servers (with custom path)
'{schema}://{host}{path}{token}', '{schema}://{host}{path}/{token}',
'{schema}://{host}:{port}{path}{token}', '{schema}://{host}:{port}/{path}/{token}',
'{schema}://{user}@{host}{path}{token}', '{schema}://{user}@{host}/{path}/{token}',
'{schema}://{user}@{host}:{port}{path}{token}', '{schema}://{user}@{host}:{port}{path}/{token}',
'{schema}://{user}:{password}@{host}{path}{token}', '{schema}://{user}:{password}@{host}{path}/{token}',
'{schema}://{user}:{password}@{host}:{port}{path}{token}', '{schema}://{user}:{password}@{host}:{port}/{path}/{token}',
) )
# Define our template tokens # Define our template tokens

View 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

View file

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# BSD 3-Clause License # BSD 2-Clause License
# #
# Apprise - Push Notification Library. # Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com> # Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation # this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution. # 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" # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE

View file

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# BSD 3-Clause License # BSD 2-Clause License
# #
# Apprise - Push Notification Library. # Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com> # Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation # this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution. # 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" # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # 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 # Default upstream/cloud host if none is defined
cloud_notify_url = 'https://ntfy.sh' cloud_notify_url = 'https://ntfy.sh'
# Support attachments
attachment_support = True
# Allows the user to specify the NotifyImageSize object # Allows the user to specify the NotifyImageSize object
image_size = NotifyImageSize.XY_256 image_size = NotifyImageSize.XY_256
@ -405,14 +404,14 @@ class NotifyNtfy(NotifyBase):
# Retrieve our topic # Retrieve our topic
topic = topics.pop() topic = topics.pop()
if attach: if attach and self.attachment_support:
# We need to upload our payload first so that we can source it # We need to upload our payload first so that we can source it
# in remaining messages # in remaining messages
for no, attachment in enumerate(attach): for no, attachment in enumerate(attach):
# First message only includes the text # First message only includes the text (if defined)
_body = body if not no else None _body = body if not no and body else None
_title = title if not no else None _title = title if not no and title else None
# Perform some simple error checking # Perform some simple error checking
if not attachment: if not attachment:
@ -453,10 +452,6 @@ class NotifyNtfy(NotifyBase):
'User-Agent': self.app_id, '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 # See https://ntfy.sh/docs/publish/#publish-as-json
data = {} data = {}
@ -494,11 +489,23 @@ class NotifyNtfy(NotifyBase):
data['topic'] = topic data['topic'] = topic
virt_payload = data virt_payload = data
if self.attach:
virt_payload['attach'] = self.attach
if self.filename:
virt_payload['filename'] = self.filename
else: else:
# Point our payload to our parameters # Point our payload to our parameters
virt_payload = params virt_payload = params
notify_url += '/{topic}'.format(topic=topic) 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: if image_url:
headers['X-Icon'] = image_url headers['X-Icon'] = image_url
@ -523,18 +530,6 @@ class NotifyNtfy(NotifyBase):
if self.__tags: if self.__tags:
headers['X-Tags'] = ",".join(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)' % ( self.logger.debug('ntfy POST URL: %s (cert_verify=%r)' % (
notify_url, self.verify_certificate, notify_url, self.verify_certificate,
)) ))
@ -547,13 +542,15 @@ class NotifyNtfy(NotifyBase):
# Default response type # Default response type
response = None response = None
if not attach:
data = dumps(data)
try: try:
r = requests.post( r = requests.post(
notify_url, notify_url,
params=params if params else None, params=params if params else None,
data=dumps(data) if data else None, data=data,
headers=headers, headers=headers,
files=files,
auth=auth, auth=auth,
verify=self.verify_certificate, verify=self.verify_certificate,
timeout=self.request_timeout, timeout=self.request_timeout,
@ -608,7 +605,6 @@ class NotifyNtfy(NotifyBase):
notify_url) + 'notification.' notify_url) + 'notification.'
) )
self.logger.debug('Socket Exception: %s' % str(e)) self.logger.debug('Socket Exception: %s' % str(e))
return False, response
except (OSError, IOError) as e: except (OSError, IOError) as e:
self.logger.warning( self.logger.warning(
@ -616,13 +612,8 @@ class NotifyNtfy(NotifyBase):
attach.name if isinstance(attach, AttachBase) attach.name if isinstance(attach, AttachBase)
else virt_payload)) else virt_payload))
self.logger.debug('I/O Exception: %s' % str(e)) self.logger.debug('I/O Exception: %s' % str(e))
return False, response
finally: return False, response
# Close our file (if it's open) stored in the second element
# of our files tuple (index 1)
if files:
files['file'][1].close()
def url(self, privacy=False, *args, **kwargs): def url(self, privacy=False, *args, **kwargs):
""" """
@ -698,6 +689,12 @@ class NotifyNtfy(NotifyBase):
params=NotifyNtfy.urlencode(params) params=NotifyNtfy.urlencode(params)
) )
def __len__(self):
"""
Returns the number of targets associated with this notification
"""
return len(self.topics)
@staticmethod @staticmethod
def parse_url(url): def parse_url(url):
""" """

View file

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# BSD 3-Clause License # BSD 2-Clause License
# #
# Apprise - Push Notification Library. # Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com> # Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation # this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution. # 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" # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
@ -148,8 +144,13 @@ class NotifyOffice365(NotifyBase):
'private': True, 'private': True,
'required': True, 'required': True,
}, },
'target_email': {
'name': _('Target Email'),
'type': 'string',
'map_to': 'targets',
},
'targets': { 'targets': {
'name': _('Target Emails'), 'name': _('Targets'),
'type': 'list:string', 'type': 'list:string',
}, },
}) })
@ -596,6 +597,12 @@ class NotifyOffice365(NotifyBase):
safe='') for e in self.targets]), safe='') for e in self.targets]),
params=NotifyOffice365.urlencode(params)) params=NotifyOffice365.urlencode(params))
def __len__(self):
"""
Returns the number of targets associated with this notification
"""
return len(self.targets)
@staticmethod @staticmethod
def parse_url(url): def parse_url(url):
""" """

View file

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# BSD 3-Clause License # BSD 2-Clause License
# #
# Apprise - Push Notification Library. # Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com> # Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation # this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution. # 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" # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # 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 _ from ..AppriseLocale import gettext_lazy as _
class OneSignalCategory(NotifyBase): class OneSignalCategory:
""" """
We define the different category types that we can notify via OneSignal We define the different category types that we can notify via OneSignal
""" """
@ -92,7 +88,7 @@ class NotifyOneSignal(NotifyBase):
image_size = NotifyImageSize.XY_72 image_size = NotifyImageSize.XY_72
# The maximum allowable batch sizes per message # The maximum allowable batch sizes per message
maximum_batch_size = 2000 default_batch_size = 2000
# Define object templates # Define object templates
templates = ( templates = (
@ -121,7 +117,7 @@ class NotifyOneSignal(NotifyBase):
'private': True, 'private': True,
'required': True, 'required': True,
}, },
'target_device': { 'target_player': {
'name': _('Target Player ID'), 'name': _('Target Player ID'),
'type': 'string', 'type': 'string',
'map_to': 'targets', 'map_to': 'targets',
@ -146,6 +142,7 @@ class NotifyOneSignal(NotifyBase):
'targets': { 'targets': {
'name': _('Targets'), 'name': _('Targets'),
'type': 'list:string', 'type': 'list:string',
'required': True,
}, },
}) })
@ -204,7 +201,7 @@ class NotifyOneSignal(NotifyBase):
raise TypeError(msg) raise TypeError(msg)
# Prepare Batch Mode Flag # 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 # Place a thumbnail image inline with the message body
self.include_image = include_image self.include_image = include_image
@ -432,6 +429,26 @@ class NotifyOneSignal(NotifyBase):
params=NotifyOneSignal.urlencode(params), 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 @staticmethod
def parse_url(url): def parse_url(url):
""" """

View file

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# BSD 3-Clause License # BSD 2-Clause License
# #
# Apprise - Push Notification Library. # Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com> # Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation # this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution. # 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" # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
@ -172,7 +168,7 @@ class NotifyOpsgenie(NotifyBase):
opsgenie_default_region = OpsgenieRegion.US opsgenie_default_region = OpsgenieRegion.US
# The maximum allowable targets within a notification # The maximum allowable targets within a notification
maximum_batch_size = 50 default_batch_size = 50
# Define object templates # Define object templates
templates = ( templates = (
@ -308,7 +304,7 @@ class NotifyOpsgenie(NotifyBase):
self.details.update(details) self.details.update(details)
# Prepare Batch Mode Flag # 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) # Assign our tags (if defined)
self.__tags = parse_list(tags) self.__tags = parse_list(tags)
@ -536,6 +532,20 @@ class NotifyOpsgenie(NotifyBase):
for x in self.targets]), for x in self.targets]),
params=NotifyOpsgenie.urlencode(params)) 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 @staticmethod
def parse_url(url): def parse_url(url):
""" """

View file

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# BSD 3-Clause License # BSD 2-Clause License
# #
# Apprise - Push Notification Library. # Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com> # Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation # this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution. # 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" # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
@ -142,7 +138,7 @@ class NotifyPagerDuty(NotifyBase):
}, },
# Optional but triggers V2 API # Optional but triggers V2 API
'integrationkey': { 'integrationkey': {
'name': _('Routing Key'), 'name': _('Integration Key'),
'type': 'string', 'type': 'string',
'private': True, 'private': True,
'required': True 'required': True

View file

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# BSD 3-Clause License # BSD 2-Clause License
# #
# Apprise - Push Notification Library. # Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com> # Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation # this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution. # 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" # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE

View file

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# BSD 3-Clause License # BSD 2-Clause License
# #
# Apprise - Push Notification Library. # Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com> # Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation # this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution. # 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" # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # 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 # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE. # POSSIBILITY OF SUCH DAMAGE.
# Official API reference: https://developer.gitter.im/docs/user-resource
import re import re
import requests import requests
from json import dumps from json import dumps

View file

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# BSD 3-Clause License # BSD 2-Clause License
# #
# Apprise - Push Notification Library. # Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com> # Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation # this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution. # 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" # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
@ -93,6 +89,7 @@ class NotifyPopcornNotify(NotifyBase):
'targets': { 'targets': {
'name': _('Targets'), 'name': _('Targets'),
'type': 'list:string', 'type': 'list:string',
'required': True,
} }
}) })
@ -265,6 +262,21 @@ class NotifyPopcornNotify(NotifyBase):
[NotifyPopcornNotify.quote(x, safe='') for x in self.targets]), [NotifyPopcornNotify.quote(x, safe='') for x in self.targets]),
params=NotifyPopcornNotify.urlencode(params)) 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 @staticmethod
def parse_url(url): def parse_url(url):
""" """

View file

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# BSD 3-Clause License # BSD 2-Clause License
# #
# Apprise - Push Notification Library. # Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com> # Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation # this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution. # 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" # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE

View file

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# BSD 3-Clause License # BSD 2-Clause License
# #
# Apprise - Push Notification Library. # Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com> # Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation # this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution. # 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" # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # 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 # PushBullet uses the http protocol with JSON requests
notify_url = 'https://api.pushbullet.com/v2/{}' notify_url = 'https://api.pushbullet.com/v2/{}'
# Support attachments
attachment_support = True
# Define object templates # Define object templates
templates = ( templates = (
'{schema}://{accesstoken}', '{schema}://{accesstoken}',
@ -150,7 +149,7 @@ class NotifyPushBullet(NotifyBase):
# Build a list of our attachments # Build a list of our attachments
attachments = [] attachments = []
if attach: if attach and self.attachment_support:
# We need to upload our payload first so that we can source it # We need to upload our payload first so that we can source it
# in remaining messages # in remaining messages
for attachment in attach: for attachment in attach:
@ -261,6 +260,7 @@ class NotifyPushBullet(NotifyBase):
"PushBullet recipient {} parsed as a device" "PushBullet recipient {} parsed as a device"
.format(recipient)) .format(recipient))
if body:
okay, response = self._send( okay, response = self._send(
self.notify_url.format('pushes'), payload) self.notify_url.format('pushes'), payload)
if not okay: if not okay:
@ -406,6 +406,12 @@ class NotifyPushBullet(NotifyBase):
targets=targets, targets=targets,
params=NotifyPushBullet.urlencode(params)) params=NotifyPushBullet.urlencode(params))
def __len__(self):
"""
Returns the number of targets associated with this notification
"""
return len(self.targets)
@staticmethod @staticmethod
def parse_url(url): def parse_url(url):
""" """

View 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

View 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

View file

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# BSD 3-Clause License # BSD 2-Clause License
# #
# Apprise - Push Notification Library. # Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com> # Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation # this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution. # 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" # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
@ -336,6 +332,9 @@ class NotifyPushSafer(NotifyBase):
# The default secure protocol # The default secure protocol
secure_protocol = 'psafers' secure_protocol = 'psafers'
# Support attachments
attachment_support = True
# Number of requests to a allow per second # Number of requests to a allow per second
request_rate_per_sec = 1.2 request_rate_per_sec = 1.2
@ -546,7 +545,7 @@ class NotifyPushSafer(NotifyBase):
# Initialize our list of attachments # Initialize our list of attachments
attachments = [] attachments = []
if attach: if attach and self.attachment_support:
# We need to upload our payload first so that we can source it # We need to upload our payload first so that we can source it
# in remaining messages # in remaining messages
for attachment in attach: for attachment in attach:
@ -794,6 +793,12 @@ class NotifyPushSafer(NotifyBase):
targets=targets, targets=targets,
params=NotifyPushSafer.urlencode(params)) params=NotifyPushSafer.urlencode(params))
def __len__(self):
"""
Returns the number of targets associated with this notification
"""
return len(self.targets)
@staticmethod @staticmethod
def parse_url(url): def parse_url(url):
""" """

View file

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# BSD 3-Clause License # BSD 2-Clause License
# #
# Apprise - Push Notification Library. # Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com> # Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation # this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution. # 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" # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
@ -329,6 +325,13 @@ class NotifyPushed(NotifyBase):
)]), )]),
params=NotifyPushed.urlencode(params)) 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 @staticmethod
def parse_url(url): def parse_url(url):
""" """

View file

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# BSD 3-Clause License # BSD 2-Clause License
# #
# Apprise - Push Notification Library. # Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com> # Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation # this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution. # 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" # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE

View file

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# BSD 3-Clause License # BSD 2-Clause License
# #
# Apprise - Push Notification Library. # Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com> # Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation # this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution. # 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" # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
@ -32,6 +28,7 @@
import re import re
import requests import requests
from itertools import chain
from .NotifyBase import NotifyBase from .NotifyBase import NotifyBase
from ..common import NotifyType from ..common import NotifyType
@ -46,7 +43,7 @@ from ..attachment.AttachBase import AttachBase
PUSHOVER_SEND_TO_ALL = 'ALL_DEVICES' PUSHOVER_SEND_TO_ALL = 'ALL_DEVICES'
# Used to detect a Device # 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 # Priorities
@ -164,6 +161,9 @@ class NotifyPushover(NotifyBase):
# Pushover uses the http protocol with JSON requests # Pushover uses the http protocol with JSON requests
notify_url = 'https://api.pushover.net/1/messages.json' notify_url = 'https://api.pushover.net/1/messages.json'
# Support attachments
attachment_support = True
# The maximum allowable characters allowed in the body per message # The maximum allowable characters allowed in the body per message
body_maxlen = 1024 body_maxlen = 1024
@ -201,7 +201,7 @@ class NotifyPushover(NotifyBase):
'target_device': { 'target_device': {
'name': _('Target Device'), 'name': _('Target Device'),
'type': 'string', 'type': 'string',
'regex': (r'^[a-z0-9_]{1,25}$', 'i'), 'regex': (r'^[a-z0-9_-]{1,25}$', 'i'),
'map_to': 'targets', 'map_to': 'targets',
}, },
'targets': { 'targets': {
@ -276,10 +276,30 @@ class NotifyPushover(NotifyBase):
self.logger.warning(msg) self.logger.warning(msg)
raise TypeError(msg) raise TypeError(msg)
self.targets = parse_list(targets) # Track our valid devices
if len(self.targets) == 0: targets = parse_list(targets)
# Track any invalid entries
self.invalid_targets = list()
if len(targets) == 0:
self.targets = (PUSHOVER_SEND_TO_ALL, ) 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 # Setup supplemental url
self.supplemental_url = supplemental_url self.supplemental_url = supplemental_url
self.supplemental_url_title = supplemental_url_title self.supplemental_url_title = supplemental_url_title
@ -288,9 +308,8 @@ class NotifyPushover(NotifyBase):
self.sound = NotifyPushover.default_pushover_sound \ self.sound = NotifyPushover.default_pushover_sound \
if not isinstance(sound, str) else sound.lower() if not isinstance(sound, str) else sound.lower()
if self.sound and self.sound not in PUSHOVER_SOUNDS: if self.sound and self.sound not in PUSHOVER_SOUNDS:
msg = 'The sound specified ({}) is invalid.'.format(sound) msg = 'Using custom sound specified ({}). '.format(sound)
self.logger.warning(msg) self.logger.debug(msg)
raise TypeError(msg)
# The Priority of the message # The Priority of the message
self.priority = int( self.priority = int(
@ -338,22 +357,11 @@ class NotifyPushover(NotifyBase):
Perform Pushover Notification Perform Pushover Notification
""" """
# error tracking (used for function return) if not self.targets:
has_error = False # There were no services to notify
# Create a copy of the devices list
devices = list(self.targets)
while len(devices):
device = devices.pop(0)
if VALIDATE_DEVICE.match(device) is None:
self.logger.warning( self.logger.warning(
'The device specified (%s) is invalid.' % device, 'There were no Pushover targets to notify.')
) return False
# Mark our failure
has_error = True
continue
# prepare JSON Object # prepare JSON Object
payload = { payload = {
@ -362,18 +370,20 @@ class NotifyPushover(NotifyBase):
'priority': str(self.priority), 'priority': str(self.priority),
'title': title if title else self.app_desc, 'title': title if title else self.app_desc,
'message': body, 'message': body,
'device': device, 'device': ','.join(self.targets),
'sound': self.sound, 'sound': self.sound,
} }
if self.supplemental_url: if self.supplemental_url:
payload['url'] = self.supplemental_url payload['url'] = self.supplemental_url
if self.supplemental_url_title: if self.supplemental_url_title:
payload['url_title'] = self.supplemental_url_title payload['url_title'] = self.supplemental_url_title
if self.notify_format == NotifyFormat.HTML: if self.notify_format == NotifyFormat.HTML:
# https://pushover.net/api#html # https://pushover.net/api#html
payload['html'] = 1 payload['html'] = 1
elif self.notify_format == NotifyFormat.MARKDOWN: elif self.notify_format == NotifyFormat.MARKDOWN:
payload['message'] = convert_between( payload['message'] = convert_between(
NotifyFormat.MARKDOWN, NotifyFormat.HTML, body) NotifyFormat.MARKDOWN, NotifyFormat.HTML, body)
@ -382,33 +392,32 @@ class NotifyPushover(NotifyBase):
if self.priority == PushoverPriority.EMERGENCY: if self.priority == PushoverPriority.EMERGENCY:
payload.update({'retry': self.retry, 'expire': self.expire}) payload.update({'retry': self.retry, 'expire': self.expire})
if attach: if attach and self.attachment_support:
# Create a copy of our payload # Create a copy of our payload
_payload = payload.copy() _payload = payload.copy()
# Send with attachments # Send with attachments
for attachment in attach: for no, attachment in enumerate(attach):
# Simple send if no or not body:
# To handle multiple attachments, clean up our message
_payload['message'] = attachment.name
if not self._send(_payload, attachment): if not self._send(_payload, attachment):
# Mark our failure # Mark our failure
has_error = True return False
# clean exit from our attachment loop
break # Clear our title if previously set
_payload['title'] = ''
# To handle multiple attachments, clean up our message
_payload['title'] = '...'
_payload['message'] = attachment.name
# No need to alarm for each consecutive attachment uploaded # No need to alarm for each consecutive attachment uploaded
# afterwards # afterwards
_payload['sound'] = PushoverSound.NONE _payload['sound'] = PushoverSound.NONE
else: else:
# Simple send # Simple send
if not self._send(payload): return self._send(payload)
# Mark our failure
has_error = True
return not has_error return True
def _send(self, payload, attach=None): def _send(self, payload, attach=None):
""" """
@ -562,8 +571,9 @@ class NotifyPushover(NotifyBase):
params.update(self.url_parameters(privacy=privacy, *args, **kwargs)) params.update(self.url_parameters(privacy=privacy, *args, **kwargs))
# Escape our devices # Escape our devices
devices = '/'.join([NotifyPushover.quote(x, safe='') devices = '/'.join(
for x in self.targets]) [NotifyPushover.quote(x, safe='')
for x in chain(self.targets, self.invalid_targets)])
if devices == PUSHOVER_SEND_TO_ALL: if devices == PUSHOVER_SEND_TO_ALL:
# keyword is reserved for internal usage only; it's safe to remove # keyword is reserved for internal usage only; it's safe to remove

View 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

View 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

View file

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# BSD 3-Clause License # BSD 2-Clause License
# #
# Apprise - Push Notification Library. # Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com> # Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation # this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution. # 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" # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # 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 # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE. # POSSIBILITY OF SUCH DAMAGE.
#
# 1. Visit https://www.reddit.com/prefs/apps and scroll to the bottom # 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...' # 2. Click on the button that reads 'are you a developer? create an app...'
# 3. Set the mode to `script`, # 3. Set the mode to `script`,
@ -56,6 +51,7 @@ import requests
from json import loads from json import loads
from datetime import timedelta from datetime import timedelta
from datetime import datetime from datetime import datetime
from datetime import timezone
from .NotifyBase import NotifyBase from .NotifyBase import NotifyBase
from ..URLBase import PrivacyMode from ..URLBase import PrivacyMode
@ -133,12 +129,6 @@ class NotifyReddit(NotifyBase):
# still allow to make. # still allow to make.
request_rate_per_sec = 0 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: # Taken right from google.auth.helpers:
clock_skew = timedelta(seconds=10) clock_skew = timedelta(seconds=10)
@ -185,6 +175,7 @@ class NotifyReddit(NotifyBase):
'targets': { 'targets': {
'name': _('Targets'), 'name': _('Targets'),
'type': 'list:string', 'type': 'list:string',
'required': True,
}, },
}) })
@ -275,7 +266,7 @@ class NotifyReddit(NotifyBase):
# Our keys we build using the provided content # Our keys we build using the provided content
self.__refresh_token = None self.__refresh_token = None
self.__access_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() \ self.kind = kind.strip().lower() \
if isinstance(kind, str) \ if isinstance(kind, str) \
@ -324,6 +315,13 @@ class NotifyReddit(NotifyBase):
if not self.subreddits: if not self.subreddits:
self.logger.warning( self.logger.warning(
'No subreddits were identified to be notified') '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 return
def url(self, privacy=False, *args, **kwargs): def url(self, privacy=False, *args, **kwargs):
@ -367,6 +365,12 @@ class NotifyReddit(NotifyBase):
params=NotifyReddit.urlencode(params), params=NotifyReddit.urlencode(params),
) )
def __len__(self):
"""
Returns the number of targets associated with this notification
"""
return len(self.subreddits)
def login(self): def login(self):
""" """
A simple wrapper to authenticate with the Reddit Server A simple wrapper to authenticate with the Reddit Server
@ -411,10 +415,10 @@ class NotifyReddit(NotifyBase):
if 'expires_in' in response: if 'expires_in' in response:
delta = timedelta(seconds=int(response['expires_in'])) delta = timedelta(seconds=int(response['expires_in']))
self.__access_token_expiry = \ self.__access_token_expiry = \
delta + datetime.utcnow() - self.clock_skew delta + datetime.now(timezone.utc) - self.clock_skew
else: else:
self.__access_token_expiry = self.access_token_lifetime_sec + \ self.__access_token_expiry = self.access_token_lifetime_sec + \
datetime.utcnow() - self.clock_skew datetime.now(timezone.utc) - self.clock_skew
# The Refresh Token # The Refresh Token
self.__refresh_token = response.get( 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 # 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 # all. This isn't fool-proof because we can't be sure the client
# time (calling this script) is completely synced up with the # 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: # the same allowing this to role smoothly:
now = datetime.utcnow() now = datetime.now(timezone.utc).replace(tzinfo=None)
if now < self.ratelimit_reset: if now < self.ratelimit_reset:
# We need to throttle for the difference in seconds # We need to throttle for the difference in seconds
wait = abs( wait = abs(
@ -665,8 +669,9 @@ class NotifyReddit(NotifyBase):
self.ratelimit_remaining = \ self.ratelimit_remaining = \
float(r.headers.get( float(r.headers.get(
'X-RateLimit-Remaining')) 'X-RateLimit-Remaining'))
self.ratelimit_reset = datetime.utcfromtimestamp( self.ratelimit_reset = datetime.fromtimestamp(
int(r.headers.get('X-RateLimit-Reset'))) int(r.headers.get('X-RateLimit-Reset')), timezone.utc
).replace(tzinfo=None)
except (TypeError, ValueError): except (TypeError, ValueError):
# This is returned if we could not retrieve this information # This is returned if we could not retrieve this information

View file

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# BSD 3-Clause License # BSD 2-Clause License
# #
# Apprise - Push Notification Library. # Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com> # Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation # this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution. # 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" # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
@ -348,6 +344,13 @@ class NotifyRocketChat(NotifyBase):
params=NotifyRocketChat.urlencode(params), 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): def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs):
""" """
wrapper to _send since we can alert more then one channel wrapper to _send since we can alert more then one channel

View file

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# BSD 3-Clause License # BSD 2-Clause License
# #
# Apprise - Push Notification Library. # Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com> # Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation # this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution. # 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" # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
@ -91,7 +87,7 @@ class NotifyRyver(NotifyBase):
# Define object templates # Define object templates
templates = ( templates = (
'{schema}://{organization}/{token}', '{schema}://{organization}/{token}',
'{schema}://{user}@{organization}/{token}', '{schema}://{botname}@{organization}/{token}',
) )
# Define our template tokens # Define our template tokens
@ -109,9 +105,10 @@ class NotifyRyver(NotifyBase):
'private': True, 'private': True,
'regex': (r'^[A-Z0-9]{15}$', 'i'), 'regex': (r'^[A-Z0-9]{15}$', 'i'),
}, },
'user': { 'botname': {
'name': _('Bot Name'), 'name': _('Bot Name'),
'type': 'string', 'type': 'string',
'map_to': 'user',
}, },
}) })

View file

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# BSD 3-Clause License # BSD 2-Clause License
# #
# Apprise - Push Notification Library. # Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com> # Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation # this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution. # 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" # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
@ -89,6 +85,7 @@ import base64
import requests import requests
from hashlib import sha256 from hashlib import sha256
from datetime import datetime from datetime import datetime
from datetime import timezone
from collections import OrderedDict from collections import OrderedDict
from xml.etree import ElementTree from xml.etree import ElementTree
from email.mime.text import MIMEText 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 # A URL that takes you to the setup/help of the specific protocol
setup_url = 'https://github.com/caronc/apprise/wiki/Notify_ses' 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 # AWS is pretty good for handling data load so request limits
# can occur in much shorter bursts # can occur in much shorter bursts
request_rate_per_sec = 2.5 request_rate_per_sec = 2.5
@ -156,6 +156,7 @@ class NotifySES(NotifyBase):
'name': _('From Email'), 'name': _('From Email'),
'type': 'string', 'type': 'string',
'map_to': 'from_addr', 'map_to': 'from_addr',
'required': True,
}, },
'access_key_id': { 'access_key_id': {
'name': _('Access Key ID'), 'name': _('Access Key ID'),
@ -173,6 +174,7 @@ class NotifySES(NotifyBase):
'name': _('Region'), 'name': _('Region'),
'type': 'string', 'type': 'string',
'regex': (r'^[a-z]{2}-[a-z-]+?-[0-9]+$', 'i'), 'regex': (r'^[a-z]{2}-[a-z-]+?-[0-9]+$', 'i'),
'required': True,
'map_to': 'region_name', 'map_to': 'region_name',
}, },
'targets': { 'targets': {
@ -424,7 +426,8 @@ class NotifySES(NotifyBase):
content = MIMEText(body, 'plain', 'utf-8') content = MIMEText(body, 'plain', 'utf-8')
# Create a Multipart container if there is an attachment # Create a Multipart container if there is an attachment
base = MIMEMultipart() if attach else content base = MIMEMultipart() \
if attach and self.attachment_support else content
# TODO: Deduplicate with `NotifyEmail`? # TODO: Deduplicate with `NotifyEmail`?
base['Subject'] = Header(title, 'utf-8') base['Subject'] = Header(title, 'utf-8')
@ -436,10 +439,11 @@ class NotifySES(NotifyBase):
base['Reply-To'] = formataddr(reply_to, charset='utf-8') base['Reply-To'] = formataddr(reply_to, charset='utf-8')
base['Cc'] = ','.join(cc) base['Cc'] = ','.join(cc)
base['Date'] = \ 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 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 # First attach our body to our content as the first element
base.attach(content) base.attach(content)
@ -585,7 +589,7 @@ class NotifySES(NotifyBase):
} }
# Get a reference time (used for header construction) # Get a reference time (used for header construction)
reference = datetime.utcnow() reference = datetime.now(timezone.utc)
# Provide Content-Length # Provide Content-Length
headers['Content-Length'] = str(len(payload)) headers['Content-Length'] = str(len(payload))
@ -816,6 +820,13 @@ class NotifySES(NotifyBase):
params=NotifySES.urlencode(params), 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 @staticmethod
def parse_url(url): def parse_url(url):
""" """

View file

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# BSD 3-Clause License # BSD 2-Clause License
# #
# Apprise - Push Notification Library. # Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com> # Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation # this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution. # 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" # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # 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): class NotifySMSEagle(NotifyBase):
""" """
A wrapper for SMSEagle Notifications A wrapper for SMSEagle Notifications
@ -96,6 +108,9 @@ class NotifySMSEagle(NotifyBase):
# The path we send our notification to # The path we send our notification to
notify_path = '/jsonrpc/sms' notify_path = '/jsonrpc/sms'
# Support attachments
attachment_support = True
# The maxumum length of the text message # The maxumum length of the text message
# The actual limit is 160 but SMSEagle looks after the handling # The actual limit is 160 but SMSEagle looks after the handling
# of large messages in it's upstream service # of large messages in it's upstream service
@ -129,6 +144,7 @@ class NotifySMSEagle(NotifyBase):
'token': { 'token': {
'name': _('Access Token'), 'name': _('Access Token'),
'type': 'string', 'type': 'string',
'required': True,
}, },
'target_phone': { 'target_phone': {
'name': _('Target Phone No'), 'name': _('Target Phone No'),
@ -154,6 +170,7 @@ class NotifySMSEagle(NotifyBase):
'targets': { 'targets': {
'name': _('Targets'), 'name': _('Targets'),
'type': 'list:string', 'type': 'list:string',
'required': True,
} }
}) })
@ -322,7 +339,7 @@ class NotifySMSEagle(NotifyBase):
has_error = False has_error = False
attachments = [] attachments = []
if attach: if attach and self.attachment_support:
for attachment in attach: for attachment in attach:
# Perform some simple error checking # Perform some simple error checking
if not attachment: if not attachment:
@ -403,15 +420,15 @@ class NotifySMSEagle(NotifyBase):
batch_size = 1 if not self.batch else self.default_batch_size batch_size = 1 if not self.batch else self.default_batch_size
notify_by = { notify_by = {
'phone': { SMSEagleCategory.PHONE: {
"method": "sms.send_sms", "method": "sms.send_sms",
'target': 'to', 'target': 'to',
}, },
'group': { SMSEagleCategory.GROUP: {
"method": "sms.send_togroup", "method": "sms.send_togroup",
'target': 'groupname', 'target': 'groupname',
}, },
'contact': { SMSEagleCategory.CONTACT: {
"method": "sms.send_tocontact", "method": "sms.send_tocontact",
'target': 'contactname', 'target': 'contactname',
}, },
@ -420,7 +437,7 @@ class NotifySMSEagle(NotifyBase):
# categories separated into a tuple since notify_by.keys() # categories separated into a tuple since notify_by.keys()
# returns an unpredicable list in Python 2.7 which causes # returns an unpredicable list in Python 2.7 which causes
# tests to fail every so often # tests to fail every so often
for category in ('phone', 'group', 'contact'): for category in SMSEAGLE_CATEGORIES:
# Create a copy of our template # Create a copy of our template
payload = { payload = {
'method': notify_by[category]['method'], 'method': notify_by[category]['method'],
@ -596,6 +613,28 @@ class NotifySMSEagle(NotifyBase):
params=NotifySMSEagle.urlencode(params), 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 @staticmethod
def parse_url(url): def parse_url(url):
""" """

Some files were not shown because too many files have changed in this diff Show more