Merge branch 'feature/UpdateApprise' into dev

This commit is contained in:
JackDandy 2023-10-21 13:01:30 +01:00
commit f69e31b295
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)
* Update Apprise 1.3.0 (6458ab0) to 1.6.0 (0c0d5da)
* Update attr 22.2.0 (683d056) to 23.1.0 (67e4ff2)
* Update Beautiful Soup 4.12.2 to 4.12.2 (30c58a1)
* Update diskcache 5.6.1 (4d30686) to 5.6.3 (323787f)

View file

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
# BSD 3-Clause License
# BSD 2-Clause License
#
# Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
# 3. Neither the name of the copyright holder nor the names of its
# contributors may be used to endorse or promote products derived from
# this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
@ -31,8 +27,8 @@
# POSSIBILITY OF SUCH DAMAGE.
import asyncio
import concurrent.futures as cf
import os
from functools import partial
from itertools import chain
from . import common
from .conversion import convert_between
@ -376,7 +372,7 @@ class Apprise:
try:
# Process arguments and build synchronous and asynchronous calls
# (this step can throw internal errors).
sync_partials, async_cors = self._create_notify_calls(
sequential_calls, parallel_calls = self._create_notify_calls(
body, title,
notify_type=notify_type, body_format=body_format,
tag=tag, match_always=match_always, attach=attach,
@ -387,49 +383,13 @@ class Apprise:
# No notifications sent, and there was an internal error.
return False
if not sync_partials and not async_cors:
if not sequential_calls and not parallel_calls:
# Nothing to send
return None
sync_result = Apprise._notify_all(*sync_partials)
if async_cors:
# A single coroutine sends all asynchronous notifications in
# parallel.
all_cor = Apprise._async_notify_all(*async_cors)
try:
# Python <3.7 automatically starts an event loop if there isn't
# already one for the main thread.
loop = asyncio.get_event_loop()
except RuntimeError:
# Python >=3.7 raises this exception if there isn't already an
# event loop. So, we can spin up our own.
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
loop.set_debug(self.debug)
# Run the coroutine and wait for the result.
async_result = loop.run_until_complete(all_cor)
# Clean up the loop.
loop.close()
asyncio.set_event_loop(None)
else:
old_debug = loop.get_debug()
loop.set_debug(self.debug)
# Run the coroutine and wait for the result.
async_result = loop.run_until_complete(all_cor)
loop.set_debug(old_debug)
else:
async_result = True
return sync_result and async_result
sequential_result = Apprise._notify_sequential(*sequential_calls)
parallel_result = Apprise._notify_parallel_threadpool(*parallel_calls)
return sequential_result and parallel_result
async def async_notify(self, *args, **kwargs):
"""
@ -442,41 +402,42 @@ class Apprise:
try:
# Process arguments and build synchronous and asynchronous calls
# (this step can throw internal errors).
sync_partials, async_cors = self._create_notify_calls(
sequential_calls, parallel_calls = self._create_notify_calls(
*args, **kwargs)
except TypeError:
# No notifications sent, and there was an internal error.
return False
if not sync_partials and not async_cors:
if not sequential_calls and not parallel_calls:
# Nothing to send
return None
sync_result = Apprise._notify_all(*sync_partials)
async_result = await Apprise._async_notify_all(*async_cors)
return sync_result and async_result
sequential_result = Apprise._notify_sequential(*sequential_calls)
parallel_result = \
await Apprise._notify_parallel_asyncio(*parallel_calls)
return sequential_result and parallel_result
def _create_notify_calls(self, *args, **kwargs):
"""
Creates notifications for all the plugins loaded.
Returns a list of synchronous calls (partial functions with no
arguments required) for plugins with async disabled and a list of
asynchronous calls (coroutines) for plugins with async enabled.
Returns a list of (server, notify() kwargs) tuples for plugins with
parallelism disabled and another list for plugins with parallelism
enabled.
"""
all_calls = list(self._create_notify_gen(*args, **kwargs))
# Split into synchronous partials and asynchronous coroutines.
sync_partials, async_cors = [], []
for notify in all_calls:
if asyncio.iscoroutine(notify):
async_cors.append(notify)
# Split into sequential and parallel notify() calls.
sequential, parallel = [], []
for (server, notify_kwargs) in all_calls:
if server.asset.async_mode:
parallel.append((server, notify_kwargs))
else:
sync_partials.append(notify)
sequential.append((server, notify_kwargs))
return sync_partials, async_cors
return sequential, parallel
def _create_notify_gen(self, body, title='',
notify_type=common.NotifyType.INFO,
@ -493,7 +454,7 @@ class Apprise:
logger.error(msg)
raise TypeError(msg)
if not (title or body):
if not (title or body or attach):
msg = "No message content specified to deliver"
logger.error(msg)
raise TypeError(msg)
@ -533,25 +494,29 @@ class Apprise:
# If our code reaches here, we either did not define a tag (it
# was set to None), or we did define a tag and the logic above
# determined we need to notify the service it's associated with
if server.notify_format not in conversion_body_map:
# Perform Conversion
conversion_body_map[server.notify_format] = \
convert_between(
body_format, server.notify_format, content=body)
# First we need to generate a key we will use to determine if we
# need to build our data out. Entries without are merged with
# the body at this stage.
key = server.notify_format if server.title_maxlen > 0\
else f'_{server.notify_format}'
if key not in conversion_title_map:
# Prepare our title
conversion_title_map[server.notify_format] = \
'' if not title else title
conversion_title_map[key] = '' if not title else title
# Tidy Title IF required (hence it will become part of the
# body)
if server.title_maxlen <= 0 and \
conversion_title_map[server.notify_format]:
# Conversion of title only occurs for services where the title
# is blended with the body (title_maxlen <= 0)
if conversion_title_map[key] and server.title_maxlen <= 0:
conversion_title_map[key] = convert_between(
body_format, server.notify_format,
content=conversion_title_map[key])
conversion_title_map[server.notify_format] = \
convert_between(
body_format, server.notify_format,
content=conversion_title_map[server.notify_format])
# Our body is always converted no matter what
conversion_body_map[key] = \
convert_between(
body_format, server.notify_format, content=body)
if interpret_escapes:
#
@ -561,13 +526,13 @@ class Apprise:
try:
# Added overhead required due to Python 3 Encoding Bug
# identified here: https://bugs.python.org/issue21331
conversion_body_map[server.notify_format] = \
conversion_body_map[server.notify_format]\
conversion_body_map[key] = \
conversion_body_map[key]\
.encode('ascii', 'backslashreplace')\
.decode('unicode-escape')
conversion_title_map[server.notify_format] = \
conversion_title_map[server.notify_format]\
conversion_title_map[key] = \
conversion_title_map[key]\
.encode('ascii', 'backslashreplace')\
.decode('unicode-escape')
@ -578,29 +543,26 @@ class Apprise:
raise TypeError(msg)
kwargs = dict(
body=conversion_body_map[server.notify_format],
title=conversion_title_map[server.notify_format],
body=conversion_body_map[key],
title=conversion_title_map[key],
notify_type=notify_type,
attach=attach,
body_format=body_format
)
if server.asset.async_mode:
yield server.async_notify(**kwargs)
else:
yield partial(server.notify, **kwargs)
yield (server, kwargs)
@staticmethod
def _notify_all(*partials):
def _notify_sequential(*servers_kwargs):
"""
Process a list of synchronous notify() calls.
Process a list of notify() calls sequentially and synchronously.
"""
success = True
for notify in partials:
for (server, kwargs) in servers_kwargs:
try:
# Send notification
result = notify()
result = server.notify(**kwargs)
success = success and result
except TypeError:
@ -616,14 +578,71 @@ class Apprise:
return success
@staticmethod
async def _async_notify_all(*cors):
def _notify_parallel_threadpool(*servers_kwargs):
"""
Process a list of asynchronous async_notify() calls.
Process a list of notify() calls in parallel and synchronously.
"""
n_calls = len(servers_kwargs)
# 0-length case
if n_calls == 0:
return True
# There's no need to use a thread pool for just a single notification
if n_calls == 1:
return Apprise._notify_sequential(servers_kwargs[0])
# Create log entry
logger.info('Notifying %d service(s) asynchronously.', len(cors))
logger.info(
'Notifying %d service(s) with threads.', len(servers_kwargs))
with cf.ThreadPoolExecutor() as executor:
success = True
futures = [executor.submit(server.notify, **kwargs)
for (server, kwargs) in servers_kwargs]
for future in cf.as_completed(futures):
try:
result = future.result()
success = success and result
except TypeError:
# These are our internally thrown notifications.
success = False
except Exception:
# A catch all so we don't have to abort early
# just because one of our plugins has a bug in it.
logger.exception("Unhandled Notification Exception")
success = False
return success
@staticmethod
async def _notify_parallel_asyncio(*servers_kwargs):
"""
Process a list of async_notify() calls in parallel and asynchronously.
"""
n_calls = len(servers_kwargs)
# 0-length case
if n_calls == 0:
return True
# (Unlike with the thread pool, we don't optimize for the single-
# notification case because asyncio can do useful work while waiting
# for that thread to complete)
# Create log entry
logger.info(
'Notifying %d service(s) asynchronously.', len(servers_kwargs))
async def do_call(server, kwargs):
return await server.async_notify(**kwargs)
cors = (do_call(server, kwargs) for (server, kwargs) in servers_kwargs)
results = await asyncio.gather(*cors, return_exceptions=True)
if any(isinstance(status, Exception)
@ -665,6 +684,12 @@ class Apprise:
'setup_url': getattr(plugin, 'setup_url', None),
# Placeholder - populated below
'details': None,
# Let upstream service know of the plugins that support
# attachments
'attachment_support': getattr(
plugin, 'attachment_support', False),
# Differentiat between what is a custom loaded plugin and
# which is native.
'category': getattr(plugin, 'category', None)
@ -790,6 +815,36 @@ class Apprise:
# If we reach here, then we indexed out of range
raise IndexError('list index out of range')
def __getstate__(self):
"""
Pickle Support dumps()
"""
attributes = {
'asset': self.asset,
# Prepare our URL list as we need to extract the associated tags
# and asset details associated with it
'urls': [{
'url': server.url(privacy=False),
'tag': server.tags if server.tags else None,
'asset': server.asset} for server in self.servers],
'locale': self.locale,
'debug': self.debug,
'location': self.location,
}
return attributes
def __setstate__(self, state):
"""
Pickle Support loads()
"""
self.servers = list()
self.asset = state['asset']
self.locale = state['locale']
self.location = state['location']
for entry in state['urls']:
self.add(entry['url'], asset=entry['asset'], tag=entry['tag'])
def __bool__(self):
"""
Allows the Apprise object to be wrapped in an 'if statement'.

View file

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
# BSD 3-Clause License
# BSD 2-Clause License
#
# Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
# 3. Neither the name of the copyright holder nor the names of its
# contributors may be used to endorse or promote products derived from
# this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE

View file

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
# BSD 3-Clause License
# BSD 2-Clause License
#
# Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
# 3. Neither the name of the copyright holder nor the names of its
# contributors may be used to endorse or promote products derived from
# this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE

View file

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
# BSD 3-Clause License
# BSD 2-Clause License
#
# Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
# 3. Neither the name of the copyright holder nor the names of its
# contributors may be used to endorse or promote products derived from
# this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE

View file

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
# BSD 3-Clause License
# BSD 2-Clause License
#
# Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
# 3. Neither the name of the copyright holder nor the names of its
# contributors may be used to endorse or promote products derived from
# this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
@ -33,14 +29,13 @@
import ctypes
import locale
import contextlib
import os
import re
from os.path import join
from os.path import dirname
from os.path import abspath
from .logger import logger
# Define our translation domain
DOMAIN = 'apprise'
LOCALE_DIR = abspath(join(dirname(__file__), 'i18n'))
# This gets toggled to True if we succeed
GETTEXT_LOADED = False
@ -49,17 +44,220 @@ try:
# Initialize gettext
import gettext
# install() creates a _() in our builtins
gettext.install(DOMAIN, localedir=LOCALE_DIR)
# Toggle our flag
GETTEXT_LOADED = True
except ImportError:
# gettext isn't available; no problem, just fall back to using
# the library features without multi-language support.
import builtins
builtins.__dict__['_'] = lambda x: x # pragma: no branch
# gettext isn't available; no problem; Use the library features without
# multi-language support.
pass
class AppriseLocale:
"""
A wrapper class to gettext so that we can manipulate multiple lanaguages
on the fly if required.
"""
# Define our translation domain
_domain = 'apprise'
# The path to our translations
_locale_dir = abspath(join(dirname(__file__), 'i18n'))
# Locale regular expression
_local_re = re.compile(
r'^((?P<ansii>C)|(?P<lang>([a-z]{2}))([_:](?P<country>[a-z]{2}))?)'
r'(\.(?P<enc>[a-z0-9-]+))?$', re.IGNORECASE)
# Define our default encoding
_default_encoding = 'utf-8'
# The function to assign `_` by default
_fn = 'gettext'
# The language we should fall back to if all else fails
_default_language = 'en'
def __init__(self, language=None):
"""
Initializes our object, if a language is specified, then we
initialize ourselves to that, otherwise we use whatever we detect
from the local operating system. If all else fails, we resort to the
defined default_language.
"""
# Cache previously loaded translations
self._gtobjs = {}
# Get our language
self.lang = AppriseLocale.detect_language(language)
# Our mapping to our _fn
self.__fn_map = None
if GETTEXT_LOADED is False:
# We're done
return
# Add language
self.add(self.lang)
def add(self, lang=None, set_default=True):
"""
Add a language to our list
"""
lang = lang if lang else self._default_language
if lang not in self._gtobjs:
# Load our gettext object and install our language
try:
self._gtobjs[lang] = gettext.translation(
self._domain, localedir=self._locale_dir, languages=[lang],
fallback=False)
# The non-intrusive method of applying the gettext change to
# the global namespace only
self.__fn_map = getattr(self._gtobjs[lang], self._fn)
except FileNotFoundError:
# The translation directory does not exist
logger.debug(
'Could not load translation path: %s',
join(self._locale_dir, lang))
# Fallback (handle case where self.lang does not exist)
if self.lang not in self._gtobjs:
self._gtobjs[self.lang] = gettext
self.__fn_map = getattr(self._gtobjs[self.lang], self._fn)
return False
logger.trace('Loaded language %s', lang)
if set_default:
logger.debug('Language set to %s', lang)
self.lang = lang
return True
@contextlib.contextmanager
def lang_at(self, lang, mapto=_fn):
"""
The syntax works as:
with at.lang_at('fr'):
# apprise works as though the french language has been
# defined. afterwards, the language falls back to whatever
# it was.
"""
if GETTEXT_LOADED is False:
# Do nothing
yield None
# we're done
return
# Tidy the language
lang = AppriseLocale.detect_language(lang, detect_fallback=False)
if lang not in self._gtobjs and not self.add(lang, set_default=False):
# Do Nothing
yield getattr(self._gtobjs[self.lang], mapto)
else:
# Yield
yield getattr(self._gtobjs[lang], mapto)
return
@property
def gettext(self):
"""
Return the current language gettext() function
Useful for assigning to `_`
"""
return self._gtobjs[self.lang].gettext
@staticmethod
def detect_language(lang=None, detect_fallback=True):
"""
Returns the language (if it's retrievable)
"""
# We want to only use the 2 character version of this language
# hence en_CA becomes en, en_US becomes en.
if not isinstance(lang, str):
if detect_fallback is False:
# no detection enabled; we're done
return None
# Posix lookup
lookup = os.environ.get
localename = None
for variable in ('LC_ALL', 'LC_CTYPE', 'LANG', 'LANGUAGE'):
localename = lookup(variable, None)
if localename:
result = AppriseLocale._local_re.match(localename)
if result and result.group('lang'):
return result.group('lang').lower()
# Windows handling
if hasattr(ctypes, 'windll'):
windll = ctypes.windll.kernel32
try:
lang = locale.windows_locale[
windll.GetUserDefaultUILanguage()]
# Our detected windows language
return lang[0:2].lower()
except (TypeError, KeyError):
# Fallback to posix detection
pass
# Built in locale library check
try:
# Acquire our locale
lang = locale.getlocale()[0]
except (ValueError, TypeError) as e:
# This occurs when an invalid locale was parsed from the
# environment variable. While we still return None in this
# case, we want to better notify the end user of this. Users
# receiving this error should check their environment
# variables.
logger.warning(
'Language detection failure / {}'.format(str(e)))
return None
return None if not lang else lang[0:2].lower()
def __getstate__(self):
"""
Pickle Support dumps()
"""
state = self.__dict__.copy()
# Remove the unpicklable entries.
del state['_gtobjs']
del state['_AppriseLocale__fn_map']
return state
def __setstate__(self, state):
"""
Pickle Support loads()
"""
self.__dict__.update(state)
# Our mapping to our _fn
self.__fn_map = None
self._gtobjs = {}
self.add(state['lang'], set_default=True)
#
# Prepare our default LOCALE Singleton
#
LOCALE = AppriseLocale()
class LazyTranslation:
@ -77,7 +275,7 @@ class LazyTranslation:
super().__init__(*args, **kwargs)
def __str__(self):
return gettext.gettext(self.text)
return LOCALE.gettext(self.text) if GETTEXT_LOADED else self.text
# Lazy translation handling
@ -86,140 +284,3 @@ def gettext_lazy(text):
A dummy function that can be referenced
"""
return LazyTranslation(text=text)
class AppriseLocale:
"""
A wrapper class to gettext so that we can manipulate multiple lanaguages
on the fly if required.
"""
def __init__(self, language=None):
"""
Initializes our object, if a language is specified, then we
initialize ourselves to that, otherwise we use whatever we detect
from the local operating system. If all else fails, we resort to the
defined default_language.
"""
# Cache previously loaded translations
self._gtobjs = {}
# Get our language
self.lang = AppriseLocale.detect_language(language)
if GETTEXT_LOADED is False:
# We're done
return
if self.lang:
# Load our gettext object and install our language
try:
self._gtobjs[self.lang] = gettext.translation(
DOMAIN, localedir=LOCALE_DIR, languages=[self.lang])
# Install our language
self._gtobjs[self.lang].install()
except IOError:
# This occurs if we can't access/load our translations
pass
@contextlib.contextmanager
def lang_at(self, lang):
"""
The syntax works as:
with at.lang_at('fr'):
# apprise works as though the french language has been
# defined. afterwards, the language falls back to whatever
# it was.
"""
if GETTEXT_LOADED is False:
# yield
yield
# we're done
return
# Tidy the language
lang = AppriseLocale.detect_language(lang, detect_fallback=False)
# Now attempt to load it
try:
if lang in self._gtobjs:
if lang != self.lang:
# Install our language only if we aren't using it
# already
self._gtobjs[lang].install()
else:
self._gtobjs[lang] = gettext.translation(
DOMAIN, localedir=LOCALE_DIR, languages=[self.lang])
# Install our language
self._gtobjs[lang].install()
# Yield
yield
except (IOError, KeyError):
# This occurs if we can't access/load our translations
# Yield reguardless
yield
finally:
# Fall back to our previous language
if lang != self.lang and lang in self._gtobjs:
# Install our language
self._gtobjs[self.lang].install()
return
@staticmethod
def detect_language(lang=None, detect_fallback=True):
"""
returns the language (if it's retrievable)
"""
# We want to only use the 2 character version of this language
# hence en_CA becomes en, en_US becomes en.
if not isinstance(lang, str):
if detect_fallback is False:
# no detection enabled; we're done
return None
if hasattr(ctypes, 'windll'):
windll = ctypes.windll.kernel32
try:
lang = locale.windows_locale[
windll.GetUserDefaultUILanguage()]
# Our detected windows language
return lang[0:2].lower()
except (TypeError, KeyError):
# Fallback to posix detection
pass
try:
# Detect language
lang = locale.getdefaultlocale()[0]
except ValueError as e:
# This occurs when an invalid locale was parsed from the
# environment variable. While we still return None in this
# case, we want to better notify the end user of this. Users
# receiving this error should check their environment
# variables.
logger.warning(
'Language detection failure / {}'.format(str(e)))
return None
except TypeError:
# None is returned if the default can't be determined
# we're done in this case
return None
return None if not lang else lang[0:2].lower()

View file

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
# BSD 3-Clause License
# BSD 2-Clause License
#
# Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
# 3. Neither the name of the copyright holder nor the names of its
# contributors may be used to endorse or promote products derived from
# this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
@ -204,7 +200,14 @@ class URLBase:
self.verify_certificate = parse_bool(kwargs.get('verify', True))
# Secure Mode
self.secure = kwargs.get('secure', False)
self.secure = kwargs.get('secure', None)
try:
if not isinstance(self.secure, bool):
# Attempt to detect
self.secure = kwargs.get('schema', '')[-1].lower() == 's'
except (TypeError, IndexError):
self.secure = False
self.host = URLBase.unquote(kwargs.get('host'))
self.port = kwargs.get('port')
@ -228,6 +231,11 @@ class URLBase:
# Always unquote the password if it exists
self.password = URLBase.unquote(self.password)
# Store our full path consistently ensuring it ends with a `/'
self.fullpath = URLBase.unquote(kwargs.get('fullpath'))
if not isinstance(self.fullpath, str) or not self.fullpath:
self.fullpath = '/'
# Store our Timeout Variables
if 'rto' in kwargs:
try:
@ -307,7 +315,36 @@ class URLBase:
arguments provied.
"""
raise NotImplementedError("url() is implimented by the child class.")
# Our default parameters
params = self.url_parameters(privacy=privacy, *args, **kwargs)
# Determine Authentication
auth = ''
if self.user and self.password:
auth = '{user}:{password}@'.format(
user=URLBase.quote(self.user, safe=''),
password=self.pprint(
self.password, privacy, mode=PrivacyMode.Secret, safe=''),
)
elif self.user:
auth = '{user}@'.format(
user=URLBase.quote(self.user, safe=''),
)
default_port = 443 if self.secure else 80
return '{schema}://{auth}{hostname}{port}{fullpath}?{params}'.format(
schema='https' if self.secure else 'http',
auth=auth,
# never encode hostname since we're expecting it to be a valid one
hostname=self.host,
port='' if self.port is None or self.port == default_port
else ':{}'.format(self.port),
fullpath=URLBase.quote(self.fullpath, safe='/')
if self.fullpath else '/',
params=URLBase.urlencode(params),
)
def __contains__(self, tags):
"""
@ -583,6 +620,33 @@ class URLBase:
"""
return (self.socket_connect_timeout, self.socket_read_timeout)
@property
def request_auth(self):
"""This is primarily used to fullfill the `auth` keyword argument
that is used by requests.get() and requests.put() calls.
"""
return (self.user, self.password) if self.user else None
@property
def request_url(self):
"""
Assemble a simple URL that can be used by the requests library
"""
# Acquire our schema
schema = 'https' if self.secure else 'http'
# Prepare our URL
url = '%s://%s' % (schema, self.host)
# Apply Port information if present
if isinstance(self.port, int):
url += ':%d' % self.port
# Append our full path
return url + self.fullpath
def url_parameters(self, *args, **kwargs):
"""
Provides a default set of args to work with. This can greatly
@ -603,7 +667,8 @@ class URLBase:
}
@staticmethod
def parse_url(url, verify_host=True, plus_to_space=False):
def parse_url(url, verify_host=True, plus_to_space=False,
strict_port=False):
"""Parses the URL and returns it broken apart into a dictionary.
This is very specific and customized for Apprise.
@ -624,13 +689,13 @@ class URLBase:
results = parse_url(
url, default_schema='unknown', verify_host=verify_host,
plus_to_space=plus_to_space)
plus_to_space=plus_to_space, strict_port=strict_port)
if not results:
# We're done; we failed to parse our url
return results
# if our URL ends with an 's', then assueme our secure flag is set.
# if our URL ends with an 's', then assume our secure flag is set.
results['secure'] = (results['schema'][-1] == 's')
# Support SSL Certificate 'verify' keyword. Default to being enabled
@ -650,6 +715,21 @@ class URLBase:
if 'user' in results['qsd']:
results['user'] = results['qsd']['user']
# parse_url() always creates a 'password' and 'user' entry in the
# results returned. Entries are set to None if they weren't specified
if results['password'] is None and 'user' in results['qsd']:
# Handle cases where the user= provided in 2 locations, we want
# the original to fall back as a being a password (if one wasn't
# otherwise defined)
# e.g.
# mailtos://PASSWORD@hostname?user=admin@mail-domain.com
# - the PASSWORD gets lost in the parse url() since a user=
# over-ride is specified.
presults = parse_url(results['url'])
if presults:
# Store our Password
results['password'] = presults['user']
# Store our socket read timeout if specified
if 'rto' in results['qsd']:
results['rto'] = results['qsd']['rto']
@ -685,6 +765,15 @@ class URLBase:
return response
def __len__(self):
"""
Should be over-ridden and allows the tracking of how many targets
are associated with each URLBase object.
Default is always 1
"""
return 1
def schemas(self):
"""A simple function that returns a set of all schemas associated
with this object based on the object.protocol and

View file

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
# BSD 3-Clause License
# BSD 2-Clause License
#
# Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
# 3. Neither the name of the copyright holder nor the names of its
# contributors may be used to endorse or promote products derived from
# this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
@ -31,7 +27,7 @@
# POSSIBILITY OF SUCH DAMAGE.
__title__ = 'Apprise'
__version__ = '1.3.0'
__version__ = '1.6.0'
__author__ = 'Chris Caron'
__license__ = 'BSD'
__copywrite__ = 'Copyright (C) 2023 Chris Caron <lead2gold@gmail.com>'

View file

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
# BSD 3-Clause License
# BSD 2-Clause License
#
# Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
# 3. Neither the name of the copyright holder nor the names of its
# contributors may be used to endorse or promote products derived from
# this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
@ -68,7 +64,8 @@ class AttachBase(URLBase):
# set to zero (0), then no check is performed
# 1 MB = 1048576 bytes
# 5 MB = 5242880 bytes
max_file_size = 5242880
# 1 GB = 1048576000 bytes
max_file_size = 1048576000
# By default all attachments types are inaccessible.
# Developers of items identified in the attachment plugin directory

View file

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
# BSD 3-Clause License
# BSD 2-Clause License
#
# Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
# 3. Neither the name of the copyright holder nor the names of its
# contributors may be used to endorse or promote products derived from
# this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE

View file

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
# BSD 3-Clause License
# BSD 2-Clause License
#
# Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
# 3. Neither the name of the copyright holder nor the names of its
# contributors may be used to endorse or promote products derived from
# this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE

View file

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
# BSD 3-Clause License
# BSD 2-Clause License
#
# Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
# 3. Neither the name of the copyright holder nor the names of its
# contributors may be used to endorse or promote products derived from
# this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE

View file

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
# BSD 3-Clause License
# BSD 2-Clause License
#
# Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
# 3. Neither the name of the copyright holder nor the names of its
# contributors may be used to endorse or promote products derived from
# this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE

View file

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
# BSD 3-Clause License
# BSD 2-Clause License
#
# Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
# 3. Neither the name of the copyright holder nor the names of its
# contributors may be used to endorse or promote products derived from
# this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
@ -355,6 +351,77 @@ class ConfigBase(URLBase):
# missing and/or expired.
return True
@staticmethod
def __normalize_tag_groups(group_tags):
"""
Used to normalize a tag assign map which looks like:
{
'group': set('{tag1}', '{group1}', '{tag2}'),
'group1': set('{tag2}','{tag3}'),
}
Then normalized it (merging groups); with respect to the above, the
output would be:
{
'group': set('{tag1}', '{tag2}', '{tag3}),
'group1': set('{tag2}','{tag3}'),
}
"""
# Prepare a key set list we can use
tag_groups = set([str(x) for x in group_tags.keys()])
def _expand(tags, ignore=None):
"""
Expands based on tag provided and returns a set
this also updates the group_tags while it goes
"""
# Prepare ourselves a return set
results = set()
ignore = set() if ignore is None else ignore
# track groups
groups = set()
for tag in tags:
if tag in ignore:
continue
# Track our groups
groups.add(tag)
# Store what we know is worth keping
results |= group_tags[tag] - tag_groups
# Get simple tag assignments
found = group_tags[tag] & tag_groups
if not found:
continue
for gtag in found:
if gtag in ignore:
continue
# Go deeper (recursion)
ignore.add(tag)
group_tags[gtag] = _expand(set([gtag]), ignore=ignore)
results |= group_tags[gtag]
# Pop ignore
ignore.remove(tag)
return results
for tag in tag_groups:
# Get our tags
group_tags[tag] |= _expand(set([tag]))
if not group_tags[tag]:
ConfigBase.logger.warning(
'The group {} has no tags assigned to it'.format(tag))
del group_tags[tag]
@staticmethod
def parse_url(url, verify_host=True):
"""Parses the URL and returns it broken apart into a dictionary.
@ -533,6 +600,9 @@ class ConfigBase(URLBase):
# as additional configuration entries when loaded.
include <ConfigURL>
# Assign tag contents to a group identifier
<Group(s)>=<Tag(s)>
"""
# A list of loaded Notification Services
servers = list()
@ -541,6 +611,12 @@ class ConfigBase(URLBase):
# the include keyword
configs = list()
# Track all of the tags we want to assign later on
group_tags = {}
# Track our entries to preload
preloaded = []
# Prepare our Asset Object
asset = asset if isinstance(asset, AppriseAsset) else AppriseAsset()
@ -548,7 +624,7 @@ class ConfigBase(URLBase):
valid_line_re = re.compile(
r'^\s*(?P<line>([;#]+(?P<comment>.*))|'
r'(\s*(?P<tags>[a-z0-9, \t_-]+)\s*=|=)?\s*'
r'(?P<url>[a-z0-9]{2,9}://.*)|'
r'((?P<url>[a-z0-9]{1,12}://.*)|(?P<assign>[a-z0-9, \t_-]+))|'
r'include\s+(?P<config>.+))?\s*$', re.I)
try:
@ -574,8 +650,13 @@ class ConfigBase(URLBase):
# otherwise.
return (list(), list())
url, config = result.group('url'), result.group('config')
if not (url or config):
# Retrieve our line
url, assign, config = \
result.group('url'), \
result.group('assign'), \
result.group('config')
if not (url or config or assign):
# Comment/empty line; do nothing
continue
@ -595,6 +676,33 @@ class ConfigBase(URLBase):
loggable_url = url if not asset.secure_logging \
else cwe312_url(url)
if assign:
groups = set(parse_list(result.group('tags'), cast=str))
if not groups:
# no tags were assigned
ConfigBase.logger.warning(
'Unparseable tag assignment - no group(s) '
'on line {}'.format(line))
continue
# Get our tags
tags = set(parse_list(assign, cast=str))
if not tags:
# no tags were assigned
ConfigBase.logger.warning(
'Unparseable tag assignment - no tag(s) to assign '
'on line {}'.format(line))
continue
# Update our tag group map
for tag_group in groups:
if tag_group not in group_tags:
group_tags[tag_group] = set()
# ensure our tag group is never included in the assignment
group_tags[tag_group] |= tags - set([tag_group])
continue
# Acquire our url tokens
results = plugins.url_to_dict(
url, secure_logging=asset.secure_logging)
@ -607,25 +715,57 @@ class ConfigBase(URLBase):
# Build a list of tags to associate with the newly added
# notifications if any were set
results['tag'] = set(parse_list(result.group('tags')))
results['tag'] = set(parse_list(result.group('tags'), cast=str))
# Set our Asset Object
results['asset'] = asset
# Store our preloaded entries
preloaded.append({
'results': results,
'line': line,
'loggable_url': loggable_url,
})
#
# Normalize Tag Groups
# - Expand Groups of Groups so that they don't exist
#
ConfigBase.__normalize_tag_groups(group_tags)
#
# URL Processing
#
for entry in preloaded:
# Point to our results entry for easier reference below
results = entry['results']
#
# Apply our tag groups if they're defined
#
for group, tags in group_tags.items():
# Detect if anything assigned to this tag also maps back to a
# group. If so we want to add the group to our list
if next((True for tag in results['tag']
if tag in tags), False):
results['tag'].add(group)
try:
# Attempt to create an instance of our plugin using the
# parsed URL information
plugin = common.NOTIFY_SCHEMA_MAP[results['schema']](**results)
plugin = common.NOTIFY_SCHEMA_MAP[
results['schema']](**results)
# Create log entry of loaded URL
ConfigBase.logger.debug(
'Loaded URL: %s', plugin.url(privacy=asset.secure_logging))
'Loaded URL: %s', plugin.url(
privacy=results['asset'].secure_logging))
except Exception as e:
# the arguments are invalid or can not be used.
ConfigBase.logger.warning(
'Could not load URL {} on line {}.'.format(
loggable_url, line))
entry['loggable_url'], entry['line']))
ConfigBase.logger.debug('Loading Exception: %s' % str(e))
continue

View file

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
# BSD 3-Clause License
# BSD 2-Clause License
#
# Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
# 3. Neither the name of the copyright holder nor the names of its
# contributors may be used to endorse or promote products derived from
# this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE

View file

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
# BSD 3-Clause License
# BSD 2-Clause License
#
# Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
# 3. Neither the name of the copyright holder nor the names of its
# contributors may be used to endorse or promote products derived from
# this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE

View file

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
# BSD 3-Clause License
# BSD 2-Clause License
#
# Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
# 3. Neither the name of the copyright holder nor the names of its
# contributors may be used to endorse or promote products derived from
# this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE

View file

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
# BSD 3-Clause License
# BSD 2-Clause License
#
# Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
# 3. Neither the name of the copyright holder nor the names of its
# contributors may be used to endorse or promote products derived from
# this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE

View file

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
# BSD 3-Clause License
# BSD 2-Clause License
#
# Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
# 3. Neither the name of the copyright holder nor the names of its
# contributors may be used to endorse or promote products derived from
# this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE

View file

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
# BSD 3-Clause License
# BSD 2-Clause License
#
# Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
# 3. Neither the name of the copyright holder nor the names of its
# contributors may be used to endorse or promote products derived from
# this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
@ -28,6 +24,7 @@
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
from ..plugins.NotifyBase import NotifyBase

View file

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
# BSD 3-Clause License
# BSD 2-Clause License
#
# Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
# 3. Neither the name of the copyright holder nor the names of its
# contributors may be used to endorse or promote products derived from
# this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE

View file

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
# BSD 3-Clause License
# BSD 2-Clause License
#
# Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
# 3. Neither the name of the copyright holder nor the names of its
# contributors may be used to endorse or promote products derived from
# this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE

View file

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

View file

@ -3,9 +3,10 @@
# This file is distributed under the same license as the apprise project.
# Chris Caron <lead2gold@gmail.com>, 2019.
#
msgid ""
msgid ""
msgstr ""
"Project-Id-Version: apprise 0.7.6\n"
"Project-Id-Version: apprise 1.4.5\n"
"Report-Msgid-Bugs-To: lead2gold@gmail.com\n"
"POT-Creation-Date: 2019-05-28 16:56-0400\n"
"PO-Revision-Date: 2019-05-24 20:00-0400\n"
@ -18,276 +19,272 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n"
"Generated-By: Babel 2.6.0\n"
msgid "API Key"
msgstr ""
msgid "API Key"
msgstr "API Key"
msgid "Access Key"
msgstr ""
msgid "Access Key"
msgstr "Access Key"
msgid "Access Key ID"
msgstr ""
msgid "Access Key ID"
msgstr "Access Key ID"
msgid "Access Secret"
msgstr ""
msgid "Access Secret"
msgstr "Access Secret"
msgid "Access Token"
msgstr ""
msgid "Access Token"
msgstr "Access Token"
msgid "Account SID"
msgstr ""
msgid "Account SID"
msgstr "Account SID"
msgid "Add Tokens"
msgstr ""
msgid "Add Tokens"
msgstr "Add Tokens"
msgid "Application Key"
msgstr ""
msgid "Application Key"
msgstr "Application Key"
msgid "Application Secret"
msgstr ""
msgid "Application Secret"
msgstr "Application Secret"
msgid "Auth Token"
msgstr ""
msgid "Auth Token"
msgstr "Auth Token"
msgid "Authorization Token"
msgstr ""
msgid "Authorization Token"
msgstr "Authorization Token"
msgid "Avatar Image"
msgstr ""
msgid "Avatar Image"
msgstr "Avatar Image"
msgid "Bot Name"
msgstr ""
msgid "Bot Name"
msgstr "Bot Name"
msgid "Bot Token"
msgstr ""
msgid "Bot Token"
msgstr "Bot Token"
msgid "Channels"
msgstr ""
msgid "Channels"
msgstr "Channels"
msgid "Consumer Key"
msgstr ""
msgid "Consumer Key"
msgstr "Consumer Key"
msgid "Consumer Secret"
msgstr ""
msgid "Consumer Secret"
msgstr "Consumer Secret"
msgid "Detect Bot Owner"
msgstr ""
msgid "Detect Bot Owner"
msgstr "Detect Bot Owner"
msgid "Device ID"
msgstr ""
msgid "Device ID"
msgstr "Device ID"
msgid "Display Footer"
msgstr ""
msgid "Display Footer"
msgstr "Display Footer"
msgid "Domain"
msgstr ""
msgid "Domain"
msgstr "Domain"
msgid "Duration"
msgstr ""
msgid "Duration"
msgstr "Duration"
msgid "Events"
msgstr ""
msgid "Events"
msgstr "Events"
msgid "Footer Logo"
msgstr ""
msgid "Footer Logo"
msgstr "Footer Logo"
msgid "From Email"
msgstr ""
msgid "From Email"
msgstr "From Email"
msgid "From Name"
msgstr ""
msgid "From Name"
msgstr "From Name"
msgid "From Phone No"
msgstr ""
msgid "From Phone No"
msgstr "From Phone No"
msgid "Group"
msgstr ""
msgid "Group"
msgstr "Group"
msgid "HTTP Header"
msgstr ""
msgid "HTTP Header"
msgstr "HTTP Header"
msgid "Hostname"
msgstr ""
msgid "Hostname"
msgstr "Hostname"
msgid "Include Image"
msgstr ""
msgid "Include Image"
msgstr "Include Image"
msgid "Modal"
msgstr ""
msgid "Modal"
msgstr "Modal"
msgid "Notify Format"
msgstr ""
msgid "Notify Format"
msgstr "Notify Format"
msgid "Organization"
msgstr ""
msgid "Organization"
msgstr "Organization"
msgid "Overflow Mode"
msgstr ""
msgid "Overflow Mode"
msgstr "Overflow Mode"
msgid "Password"
msgstr ""
msgid "Password"
msgstr "Password"
msgid "Port"
msgstr ""
msgid "Port"
msgstr "Port"
msgid "Priority"
msgstr ""
msgid "Priority"
msgstr "Priority"
msgid "Provider Key"
msgstr ""
msgid "Provider Key"
msgstr "Provider Key"
msgid "Region"
msgstr ""
msgid "Region"
msgstr "Region"
msgid "Region Name"
msgstr ""
msgid "Region Name"
msgstr "Region Name"
msgid "Remove Tokens"
msgstr ""
msgid "Remove Tokens"
msgstr "Remove Tokens"
msgid "Rooms"
msgstr ""
msgid "Rooms"
msgstr "Rooms"
msgid "SMTP Server"
msgstr ""
msgid "SMTP Server"
msgstr "SMTP Server"
msgid "Schema"
msgstr ""
msgid "Schema"
msgstr "Schema"
msgid "Secret Access Key"
msgstr ""
msgid "Secret Access Key"
msgstr "Secret Access Key"
msgid "Secret Key"
msgstr ""
msgid "Secret Key"
msgstr "Secret Key"
msgid "Secure Mode"
msgstr ""
msgid "Secure Mode"
msgstr "Secure Mode"
msgid "Server Timeout"
msgstr ""
msgid "Server Timeout"
msgstr "Server Timeout"
msgid "Sound"
msgstr ""
msgid "Sound"
msgstr "Sound"
msgid "Source JID"
msgstr ""
msgid "Source JID"
msgstr "Source JID"
msgid "Target Channel"
msgstr ""
msgid "Target Channel"
msgstr "Target Channel"
msgid "Target Chat ID"
msgstr ""
msgid "Target Chat ID"
msgstr "Target Chat ID"
msgid "Target Device"
msgstr ""
msgid "Target Device"
msgstr "Target Device"
msgid "Target Device ID"
msgstr ""
msgid "Target Device ID"
msgstr "Target Device ID"
msgid "Target Email"
msgstr ""
msgid "Target Email"
msgstr "Target Email"
msgid "Target Emails"
msgstr ""
msgid "Target Emails"
msgstr "Target Emails"
msgid "Target Encoded ID"
msgstr ""
msgid "Target Encoded ID"
msgstr "Target Encoded ID"
msgid "Target JID"
msgstr ""
msgid "Target JID"
msgstr "Target JID"
msgid "Target Phone No"
msgstr ""
msgid "Target Phone No"
msgstr "Target Phone No"
msgid "Target Room Alias"
msgstr ""
msgid "Target Room Alias"
msgstr "Target Room Alias"
msgid "Target Room ID"
msgstr ""
msgid "Target Room ID"
msgstr "Target Room ID"
msgid "Target Short Code"
msgstr ""
msgid "Target Short Code"
msgstr "Target Short Code"
msgid "Target Tag ID"
msgstr ""
msgid "Target Tag ID"
msgstr "Target Tag ID"
msgid "Target Topic"
msgstr ""
msgid "Target Topic"
msgstr "Target Topic"
msgid "Target User"
msgstr ""
msgid "Target User"
msgstr "Target User"
msgid "Targets"
msgstr ""
msgid "Targets"
msgstr "Targets"
msgid "Text To Speech"
msgstr ""
msgid "Text To Speech"
msgstr "Text To Speech"
msgid "To Channel ID"
msgstr ""
msgid "To Channel ID"
msgstr "To Channel ID"
msgid "To Email"
msgstr ""
msgid "To Email"
msgstr "To Email"
msgid "To User ID"
msgstr ""
msgid "To User ID"
msgstr "To User ID"
msgid "Token"
msgstr ""
msgid "Token"
msgstr "Token"
msgid "Token A"
msgstr ""
msgid "Token A"
msgstr "Token A"
msgid "Token B"
msgstr ""
msgid "Token B"
msgstr "Token B"
msgid "Token C"
msgstr ""
msgid "Token C"
msgstr "Token C"
msgid "Urgency"
msgstr ""
msgid "Urgency"
msgstr "Urgency"
msgid "Use Avatar"
msgstr ""
msgid "Use Avatar"
msgstr "Use Avatar"
msgid "User"
msgstr ""
msgid "User"
msgstr "User"
msgid "User Key"
msgstr ""
msgid "User Key"
msgstr "User Key"
msgid "User Name"
msgstr ""
msgid "User Name"
msgstr "User Name"
msgid "Username"
msgstr ""
msgid "Username"
msgstr "Username"
msgid "Verify SSL"
msgstr ""
msgid "Verify SSL"
msgstr "Verify SSL"
msgid "Version"
msgstr ""
msgid "Version"
msgstr "Version"
msgid "Webhook"
msgstr ""
msgid "Webhook"
msgstr "Webhook"
msgid "Webhook ID"
msgstr ""
msgid "Webhook ID"
msgstr "Webhook ID"
msgid "Webhook Mode"
msgstr ""
msgid "Webhook Mode"
msgstr "Webhook Mode"
msgid "Webhook Token"
msgstr ""
msgid "Webhook Token"
msgstr "Webhook Token"
msgid "X-Axis"
msgstr ""
msgid "X-Axis"
msgstr "X-Axis"
msgid "XEP"
msgstr ""
msgid "Y-Axis"
msgstr ""
#~ msgid "Access Key Secret"
#~ msgstr ""
msgid "XEP"
msgstr "XEP"
msgid "Y-Axis"
msgstr "Y-Axis"

View file

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
# BSD 3-Clause License
# BSD 2-Clause License
#
# Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
# 3. Neither the name of the copyright holder nor the names of its
# contributors may be used to endorse or promote products derived from
# this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE

View file

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
# BSD 3-Clause License
# BSD 2-Clause License
#
# Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
# 3. Neither the name of the copyright holder nor the names of its
# contributors may be used to endorse or promote products derived from
# this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
@ -33,6 +29,7 @@
import re
import requests
from json import dumps
import base64
from .NotifyBase import NotifyBase
from ..URLBase import PrivacyMode
@ -42,6 +39,20 @@ from ..utils import validate_regex
from ..AppriseLocale import gettext_lazy as _
class AppriseAPIMethod:
"""
Defines the method to post data tot he remote server
"""
JSON = 'json'
FORM = 'form'
APPRISE_API_METHODS = (
AppriseAPIMethod.FORM,
AppriseAPIMethod.JSON,
)
class NotifyAppriseAPI(NotifyBase):
"""
A wrapper for Apprise (Persistent) API Notifications
@ -62,9 +73,12 @@ class NotifyAppriseAPI(NotifyBase):
# A URL that takes you to the setup/help of the specific protocol
setup_url = 'https://github.com/caronc/apprise/wiki/Notify_apprise_api'
# Support attachments
attachment_support = True
# Depending on the number of transactions/notifications taking place, this
# could take a while. 30 seconds should be enough to perform the task
socket_connect_timeout = 30.0
socket_read_timeout = 30.0
# Disable throttle rate for Apprise API requests since they are normally
# local anyway
@ -119,6 +133,12 @@ class NotifyAppriseAPI(NotifyBase):
'name': _('Tags'),
'type': 'string',
},
'method': {
'name': _('Query Method'),
'type': 'choice:string',
'values': APPRISE_API_METHODS,
'default': APPRISE_API_METHODS[0],
},
'to': {
'alias_of': 'token',
},
@ -132,7 +152,8 @@ class NotifyAppriseAPI(NotifyBase):
},
}
def __init__(self, token=None, tags=None, headers=None, **kwargs):
def __init__(self, token=None, tags=None, method=None, headers=None,
**kwargs):
"""
Initialize Apprise API Object
@ -142,10 +163,6 @@ class NotifyAppriseAPI(NotifyBase):
"""
super().__init__(**kwargs)
self.fullpath = kwargs.get('fullpath')
if not isinstance(self.fullpath, str):
self.fullpath = '/'
self.token = validate_regex(
token, *self.template_tokens['token']['regex'])
if not self.token:
@ -154,6 +171,14 @@ class NotifyAppriseAPI(NotifyBase):
self.logger.warning(msg)
raise TypeError(msg)
self.method = self.template_args['method']['default'] \
if not isinstance(method, str) else method.lower()
if self.method not in APPRISE_API_METHODS:
msg = 'The method specified ({}) is invalid.'.format(method)
self.logger.warning(msg)
raise TypeError(msg)
# Build list of tags
self.__tags = parse_list(tags)
@ -169,8 +194,13 @@ class NotifyAppriseAPI(NotifyBase):
Returns the URL built dynamically based on specified arguments.
"""
# Our URL parameters
params = self.url_parameters(privacy=privacy, *args, **kwargs)
# Define any URL parameters
params = {
'method': self.method,
}
# Extend our parameters
params.update(self.url_parameters(privacy=privacy, *args, **kwargs))
# Append our headers into our parameters
params.update({'+{}'.format(k): v for k, v in self.headers.items()})
@ -209,15 +239,61 @@ class NotifyAppriseAPI(NotifyBase):
token=self.pprint(self.token, privacy, safe=''),
params=NotifyAppriseAPI.urlencode(params))
def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs):
def send(self, body, title='', notify_type=NotifyType.INFO, attach=None,
**kwargs):
"""
Perform Apprise API Notification
"""
headers = {}
# Prepare HTTP Headers
headers = {
'User-Agent': self.app_id,
}
# Apply any/all header over-rides defined
headers.update(self.headers)
attachments = []
files = []
if attach and self.attachment_support:
for no, attachment in enumerate(attach, start=1):
# Perform some simple error checking
if not attachment:
# We could not access the attachment
self.logger.error(
'Could not access attachment {}.'.format(
attachment.url(privacy=True)))
return False
try:
if self.method == AppriseAPIMethod.JSON:
with open(attachment.path, 'rb') as f:
# Output must be in a DataURL format (that's what
# PushSafer calls it):
attachments.append({
'filename': attachment.name,
'base64': base64.b64encode(f.read())
.decode('utf-8'),
'mimetype': attachment.mimetype,
})
else: # AppriseAPIMethod.FORM
files.append((
'file{:02d}'.format(no),
(
attachment.name,
open(attachment.path, 'rb'),
attachment.mimetype,
)
))
except (OSError, IOError) as e:
self.logger.warning(
'An I/O error occurred while reading {}.'.format(
attachment.name if attachment else 'attachment'))
self.logger.debug('I/O Exception: %s' % str(e))
return False
# prepare Apprise API Object
payload = {
# Apprise API Payload
@ -227,6 +303,14 @@ class NotifyAppriseAPI(NotifyBase):
'format': self.notify_format,
}
if self.method == AppriseAPIMethod.JSON:
headers['Content-Type'] = 'application/json'
if attachments:
payload['attachments'] = attachments
payload = dumps(payload)
if self.__tags:
payload['tag'] = self.__tags
@ -242,13 +326,13 @@ class NotifyAppriseAPI(NotifyBase):
url += ':%d' % self.port
fullpath = self.fullpath.strip('/')
url += '/{}/'.format(fullpath) if fullpath else '/'
url += 'notify/{}'.format(self.token)
url += '{}'.format('/' + fullpath) if fullpath else ''
url += '/notify/{}'.format(self.token)
# Some entries can not be over-ridden
headers.update({
'User-Agent': self.app_id,
'Content-Type': 'application/json',
# Our response to be in JSON format always
'Accept': 'application/json',
# Pass our Source UUID4 Identifier
'X-Apprise-ID': self.asset._uid,
# Pass our current recursion count to our upstream server
@ -266,9 +350,10 @@ class NotifyAppriseAPI(NotifyBase):
try:
r = requests.post(
url,
data=dumps(payload),
data=payload,
headers=headers,
auth=auth,
files=files if files else None,
verify=self.verify_certificate,
timeout=self.request_timeout,
)
@ -290,7 +375,8 @@ class NotifyAppriseAPI(NotifyBase):
return False
else:
self.logger.info('Sent Apprise API notification.')
self.logger.info(
'Sent Apprise API notification; method=%s.', self.method)
except requests.RequestException as e:
self.logger.warning(
@ -301,6 +387,18 @@ class NotifyAppriseAPI(NotifyBase):
# Return; we're done
return False
except (OSError, IOError) as e:
self.logger.warning(
'An I/O error occurred while reading one of the '
'attached files.')
self.logger.debug('I/O Exception: %s' % str(e))
return False
finally:
for file in files:
# Ensure all files are closed
file[1][1].close()
return True
@staticmethod
@ -377,4 +475,9 @@ class NotifyAppriseAPI(NotifyBase):
# re-assemble our full path
results['fullpath'] = '/'.join(entries)
# Set method if specified
if 'method' in results['qsd'] and len(results['qsd']['method']):
results['method'] = \
NotifyAppriseAPI.unquote(results['qsd']['method'])
return results

View file

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

View file

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
# BSD 3-Clause License
# BSD 2-Clause License
#
# Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
# 3. Neither the name of the copyright holder nor the names of its
# contributors may be used to endorse or promote products derived from
# this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
@ -139,6 +135,18 @@ class NotifyBase(URLBase):
# Default Overflow Mode
overflow_mode = OverflowMode.UPSTREAM
# Support Attachments; this defaults to being disabled.
# Since apprise allows you to send attachments without a body or title
# defined, by letting Apprise know the plugin won't support attachments
# up front, it can quickly pass over and ignore calls to these end points.
# You must set this to true if your application can handle attachments.
# You must also consider a flow change to your notification if this is set
# to True as well as now there will be cases where both the body and title
# may not be set. There will never be a case where a body, or attachment
# isn't set in the same call to your notify() function.
attachment_support = False
# Default Title HTML Tagging
# When a title is specified for a notification service that doesn't accept
# titles, by default apprise tries to give a plesant view and convert the
@ -316,7 +324,7 @@ class NotifyBase(URLBase):
the_cors = (do_send(**kwargs2) for kwargs2 in send_calls)
return all(await asyncio.gather(*the_cors))
def _build_send_calls(self, body, title=None,
def _build_send_calls(self, body=None, title=None,
notify_type=NotifyType.INFO, overflow=None,
attach=None, body_format=None, **kwargs):
"""
@ -339,6 +347,28 @@ class NotifyBase(URLBase):
# bad attachments
raise
# Handle situations where the body is None
body = '' if not body else body
elif not (body or attach):
# If there is not an attachment at the very least, a body must be
# present
msg = "No message body or attachment was specified."
self.logger.warning(msg)
raise TypeError(msg)
if not body and not self.attachment_support:
# If no body was specified, then we know that an attachment
# was. This is logic checked earlier in the code.
#
# Knowing this, if the plugin itself doesn't support sending
# attachments, there is nothing further to do here, just move
# along.
msg = f"{self.service_name} does not support attachments; " \
" service skipped"
self.logger.warning(msg)
raise TypeError(msg)
# Handle situations where the title is None
title = '' if not title else title

View file

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
# BSD 3-Clause License
# BSD 2-Clause License
#
# Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
# 3. Neither the name of the copyright holder nor the names of its
# contributors may be used to endorse or promote products derived from
# this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
@ -46,6 +42,7 @@ except ImportError:
from .NotifyBase import NotifyBase
from ..URLBase import PrivacyMode
from ..utils import parse_bool
from ..utils import parse_list
from ..utils import validate_regex
from ..common import NotifyType
from ..common import NotifyImageSize
@ -58,7 +55,7 @@ DEFAULT_TAG = '@all'
# list of tagged devices that the notification need to be send to, and a
# boolean operator (and / or) that defines the criteria to match devices
# against those tags.
IS_TAG = re.compile(r'^[@](?P<name>[A-Z0-9]{1,63})$', re.I)
IS_TAG = re.compile(r'^[@]?(?P<name>[A-Z0-9]{1,63})$', re.I)
# Device tokens are only referenced when developing.
# It's not likely you'll send a message directly to a device, but if you do;
@ -150,6 +147,12 @@ class NotifyBoxcar(NotifyBase):
'to': {
'alias_of': 'targets',
},
'access': {
'alias_of': 'access_key',
},
'secret': {
'alias_of': 'secret_key',
},
})
def __init__(self, access, secret, targets=None, include_image=True,
@ -160,7 +163,7 @@ class NotifyBoxcar(NotifyBase):
super().__init__(**kwargs)
# Initialize tag list
self.tags = list()
self._tags = list()
# Initialize device_token list
self.device_tokens = list()
@ -184,29 +187,27 @@ class NotifyBoxcar(NotifyBase):
raise TypeError(msg)
if not targets:
self.tags.append(DEFAULT_TAG)
self._tags.append(DEFAULT_TAG)
targets = []
elif isinstance(targets, str):
targets = [x for x in filter(bool, TAGS_LIST_DELIM.split(
targets,
))]
# Validate targets and drop bad ones:
for target in targets:
if IS_TAG.match(target):
for target in parse_list(targets):
result = IS_TAG.match(target)
if result:
# store valid tag/alias
self.tags.append(IS_TAG.match(target).group('name'))
self._tags.append(result.group('name'))
continue
elif IS_DEVICETOKEN.match(target):
result = IS_DEVICETOKEN.match(target)
if result:
# store valid device
self.device_tokens.append(target)
continue
else:
self.logger.warning(
'Dropped invalid tag/alias/device_token '
'({}) specified.'.format(target),
)
self.logger.warning(
'Dropped invalid tag/alias/device_token '
'({}) specified.'.format(target),
)
# Track whether or not we want to send an image with our notification
# or not.
@ -235,11 +236,10 @@ class NotifyBoxcar(NotifyBase):
if title:
payload['aps']['@title'] = title
if body:
payload['aps']['alert'] = body
payload['aps']['alert'] = body
if self.tags:
payload['tags'] = {'or': self.tags}
if self._tags:
payload['tags'] = {'or': self._tags}
if self.device_tokens:
payload['device_tokens'] = self.device_tokens
@ -341,10 +341,18 @@ class NotifyBoxcar(NotifyBase):
self.secret, privacy, mode=PrivacyMode.Secret, safe=''),
targets='/'.join([
NotifyBoxcar.quote(x, safe='') for x in chain(
self.tags, self.device_tokens) if x != DEFAULT_TAG]),
self._tags, self.device_tokens) if x != DEFAULT_TAG]),
params=NotifyBoxcar.urlencode(params),
)
def __len__(self):
"""
Returns the number of targets associated with this notification
"""
targets = len(self._tags) + len(self.device_tokens)
# DEFAULT_TAG is set if no tokens/tags are otherwise set
return targets if targets > 0 else 1
@staticmethod
def parse_url(url):
"""
@ -374,6 +382,16 @@ class NotifyBoxcar(NotifyBase):
results['targets'] += \
NotifyBoxcar.parse_list(results['qsd'].get('to'))
# Access
if 'access' in results['qsd'] and results['qsd']['access']:
results['access'] = NotifyBoxcar.unquote(
results['qsd']['access'].strip())
# Secret
if 'secret' in results['qsd'] and results['qsd']['secret']:
results['secret'] = NotifyBoxcar.unquote(
results['qsd']['secret'].strip())
# Include images with our message
results['include_image'] = \
parse_bool(results['qsd'].get('image', True))

View file

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
# BSD 3-Clause License
# BSD 2-Clause License
#
# Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
# 3. Neither the name of the copyright holder nor the names of its
# contributors may be used to endorse or promote products derived from
# this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
@ -121,11 +117,13 @@ class NotifyBulkSMS(NotifyBase):
'user': {
'name': _('User Name'),
'type': 'string',
'required': True,
},
'password': {
'name': _('Password'),
'type': 'string',
'private': True,
'required': True,
},
'target_phone': {
'name': _('Target Phone No'),
@ -144,6 +142,7 @@ class NotifyBulkSMS(NotifyBase):
'targets': {
'name': _('Targets'),
'type': 'list:string',
'required': True,
},
})
@ -414,6 +413,24 @@ class NotifyBulkSMS(NotifyBase):
for x in self.groups])),
params=NotifyBulkSMS.urlencode(params))
def __len__(self):
"""
Returns the number of targets associated with this notification
"""
#
# Factor batch into calculation
#
# Note: Groups always require a separate request (and can not be
# included in batch calculations)
batch_size = 1 if not self.batch else self.default_batch_size
targets = len(self.targets)
if batch_size > 1:
targets = int(targets / batch_size) + \
(1 if targets % batch_size else 0)
return targets + len(self.groups)
@staticmethod
def parse_url(url):
"""

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 -*-
# BSD 3-Clause License
# BSD 2-Clause License
#
# Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
# 3. Neither the name of the copyright holder nor the names of its
# contributors may be used to endorse or promote products derived from
# this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
@ -288,6 +284,21 @@ class NotifyClickSend(NotifyBase):
params=NotifyClickSend.urlencode(params),
)
def __len__(self):
"""
Returns the number of targets associated with this notification
"""
#
# Factor batch into calculation
#
batch_size = 1 if not self.batch else self.default_batch_size
targets = len(self.targets)
if batch_size > 1:
targets = int(targets / batch_size) + \
(1 if targets % batch_size else 0)
return targets
@staticmethod
def parse_url(url):
"""

View file

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
# BSD 3-Clause License
# BSD 2-Clause License
#
# Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
# 3. Neither the name of the copyright holder nor the names of its
# contributors may be used to endorse or promote products derived from
# this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
@ -114,6 +110,7 @@ class NotifyD7Networks(NotifyBase):
'targets': {
'name': _('Targets'),
'type': 'list:string',
'required': True,
},
})
@ -357,6 +354,15 @@ class NotifyD7Networks(NotifyBase):
[NotifyD7Networks.quote(x, safe='') for x in self.targets]),
params=NotifyD7Networks.urlencode(params))
def __len__(self):
"""
Returns the number of targets associated with this notification
"""
#
# Factor batch into calculation
#
return len(self.targets) if not self.batch else 1
@staticmethod
def parse_url(url):
"""

View file

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
# BSD 3-Clause License
# BSD 2-Clause License
#
# Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
# 3. Neither the name of the copyright holder nor the names of its
# contributors may be used to endorse or promote products derived from
# this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE

View file

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
# BSD 3-Clause License
# BSD 2-Clause License
#
# Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
# 3. Neither the name of the copyright holder nor the names of its
# contributors may be used to endorse or promote products derived from
# this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
@ -350,6 +346,21 @@ class NotifyDapnet(NotifyBase):
params=NotifyDapnet.urlencode(params),
)
def __len__(self):
"""
Returns the number of targets associated with this notification
"""
#
# Factor batch into calculation
#
batch_size = 1 if not self.batch else self.default_batch_size
targets = len(self.targets)
if batch_size > 1:
targets = int(targets / batch_size) + \
(1 if targets % batch_size else 0)
return targets
@staticmethod
def parse_url(url):
"""

View file

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
# BSD 3-Clause License
# BSD 2-Clause License
#
# Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
# 3. Neither the name of the copyright holder nor the names of its
# contributors may be used to endorse or promote products derived from
# this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
@ -103,13 +99,18 @@ class NotifyDingTalk(NotifyBase):
'regex': (r'^[a-z0-9]+$', 'i'),
},
'secret': {
'name': _('Token'),
'name': _('Secret'),
'type': 'string',
'private': True,
'regex': (r'^[a-z0-9]+$', 'i'),
},
'targets': {
'target_phone_no': {
'name': _('Target Phone No'),
'type': 'string',
'map_to': 'targets',
},
'targets': {
'name': _('Targets'),
'type': 'list:string',
},
})
@ -309,6 +310,13 @@ class NotifyDingTalk(NotifyBase):
[NotifyDingTalk.quote(x, safe='') for x in self.targets]),
args=NotifyDingTalk.urlencode(args))
def __len__(self):
"""
Returns the number of targets associated with this notification
"""
targets = len(self.targets)
return targets if targets > 0 else 1
@staticmethod
def parse_url(url):
"""

View file

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
# BSD 3-Clause License
# BSD 2-Clause License
#
# Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
# 3. Neither the name of the copyright holder nor the names of its
# contributors may be used to endorse or promote products derived from
# this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
@ -50,6 +46,9 @@
import re
import requests
from json import dumps
from datetime import timedelta
from datetime import datetime
from datetime import timezone
from .NotifyBase import NotifyBase
from ..common import NotifyImageSize
@ -81,9 +80,23 @@ class NotifyDiscord(NotifyBase):
# Discord Webhook
notify_url = 'https://discord.com/api/webhooks'
# Support attachments
attachment_support = True
# Allows the user to specify the NotifyImageSize object
image_size = NotifyImageSize.XY_256
# Discord is kind enough to return how many more requests we're allowed to
# continue to make within it's header response as:
# X-RateLimit-Reset: The epoc time (in seconds) we can expect our
# rate-limit to be reset.
# X-RateLimit-Remaining: an integer identifying how many requests we're
# still allow to make.
request_rate_per_sec = 0
# Taken right from google.auth.helpers:
clock_skew = timedelta(seconds=10)
# The maximum allowable characters allowed in the body per message
body_maxlen = 2000
@ -135,6 +148,13 @@ class NotifyDiscord(NotifyBase):
'name': _('Avatar URL'),
'type': 'string',
},
'href': {
'name': _('URL'),
'type': 'string',
},
'url': {
'alias_of': 'href',
},
# Send a message to the specified thread within a webhook's channel.
# The thread will automatically be unarchived.
'thread': {
@ -166,7 +186,8 @@ class NotifyDiscord(NotifyBase):
def __init__(self, webhook_id, webhook_token, tts=False, avatar=True,
footer=False, footer_logo=True, include_image=False,
fields=True, avatar_url=None, thread=None, **kwargs):
fields=True, avatar_url=None, href=None, thread=None,
**kwargs):
"""
Initialize Discord Object
@ -215,6 +236,15 @@ class NotifyDiscord(NotifyBase):
# dynamically generated avatar url images
self.avatar_url = avatar_url
# A URL to have the title link to
self.href = href
# For Tracking Purposes
self.ratelimit_reset = datetime.now(timezone.utc).replace(tzinfo=None)
# Default to 1.0
self.ratelimit_remaining = 1.0
return
def send(self, body, title='', notify_type=NotifyType.INFO, attach=None,
@ -235,64 +265,6 @@ class NotifyDiscord(NotifyBase):
# Acquire image_url
image_url = self.image_url(notify_type)
# our fields variable
fields = []
if self.notify_format == NotifyFormat.MARKDOWN:
# Use embeds for payload
payload['embeds'] = [{
'author': {
'name': self.app_id,
'url': self.app_url,
},
'title': title,
'description': body,
# Our color associated with our notification
'color': self.color(notify_type, int),
}]
if self.footer:
# Acquire logo URL
logo_url = self.image_url(notify_type, logo=True)
# Set Footer text to our app description
payload['embeds'][0]['footer'] = {
'text': self.app_desc,
}
if self.footer_logo and logo_url:
payload['embeds'][0]['footer']['icon_url'] = logo_url
if self.include_image and image_url:
payload['embeds'][0]['thumbnail'] = {
'url': image_url,
'height': 256,
'width': 256,
}
if self.fields:
# Break titles out so that we can sort them in embeds
description, fields = self.extract_markdown_sections(body)
# Swap first entry for description
payload['embeds'][0]['description'] = description
if fields:
# Apply our additional parsing for a better presentation
payload['embeds'][0]['fields'] = \
fields[:self.discord_max_fields]
# Remove entry from head of fields
fields = fields[self.discord_max_fields:]
else:
# not markdown
payload['content'] = \
body if not title else "{}\r\n{}".format(title, body)
if self.thread_id:
payload['thread_id'] = self.thread_id
if self.avatar and (image_url or self.avatar_url):
payload['avatar_url'] = \
self.avatar_url if self.avatar_url else image_url
@ -301,21 +273,84 @@ class NotifyDiscord(NotifyBase):
# Optionally override the default username of the webhook
payload['username'] = self.user
if not self._send(payload):
# We failed to post our message
return False
# Associate our thread_id with our message
params = {'thread_id': self.thread_id} if self.thread_id else None
# Process any remaining fields IF set
if fields:
payload['embeds'][0]['description'] = ''
for i in range(0, len(fields), self.discord_max_fields):
payload['embeds'][0]['fields'] = \
fields[i:i + self.discord_max_fields]
if not self._send(payload):
# We failed to post our message
return False
if body:
# our fields variable
fields = []
if attach:
if self.notify_format == NotifyFormat.MARKDOWN:
# Use embeds for payload
payload['embeds'] = [{
'author': {
'name': self.app_id,
'url': self.app_url,
},
'title': title,
'description': body,
# Our color associated with our notification
'color': self.color(notify_type, int),
}]
if self.href:
payload['embeds'][0]['url'] = self.href
if self.footer:
# Acquire logo URL
logo_url = self.image_url(notify_type, logo=True)
# Set Footer text to our app description
payload['embeds'][0]['footer'] = {
'text': self.app_desc,
}
if self.footer_logo and logo_url:
payload['embeds'][0]['footer']['icon_url'] = logo_url
if self.include_image and image_url:
payload['embeds'][0]['thumbnail'] = {
'url': image_url,
'height': 256,
'width': 256,
}
if self.fields:
# Break titles out so that we can sort them in embeds
description, fields = self.extract_markdown_sections(body)
# Swap first entry for description
payload['embeds'][0]['description'] = description
if fields:
# Apply our additional parsing for a better
# presentation
payload['embeds'][0]['fields'] = \
fields[:self.discord_max_fields]
# Remove entry from head of fields
fields = fields[self.discord_max_fields:]
else:
# not markdown
payload['content'] = \
body if not title else "{}\r\n{}".format(title, body)
if not self._send(payload, params=params):
# We failed to post our message
return False
# Process any remaining fields IF set
if fields:
payload['embeds'][0]['description'] = ''
for i in range(0, len(fields), self.discord_max_fields):
payload['embeds'][0]['fields'] = \
fields[i:i + self.discord_max_fields]
if not self._send(payload):
# We failed to post our message
return False
if attach and self.attachment_support:
# Update our payload; the idea is to preserve it's other detected
# and assigned values for re-use here too
payload.update({
@ -338,14 +373,15 @@ class NotifyDiscord(NotifyBase):
for attachment in attach:
self.logger.info(
'Posting Discord Attachment {}'.format(attachment.name))
if not self._send(payload, attach=attachment):
if not self._send(payload, params=params, attach=attachment):
# We failed to post our message
return False
# Otherwise return
return True
def _send(self, payload, attach=None, **kwargs):
def _send(self, payload, attach=None, params=None, rate_limit=1,
**kwargs):
"""
Wrapper to the requests (post) object
"""
@ -367,8 +403,25 @@ class NotifyDiscord(NotifyBase):
))
self.logger.debug('Discord Payload: %s' % str(payload))
# Always call throttle before any remote server i/o is made
self.throttle()
# By default set wait to None
wait = None
if self.ratelimit_remaining <= 0.0:
# Determine how long we should wait for or if we should wait at
# all. This isn't fool-proof because we can't be sure the client
# time (calling this script) is completely synced up with the
# Discord server. One would hope we're on NTP and our clocks are
# the same allowing this to role smoothly:
now = datetime.now(timezone.utc).replace(tzinfo=None)
if now < self.ratelimit_reset:
# We need to throttle for the difference in seconds
wait = abs(
(self.ratelimit_reset - now + self.clock_skew)
.total_seconds())
# Always call throttle before any remote server i/o is made;
self.throttle(wait=wait)
# Perform some simple error checking
if isinstance(attach, AttachBase):
@ -396,12 +449,29 @@ class NotifyDiscord(NotifyBase):
r = requests.post(
notify_url,
params=params,
data=payload if files else dumps(payload),
headers=headers,
files=files,
verify=self.verify_certificate,
timeout=self.request_timeout,
)
# Handle rate limiting (if specified)
try:
# Store our rate limiting (if provided)
self.ratelimit_remaining = \
float(r.headers.get(
'X-RateLimit-Remaining'))
self.ratelimit_reset = datetime.fromtimestamp(
int(r.headers.get('X-RateLimit-Reset')),
timezone.utc).replace(tzinfo=None)
except (TypeError, ValueError):
# This is returned if we could not retrieve this
# information gracefully accept this state and move on
pass
if r.status_code not in (
requests.codes.ok, requests.codes.no_content):
@ -409,6 +479,20 @@ class NotifyDiscord(NotifyBase):
status_str = \
NotifyBase.http_response_code_lookup(r.status_code)
if r.status_code == requests.codes.too_many_requests \
and rate_limit > 0:
# handle rate limiting
self.logger.warning(
'Discord rate limiting in effect; '
'blocking for %.2f second(s)',
self.ratelimit_remaining)
# Try one more time before failing
return self._send(
payload=payload, attach=attach, params=params,
rate_limit=rate_limit - 1, **kwargs)
self.logger.warning(
'Failed to send {}to Discord notification: '
'{}{}error={}.'.format(
@ -466,6 +550,9 @@ class NotifyDiscord(NotifyBase):
if self.avatar_url:
params['avatar_url'] = self.avatar_url
if self.href:
params['href'] = self.href
if self.thread_id:
params['thread'] = self.thread_id
@ -537,10 +624,23 @@ class NotifyDiscord(NotifyBase):
results['avatar_url'] = \
NotifyDiscord.unquote(results['qsd']['avatar_url'])
# Extract url if it was specified
if 'href' in results['qsd']:
results['href'] = \
NotifyDiscord.unquote(results['qsd']['href'])
elif 'url' in results['qsd']:
results['href'] = \
NotifyDiscord.unquote(results['qsd']['url'])
# Markdown is implied
results['format'] = NotifyFormat.MARKDOWN
# Extract thread id if it was specified
if 'thread' in results['qsd']:
results['thread'] = \
NotifyDiscord.unquote(results['qsd']['thread'])
# Markdown is implied
results['format'] = NotifyFormat.MARKDOWN
return results

View file

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

View file

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
# BSD 3-Clause License
# BSD 2-Clause License
#
# Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
# 3. Neither the name of the copyright holder nor the names of its
# contributors may be used to endorse or promote products derived from
# this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE

View file

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
# BSD 3-Clause License
# BSD 2-Clause License
#
# Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
# 3. Neither the name of the copyright holder nor the names of its
# contributors may be used to endorse or promote products derived from
# this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE

View file

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

View file

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
# BSD 3-Clause License
# BSD 2-Clause License
#
# Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
# 3. Neither the name of the copyright holder nor the names of its
# contributors may be used to endorse or promote products derived from
# this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE

View file

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
# BSD 3-Clause License
# BSD 2-Clause License
#
# Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
# 3. Neither the name of the copyright holder nor the names of its
# contributors may be used to endorse or promote products derived from
# this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE

View file

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

View file

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
# BSD 3-Clause License
# BSD 2-Clause License
#
# Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
# 3. Neither the name of the copyright holder nor the names of its
# contributors may be used to endorse or promote products derived from
# this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE

View file

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
# BSD 3-Clause License
# BSD 2-Clause License
#
# Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
# 3. Neither the name of the copyright holder nor the names of its
# contributors may be used to endorse or promote products derived from
# this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE

View file

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
# BSD 3-Clause License
# BSD 2-Clause License
#
# Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
# 3. Neither the name of the copyright holder nor the names of its
# contributors may be used to endorse or promote products derived from
# this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
@ -97,8 +93,8 @@ class NotifyFlock(NotifyBase):
# Define object templates
templates = (
'{schema}://{token}',
'{schema}://{user}@{token}',
'{schema}://{user}@{token}/{targets}',
'{schema}://{botname}@{token}',
'{schema}://{botname}@{token}/{targets}',
'{schema}://{token}/{targets}',
)
@ -111,9 +107,10 @@ class NotifyFlock(NotifyBase):
'private': True,
'required': True,
},
'user': {
'botname': {
'name': _('Bot Name'),
'type': 'string',
'map_to': 'user',
},
'to_user': {
'name': _('To User ID'),
@ -334,6 +331,13 @@ class NotifyFlock(NotifyBase):
params=NotifyFlock.urlencode(params),
)
def __len__(self):
"""
Returns the number of targets associated with this notification
"""
targets = len(self.targets)
return targets if targets > 0 else 1
@staticmethod
def parse_url(url):
"""

View file

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
# BSD 3-Clause License
# BSD 2-Clause License
#
# Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
# 3. Neither the name of the copyright holder nor the names of its
# contributors may be used to endorse or promote products derived from
# this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
@ -40,6 +36,16 @@ from ..common import NotifyType
from ..AppriseLocale import gettext_lazy as _
class FORMPayloadField:
"""
Identifies the fields available in the FORM Payload
"""
VERSION = 'version'
TITLE = 'title'
MESSAGE = 'message'
MESSAGETYPE = 'type'
# Defines the method to send the notification
METHODS = (
'POST',
@ -89,6 +95,9 @@ class NotifyForm(NotifyBase):
# A URL that takes you to the setup/help of the specific protocol
setup_url = 'https://github.com/caronc/apprise/wiki/Notify_Custom_Form'
# Support attachments
attachment_support = True
# Allows the user to specify the NotifyImageSize object
image_size = NotifyImageSize.XY_128
@ -96,6 +105,12 @@ class NotifyForm(NotifyBase):
# local anyway
request_rate_per_sec = 0
# Define the FORM version to place in all payloads
# Version: Major.Minor, Major is only updated if the entire schema is
# changed. If just adding new items (or removing old ones, only increment
# the Minor!
form_version = '1.0'
# Define object templates
templates = (
'{schema}://{host}',
@ -218,6 +233,18 @@ class NotifyForm(NotifyBase):
self.attach_as += self.attach_as_count
self.attach_multi_support = True
# A payload map allows users to over-ride the default mapping if
# they're detected with the :overide=value. Normally this would
# create a new key and assign it the value specified. However
# if the key you specify is actually an internally mapped one,
# then a re-mapping takes place using the value
self.payload_map = {
FORMPayloadField.VERSION: FORMPayloadField.VERSION,
FORMPayloadField.TITLE: FORMPayloadField.TITLE,
FORMPayloadField.MESSAGE: FORMPayloadField.MESSAGE,
FORMPayloadField.MESSAGETYPE: FORMPayloadField.MESSAGETYPE,
}
self.params = {}
if params:
# Store our extra headers
@ -228,10 +255,20 @@ class NotifyForm(NotifyBase):
# Store our extra headers
self.headers.update(headers)
self.payload_overrides = {}
self.payload_extras = {}
if payload:
# Store our extra payload entries
self.payload_extras.update(payload)
for key in list(self.payload_extras.keys()):
# Any values set in the payload to alter a system related one
# alters the system key. Hence :message=msg maps the 'message'
# variable that otherwise already contains the payload to be
# 'msg' instead (containing the payload)
if key in self.payload_map:
self.payload_map[key] = self.payload_extras[key]
self.payload_overrides[key] = self.payload_extras[key]
del self.payload_extras[key]
return
@ -257,6 +294,8 @@ class NotifyForm(NotifyBase):
# Append our payload extra's into our parameters
params.update(
{':{}'.format(k): v for k, v in self.payload_extras.items()})
params.update(
{':{}'.format(k): v for k, v in self.payload_overrides.items()})
if self.attach_as != self.attach_as_default:
# Provide Attach-As extension details
@ -305,7 +344,7 @@ class NotifyForm(NotifyBase):
# Track our potential attachments
files = []
if attach:
if attach and self.attachment_support:
for no, attachment in enumerate(attach, start=1):
# Perform some simple error checking
if not attachment:
@ -337,15 +376,18 @@ class NotifyForm(NotifyBase):
'form:// Multi-Attachment Support not enabled')
# prepare Form Object
payload = {
# Version: Major.Minor, Major is only updated if the entire
# schema is changed. If just adding new items (or removing
# old ones, only increment the Minor!
'version': '1.0',
'title': title,
'message': body,
'type': notify_type,
}
payload = {}
for key, value in (
(FORMPayloadField.VERSION, self.form_version),
(FORMPayloadField.TITLE, title),
(FORMPayloadField.MESSAGE, body),
(FORMPayloadField.MESSAGETYPE, notify_type)):
if not self.payload_map[key]:
# Do not store element in payload response
continue
payload[self.payload_map[key]] = value
# Apply any/all payload over-rides defined
payload.update(self.payload_extras)

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 -*-
# BSD 3-Clause License
# BSD 2-Clause License
#
# Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
# 3. Neither the name of the copyright holder nor the names of its
# contributors may be used to endorse or promote products derived from
# this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE

View file

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
# BSD 3-Clause License
# BSD 2-Clause License
#
# Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
# 3. Neither the name of the copyright holder nor the names of its
# contributors may be used to endorse or promote products derived from
# this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE

View file

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
# BSD 3-Clause License
# BSD 2-Clause License
#
# Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
# 3. Neither the name of the copyright holder nor the names of its
# contributors may be used to endorse or promote products derived from
# this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
@ -134,7 +130,6 @@ class NotifyGotify(NotifyBase):
'type': 'string',
'map_to': 'fullpath',
'default': '/',
'required': True,
},
'port': {
'name': _('Port'),

View file

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
# BSD 3-Clause License
# BSD 2-Clause License
#
# Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
# 3. Neither the name of the copyright holder nor the names of its
# contributors may be used to endorse or promote products derived from
# this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE

View file

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
# BSD 3-Clause License
# BSD 2-Clause License
#
# Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
# 3. Neither the name of the copyright holder nor the names of its
# contributors may be used to endorse or promote products derived from
# this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE

View file

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
# BSD 3-Clause License
# BSD 2-Clause License
#
# Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
# 3. Neither the name of the copyright holder nor the names of its
# contributors may be used to endorse or promote products derived from
# this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE

View file

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
# BSD 3-Clause License
# BSD 2-Clause License
#
# Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
# 3. Neither the name of the copyright holder nor the names of its
# contributors may be used to endorse or promote products derived from
# this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
@ -30,7 +26,6 @@
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
#
# For this plugin to work, you need to add the Maker applet to your profile
# Simply visit https://ifttt.com/search and search for 'Webhooks'
# Or if you're signed in, click here: https://ifttt.com/maker_webhooks
@ -312,6 +307,12 @@ class NotifyIFTTT(NotifyBase):
params=NotifyIFTTT.urlencode(params),
)
def __len__(self):
"""
Returns the number of targets associated with this notification
"""
return len(self.events)
@staticmethod
def parse_url(url):
"""

View file

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

View file

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

View file

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
# BSD 3-Clause License
# BSD 2-Clause License
#
# Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
# 3. Neither the name of the copyright holder nor the names of its
# contributors may be used to endorse or promote products derived from
# this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
@ -324,6 +320,12 @@ class NotifyKavenegar(NotifyBase):
[NotifyKavenegar.quote(x, safe='') for x in self.targets]),
params=NotifyKavenegar.urlencode(params))
def __len__(self):
"""
Returns the number of targets associated with this notification
"""
return len(self.targets)
@staticmethod
def parse_url(url):
"""

View file

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
# BSD 3-Clause License
# BSD 2-Clause License
#
# Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
# 3. Neither the name of the copyright holder nor the names of its
# contributors may be used to endorse or promote products derived from
# this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE

View file

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

View file

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

View file

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

View file

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
# BSD 3-Clause License
# BSD 2-Clause License
#
# Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
# 3. Neither the name of the copyright holder nor the names of its
# contributors may be used to endorse or promote products derived from
# this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
@ -35,50 +31,31 @@
# Get your (authkey) from the dashboard here:
# - https://world.msg91.com/user/index.php#api
#
# Note: You will need to define a template for this to work
#
# Get details on the API used in this plugin here:
# - https://world.msg91.com/apidoc/textsms/send-sms.php
# - https://docs.msg91.com/reference/send-sms
import re
import requests
from json import dumps
from .NotifyBase import NotifyBase
from ..common import NotifyType
from ..utils import is_phone_no
from ..utils import parse_phone_no
from ..utils import parse_phone_no, parse_bool
from ..utils import validate_regex
from ..AppriseLocale import gettext_lazy as _
class MSG91Route:
class MSG91PayloadField:
"""
Transactional SMS Routes
route=1 for promotional, route=4 for transactional SMS.
Identifies the fields available in the JSON Payload
"""
PROMOTIONAL = 1
TRANSACTIONAL = 4
BODY = 'body'
MESSAGETYPE = 'type'
# Used for verification
MSG91_ROUTES = (
MSG91Route.PROMOTIONAL,
MSG91Route.TRANSACTIONAL,
)
class MSG91Country:
"""
Optional value that can be specified on the MSG91 api
"""
INTERNATIONAL = 0
USA = 1
INDIA = 91
# Used for verification
MSG91_COUNTRIES = (
MSG91Country.INTERNATIONAL,
MSG91Country.USA,
MSG91Country.INDIA,
)
# Add entries here that are reserved
RESERVED_KEYWORDS = ('mobiles', )
class NotifyMSG91(NotifyBase):
@ -99,7 +76,7 @@ class NotifyMSG91(NotifyBase):
setup_url = 'https://github.com/caronc/apprise/wiki/Notify_msg91'
# MSG91 uses the http protocol with JSON requests
notify_url = 'https://world.msg91.com/api/sendhttp.php'
notify_url = 'https://control.msg91.com/api/v5/flow/'
# The maximum length of the body
body_maxlen = 160
@ -108,14 +85,24 @@ class NotifyMSG91(NotifyBase):
# cause any title (if defined) to get placed into the message body.
title_maxlen = 0
# Our supported mappings and component keys
component_key_re = re.compile(
r'(?P<key>((?P<id>[a-z0-9_-])?|(?P<map>body|type)))', re.IGNORECASE)
# Define object templates
templates = (
'{schema}://{authkey}/{targets}',
'{schema}://{sender}@{authkey}/{targets}',
'{schema}://{template}@{authkey}/{targets}',
)
# Define our template tokens
template_tokens = dict(NotifyBase.template_tokens, **{
'template': {
'name': _('Template ID'),
'type': 'string',
'required': True,
'private': True,
'regex': (r'^[a-z0-9 _-]+$', 'i'),
},
'authkey': {
'name': _('Authentication Key'),
'type': 'string',
@ -133,10 +120,7 @@ class NotifyMSG91(NotifyBase):
'targets': {
'name': _('Targets'),
'type': 'list:string',
},
'sender': {
'name': _('Sender ID'),
'type': 'string',
'required': True,
},
})
@ -145,21 +129,23 @@ class NotifyMSG91(NotifyBase):
'to': {
'alias_of': 'targets',
},
'route': {
'name': _('Route'),
'type': 'choice:int',
'values': MSG91_ROUTES,
'default': MSG91Route.TRANSACTIONAL,
},
'country': {
'name': _('Country'),
'type': 'choice:int',
'values': MSG91_COUNTRIES,
'short_url': {
'name': _('Short URL'),
'type': 'bool',
'default': False,
},
})
def __init__(self, authkey, targets=None, sender=None, route=None,
country=None, **kwargs):
# Define any kwargs we're using
template_kwargs = {
'template_mapping': {
'name': _('Template Mapping'),
'prefix': ':',
},
}
def __init__(self, template, authkey, targets=None, short_url=None,
template_mapping=None, **kwargs):
"""
Initialize MSG91 Object
"""
@ -174,39 +160,20 @@ class NotifyMSG91(NotifyBase):
self.logger.warning(msg)
raise TypeError(msg)
if route is None:
self.route = self.template_args['route']['default']
# Template ID
self.template = validate_regex(
template, *self.template_tokens['template']['regex'])
if not self.template:
msg = 'An invalid MSG91 Template ID ' \
'({}) was specified.'.format(template)
self.logger.warning(msg)
raise TypeError(msg)
if short_url is None:
self.short_url = self.template_args['short_url']['default']
else:
try:
self.route = int(route)
if self.route not in MSG91_ROUTES:
# Let outer except catch thi
raise ValueError()
except (ValueError, TypeError):
msg = 'The MSG91 route specified ({}) is invalid.'\
.format(route)
self.logger.warning(msg)
raise TypeError(msg)
if country:
try:
self.country = int(country)
if self.country not in MSG91_COUNTRIES:
# Let outer except catch thi
raise ValueError()
except (ValueError, TypeError):
msg = 'The MSG91 country specified ({}) is invalid.'\
.format(country)
self.logger.warning(msg)
raise TypeError(msg)
else:
self.country = country
# Store our sender
self.sender = sender
self.short_url = parse_bool(short_url)
# Parse our targets
self.targets = list()
@ -224,6 +191,11 @@ class NotifyMSG91(NotifyBase):
# store valid phone number
self.targets.append(result['full'])
self.template_mapping = {}
if template_mapping:
# Store our extra payload entries
self.template_mapping.update(template_mapping)
return
def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs):
@ -239,23 +211,55 @@ class NotifyMSG91(NotifyBase):
# Prepare our headers
headers = {
'User-Agent': self.app_id,
'Content-Type': 'application/x-www-form-urlencoded',
'Content-Type': 'application/json',
'authkey': self.authkey,
}
# Base
recipient_payload = {
'mobiles': None,
# Keyword Tokens
MSG91PayloadField.BODY: body,
MSG91PayloadField.MESSAGETYPE: notify_type,
}
# Prepare Recipient Payload Object
for key, value in self.template_mapping.items():
if key in RESERVED_KEYWORDS:
self.logger.warning(
'Ignoring MSG91 custom payload entry %s', key)
continue
if key in recipient_payload:
if not value:
# Do not store element in payload response
del recipient_payload[key]
else:
# Re-map
recipient_payload[value] = recipient_payload[key]
del recipient_payload[key]
else:
# Append entry
recipient_payload[key] = value
# Prepare our recipients
recipients = []
for target in self.targets:
recipient = recipient_payload.copy()
recipient['mobiles'] = target
recipients.append(recipient)
# Prepare our payload
payload = {
'sender': self.sender if self.sender else self.app_id,
'authkey': self.authkey,
'message': body,
'response': 'json',
'template_id': self.template,
'short_url': 1 if self.short_url else 0,
# target phone numbers are sent with a comma delimiter
'mobiles': ','.join(self.targets),
'route': str(self.route),
'recipients': recipients,
}
if self.country:
payload['country'] = str(self.country)
# Some Debug Logging
self.logger.debug('MSG91 POST URL: {} (cert_verify={})'.format(
self.notify_url, self.verify_certificate))
@ -267,7 +271,7 @@ class NotifyMSG91(NotifyBase):
try:
r = requests.post(
self.notify_url,
data=payload,
data=dumps(payload),
headers=headers,
verify=self.verify_certificate,
timeout=self.request_timeout,
@ -313,22 +317,32 @@ class NotifyMSG91(NotifyBase):
# Define any URL parameters
params = {
'route': str(self.route),
'short_url': str(self.short_url),
}
# Extend our parameters
params.update(self.url_parameters(privacy=privacy, *args, **kwargs))
if self.country:
params['country'] = str(self.country)
# Payload body extras prefixed with a ':' sign
# Append our payload extras into our parameters
params.update(
{':{}'.format(k): v for k, v in self.template_mapping.items()})
return '{schema}://{authkey}/{targets}/?{params}'.format(
return '{schema}://{template}@{authkey}/{targets}/?{params}'.format(
schema=self.secure_protocol,
template=self.pprint(self.template, privacy, safe=''),
authkey=self.pprint(self.authkey, privacy, safe=''),
targets='/'.join(
[NotifyMSG91.quote(x, safe='') for x in self.targets]),
params=NotifyMSG91.urlencode(params))
def __len__(self):
"""
Returns the number of targets associated with this notification
"""
targets = len(self.targets)
return targets if targets > 0 else 1
@staticmethod
def parse_url(url):
"""
@ -349,11 +363,11 @@ class NotifyMSG91(NotifyBase):
# The hostname is our authentication key
results['authkey'] = NotifyMSG91.unquote(results['host'])
if 'route' in results['qsd'] and len(results['qsd']['route']):
results['route'] = results['qsd']['route']
# The template id is kept in the user field
results['template'] = NotifyMSG91.unquote(results['user'])
if 'country' in results['qsd'] and len(results['qsd']['country']):
results['country'] = results['qsd']['country']
if 'short_url' in results['qsd'] and len(results['qsd']['short_url']):
results['short_url'] = parse_bool(results['qsd']['short_url'])
# Support the 'to' variable so that we can support targets this way too
# The 'to' makes it easier to use yaml configuration
@ -361,4 +375,10 @@ class NotifyMSG91(NotifyBase):
results['targets'] += \
NotifyMSG91.parse_phone_no(results['qsd']['to'])
# store any additional payload extra's defined
results['template_mapping'] = {
NotifyMSG91.unquote(x): NotifyMSG91.unquote(y)
for x, y in results['qsd:'].items()
}
return results

View file

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
# BSD 3-Clause License
# BSD 2-Clause License
#
# Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
# 3. Neither the name of the copyright holder nor the names of its
# contributors may be used to endorse or promote products derived from
# this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE

View file

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
# BSD 3-Clause License
# BSD 2-Clause License
#
# Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
# 3. Neither the name of the copyright holder nor the names of its
# contributors may be used to endorse or promote products derived from
# this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
@ -197,8 +193,7 @@ class NotifyMacOSX(NotifyBase):
self.logger.debug('MacOSX CMD: {}'.format(' '.join(cmd)))
# Send our notification
output = subprocess.Popen(
cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
output = subprocess.Popen(cmd)
# Wait for process to complete
output.wait()

View file

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
# BSD 3-Clause License
# BSD 2-Clause License
#
# Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
# 3. Neither the name of the copyright holder nor the names of its
# contributors may be used to endorse or promote products derived from
# this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
@ -121,6 +117,9 @@ class NotifyMailgun(NotifyBase):
# A URL that takes you to the setup/help of the specific protocol
setup_url = 'https://github.com/caronc/apprise/wiki/Notify_mailgun'
# Support attachments
attachment_support = True
# Default Notify Format
notify_format = NotifyFormat.HTML
@ -152,8 +151,13 @@ class NotifyMailgun(NotifyBase):
'private': True,
'required': True,
},
'target_email': {
'name': _('Target Email'),
'type': 'string',
'map_to': 'targets',
},
'targets': {
'name': _('Target Emails'),
'name': _('Targets'),
'type': 'list:string',
},
})
@ -366,7 +370,7 @@ class NotifyMailgun(NotifyBase):
# Track our potential files
files = {}
if attach:
if attach and self.attachment_support:
for idx, attachment in enumerate(attach):
# Perform some simple error checking
if not attachment:
@ -627,6 +631,20 @@ class NotifyMailgun(NotifyBase):
safe='') for e in self.targets]),
params=NotifyMailgun.urlencode(params))
def __len__(self):
"""
Returns the number of targets associated with this notification
"""
#
# Factor batch into calculation
#
batch_size = 1 if not self.batch else self.default_batch_size
targets = len(self.targets)
if batch_size > 1:
targets = int(targets / batch_size) + \
(1 if targets % batch_size else 0)
return targets if targets > 0 else 1
@staticmethod
def parse_url(url):
"""

View file

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

View file

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
# BSD 3-Clause License
# BSD 2-Clause License
#
# Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
# 3. Neither the name of the copyright holder nor the names of its
# contributors may be used to endorse or promote products derived from
# this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
@ -53,8 +49,11 @@ from ..utils import validate_regex
from ..AppriseLocale import gettext_lazy as _
# Define default path
MATRIX_V2_API_PATH = '/_matrix/client/r0'
MATRIX_V1_WEBHOOK_PATH = '/api/v1/matrix/hook'
MATRIX_V2_API_PATH = '/_matrix/client/r0'
MATRIX_V3_API_PATH = '/_matrix/client/v3'
MATRIX_V3_MEDIA_PATH = '/_matrix/media/v3'
MATRIX_V2_MEDIA_PATH = '/_matrix/media/r0'
# Extend HTTP Error Messages
MATRIX_HTTP_ERROR_MAP = {
@ -88,6 +87,21 @@ MATRIX_MESSAGE_TYPES = (
)
class MatrixVersion:
# Version 2
V2 = "2"
# Version 3
V3 = "3"
# webhook modes are placed into this list for validation purposes
MATRIX_VERSIONS = (
MatrixVersion.V2,
MatrixVersion.V3,
)
class MatrixWebhookMode:
# Webhook Mode is disabled
DISABLED = "off"
@ -128,6 +142,9 @@ class NotifyMatrix(NotifyBase):
# The default secure protocol
secure_protocol = 'matrixs'
# Support Attachments
attachment_support = True
# A URL that takes you to the setup/help of the specific protocol
setup_url = 'https://github.com/caronc/apprise/wiki/Notify_matrix'
@ -147,6 +164,9 @@ class NotifyMatrix(NotifyBase):
# Throttle a wee-bit to avoid thrashing
request_rate_per_sec = 0.5
# Our Matrix API Version
matrix_api_version = '3'
# How many retry attempts we'll make in the event the server asks us to
# throttle back.
default_retries = 2
@ -175,7 +195,6 @@ class NotifyMatrix(NotifyBase):
'host': {
'name': _('Hostname'),
'type': 'string',
'required': True,
},
'port': {
'name': _('Port'),
@ -194,6 +213,7 @@ class NotifyMatrix(NotifyBase):
},
'token': {
'name': _('Access Token'),
'private': True,
'map_to': 'password',
},
'target_user': {
@ -234,6 +254,12 @@ class NotifyMatrix(NotifyBase):
'values': MATRIX_WEBHOOK_MODES,
'default': MatrixWebhookMode.DISABLED,
},
'version': {
'name': _('Matrix API Verion'),
'type': 'choice:string',
'values': MATRIX_VERSIONS,
'default': MatrixVersion.V3,
},
'msgtype': {
'name': _('Message Type'),
'type': 'choice:string',
@ -248,7 +274,7 @@ class NotifyMatrix(NotifyBase):
},
})
def __init__(self, targets=None, mode=None, msgtype=None,
def __init__(self, targets=None, mode=None, msgtype=None, version=None,
include_image=False, **kwargs):
"""
Initialize Matrix Object
@ -282,6 +308,14 @@ class NotifyMatrix(NotifyBase):
self.logger.warning(msg)
raise TypeError(msg)
# Setup our version
self.version = self.template_args['version']['default'] \
if not isinstance(version, str) else version
if self.version not in MATRIX_VERSIONS:
msg = 'The version specified ({}) is invalid.'.format(version)
self.logger.warning(msg)
raise TypeError(msg)
# Setup our message type
self.msgtype = self.template_args['msgtype']['default'] \
if not isinstance(msgtype, str) else msgtype.lower()
@ -521,7 +555,8 @@ class NotifyMatrix(NotifyBase):
return payload
def _send_server_notification(self, body, title='',
notify_type=NotifyType.INFO, **kwargs):
notify_type=NotifyType.INFO, attach=None,
**kwargs):
"""
Perform Direct Matrix Server Notification (no webhook)
"""
@ -548,6 +583,13 @@ class NotifyMatrix(NotifyBase):
# Initiaize our error tracking
has_error = False
attachments = None
if attach and self.attachment_support:
attachments = self._send_attachments(attach)
if attachments is False:
# take an early exit
return False
while len(rooms) > 0:
# Get our room
@ -568,23 +610,47 @@ class NotifyMatrix(NotifyBase):
image_url = None if not self.include_image else \
self.image_url(notify_type)
if image_url:
# Define our payload
image_payload = {
'msgtype': 'm.image',
'url': image_url,
'body': '{}'.format(notify_type if not title else title),
}
# Build our path
# Build our path
if self.version == MatrixVersion.V3:
path = '/rooms/{}/send/m.room.message/0'.format(
NotifyMatrix.quote(room_id))
else:
path = '/rooms/{}/send/m.room.message'.format(
NotifyMatrix.quote(room_id))
# Post our content
postokay, response = self._fetch(path, payload=image_payload)
if not postokay:
# Mark our failure
has_error = True
continue
if self.version == MatrixVersion.V2:
#
# Attachments don't work beyond V2 at this time
#
if image_url:
# Define our payload
image_payload = {
'msgtype': 'm.image',
'url': image_url,
'body': '{}'.format(
notify_type if not title else title),
}
# Post our content
postokay, response = self._fetch(
path, payload=image_payload)
if not postokay:
# Mark our failure
has_error = True
continue
if attachments:
for attachment in attachments:
attachment['room_id'] = room_id
attachment['type'] = 'm.room.message'
postokay, response = self._fetch(
path, payload=attachment)
if not postokay:
# Mark our failure
has_error = True
continue
# Define our payload
payload = {
@ -615,12 +681,10 @@ class NotifyMatrix(NotifyBase):
)
})
# Build our path
path = '/rooms/{}/send/m.room.message'.format(
NotifyMatrix.quote(room_id))
# Post our content
postokay, response = self._fetch(path, payload=payload)
method = 'PUT' if self.version == MatrixVersion.V3 else 'POST'
postokay, response = self._fetch(
path, payload=payload, method=method)
if not postokay:
# Notify our user
self.logger.warning(
@ -632,6 +696,62 @@ class NotifyMatrix(NotifyBase):
return not has_error
def _send_attachments(self, attach):
"""
Posts all of the provided attachments
"""
payloads = []
if self.version != MatrixVersion.V2:
self.logger.warning(
'Add ?v=2 to Apprise URL to support Attachments')
return next((False for a in attach if not a), [])
for attachment in attach:
if not attachment:
# invalid attachment (bad file)
return False
if not re.match(r'^image/', attachment.mimetype, re.I):
# unsuppored at this time
continue
postokay, response = \
self._fetch('/upload', attachment=attachment)
if not (postokay and isinstance(response, dict)):
# Failed to perform upload
return False
# If we get here, we'll have a response that looks like:
# {
# "content_uri": "mxc://example.com/a-unique-key"
# }
if self.version == MatrixVersion.V3:
# Prepare our payload
payloads.append({
"body": attachment.name,
"info": {
"mimetype": attachment.mimetype,
"size": len(attachment),
},
"msgtype": "m.image",
"url": response.get('content_uri'),
})
else:
# Prepare our payload
payloads.append({
"info": {
"mimetype": attachment.mimetype,
},
"msgtype": "m.image",
"body": "tta.webp",
"url": response.get('content_uri'),
})
return payloads
def _register(self):
"""
Register with the service if possible.
@ -695,12 +815,23 @@ class NotifyMatrix(NotifyBase):
'user/pass combo is missing.')
return False
# Prepare our Registration Payload
payload = {
'type': 'm.login.password',
'user': self.user,
'password': self.password,
}
# Prepare our Authentication Payload
if self.version == MatrixVersion.V3:
payload = {
'type': 'm.login.password',
'identifier': {
'type': 'm.id.user',
'user': self.user,
},
'password': self.password,
}
else:
payload = {
'type': 'm.login.password',
'user': self.user,
'password': self.password,
}
# Build our URL
postokay, response = self._fetch('/login', payload=payload)
@ -970,7 +1101,8 @@ class NotifyMatrix(NotifyBase):
return None
def _fetch(self, path, payload=None, params=None, method='POST'):
def _fetch(self, path, payload=None, params=None, attachment=None,
method='POST'):
"""
Wrapper to request.post() to manage it's response better and make
the send() function cleaner and easier to maintain.
@ -983,6 +1115,7 @@ class NotifyMatrix(NotifyBase):
headers = {
'User-Agent': self.app_id,
'Content-Type': 'application/json',
'Accept': 'application/json',
}
if self.access_token is not None:
@ -991,19 +1124,39 @@ class NotifyMatrix(NotifyBase):
default_port = 443 if self.secure else 80
url = \
'{schema}://{hostname}:{port}{matrix_api}{path}'.format(
'{schema}://{hostname}{port}'.format(
schema='https' if self.secure else 'http',
hostname=self.host,
port='' if self.port is None
or self.port == default_port else self.port,
matrix_api=MATRIX_V2_API_PATH,
path=path)
or self.port == default_port else f':{self.port}')
if path == '/upload':
if self.version == MatrixVersion.V3:
url += MATRIX_V3_MEDIA_PATH + path
else:
url += MATRIX_V2_MEDIA_PATH + path
params = {'filename': attachment.name}
with open(attachment.path, 'rb') as fp:
payload = fp.read()
# Update our content type
headers['Content-Type'] = attachment.mimetype
else:
if self.version == MatrixVersion.V3:
url += MATRIX_V3_API_PATH + path
else:
url += MATRIX_V2_API_PATH + path
# Our response object
response = {}
# fetch function
fn = requests.post if method == 'POST' else requests.get
fn = requests.post if method == 'POST' else (
requests.put if method == 'PUT' else requests.get)
# Define how many attempts we'll make if we get caught in a throttle
# event
@ -1024,13 +1177,16 @@ class NotifyMatrix(NotifyBase):
try:
r = fn(
url,
data=dumps(payload),
data=dumps(payload) if not attachment else payload,
params=params,
headers=headers,
verify=self.verify_certificate,
timeout=self.request_timeout,
)
self.logger.debug(
'Matrix Response: code=%d, %s' % (
r.status_code, str(r.content)))
response = loads(r.content)
if r.status_code == 429:
@ -1094,6 +1250,13 @@ class NotifyMatrix(NotifyBase):
# Return; we're done
return (False, response)
except (OSError, IOError) as e:
self.logger.warning(
'An I/O error occurred while reading {}.'.format(
attachment.name if attachment else 'unknown file'))
self.logger.debug('I/O Exception: %s' % str(e))
return (False, {})
return (True, response)
# If we get here, we ran out of retries
@ -1160,6 +1323,7 @@ class NotifyMatrix(NotifyBase):
params = {
'image': 'yes' if self.include_image else 'no',
'mode': self.mode,
'version': self.version,
'msgtype': self.msgtype,
}
@ -1196,6 +1360,13 @@ class NotifyMatrix(NotifyBase):
params=NotifyMatrix.urlencode(params),
)
def __len__(self):
"""
Returns the number of targets associated with this notification
"""
targets = len(self.rooms)
return targets if targets > 0 else 1
@staticmethod
def parse_url(url):
"""
@ -1250,6 +1421,14 @@ class NotifyMatrix(NotifyBase):
if 'token' in results['qsd'] and len(results['qsd']['token']):
results['password'] = NotifyMatrix.unquote(results['qsd']['token'])
# Support the use of the version= or v= keyword
if 'version' in results['qsd'] and len(results['qsd']['version']):
results['version'] = \
NotifyMatrix.unquote(results['qsd']['version'])
elif 'v' in results['qsd'] and len(results['qsd']['v']):
results['version'] = NotifyMatrix.unquote(results['qsd']['v'])
return results
@staticmethod
@ -1259,7 +1438,7 @@ class NotifyMatrix(NotifyBase):
"""
result = re.match(
r'^https?://webhooks\.t2bot\.io/api/v1/matrix/hook/'
r'^https?://webhooks\.t2bot\.io/api/v[0-9]+/matrix/hook/'
r'(?P<webhook_token>[A-Z0-9_-]+)/?'
r'(?P<params>\?.+)?$', url, re.I)

View file

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
# BSD 3-Clause License
# BSD 2-Clause License
#
# Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
# 3. Neither the name of the copyright holder nor the names of its
# contributors may be used to endorse or promote products derived from
# this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
@ -91,11 +87,11 @@ class NotifyMattermost(NotifyBase):
# Define object templates
templates = (
'{schema}://{host}/{token}',
'{schema}://{host}/{token}:{port}',
'{schema}://{host}:{port}/{token}',
'{schema}://{host}/{fullpath}/{token}',
'{schema}://{host}:{port}/{fullpath}/{token}',
'{schema}://{botname}@{host}/{token}',
'{schema}://{botname}@{host}:{port}/{token}',
'{schema}://{host}/{fullpath}/{token}',
'{schema}://{host}/{fullpath}{token}:{port}',
'{schema}://{botname}@{host}/{fullpath}/{token}',
'{schema}://{botname}@{host}:{port}/{fullpath}/{token}',
)

View file

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
# BSD 3-Clause License
# BSD 2-Clause License
#
# Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
# 3. Neither the name of the copyright holder nor the names of its
# contributors may be used to endorse or promote products derived from
# this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
@ -311,6 +307,13 @@ class NotifyMessageBird(NotifyBase):
[NotifyMessageBird.quote(x, safe='') for x in self.targets]),
params=NotifyMessageBird.urlencode(params))
def __len__(self):
"""
Returns the number of targets associated with this notification
"""
targets = len(self.targets)
return targets if targets > 0 else 1
@staticmethod
def parse_url(url):
"""

View file

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

View file

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
# BSD 3-Clause License
# BSD 2-Clause License
#
# Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
# 3. Neither the name of the copyright holder nor the names of its
# contributors may be used to endorse or promote products derived from
# this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
@ -67,6 +63,8 @@ class NotifyNextcloud(NotifyBase):
# Define object templates
templates = (
'{schema}://{host}/{targets}',
'{schema}://{host}:{port}/{targets}',
'{schema}://{user}:{password}@{host}/{targets}',
'{schema}://{user}:{password}@{host}:{port}/{targets}',
)
@ -116,6 +114,10 @@ class NotifyNextcloud(NotifyBase):
'min': 1,
'default': 21,
},
'url_prefix': {
'name': _('URL Prefix'),
'type': 'string',
},
'to': {
'alias_of': 'targets',
},
@ -129,17 +131,15 @@ class NotifyNextcloud(NotifyBase):
},
}
def __init__(self, targets=None, version=None, headers=None, **kwargs):
def __init__(self, targets=None, version=None, headers=None,
url_prefix=None, **kwargs):
"""
Initialize Nextcloud Object
"""
super().__init__(**kwargs)
# Store our targets
self.targets = parse_list(targets)
if len(self.targets) == 0:
msg = 'At least one Nextcloud target user must be specified.'
self.logger.warning(msg)
raise TypeError(msg)
self.version = self.template_args['version']['default']
if version is not None:
@ -155,6 +155,10 @@ class NotifyNextcloud(NotifyBase):
self.logger.warning(msg)
raise TypeError(msg)
# Support URL Prefix
self.url_prefix = '' if not url_prefix \
else url_prefix.strip('/')
self.headers = {}
if headers:
# Store our extra headers
@ -167,6 +171,11 @@ class NotifyNextcloud(NotifyBase):
Perform Nextcloud Notification
"""
if len(self.targets) == 0:
# There were no services to notify
self.logger.warning('There were no Nextcloud targets to notify.')
return False
# Prepare our Header
headers = {
'User-Agent': self.app_id,
@ -198,11 +207,11 @@ class NotifyNextcloud(NotifyBase):
auth = (self.user, self.password)
# Nextcloud URL based on version used
notify_url = '{schema}://{host}/ocs/v2.php/'\
notify_url = '{schema}://{host}/{url_prefix}/ocs/v2.php/'\
'apps/admin_notifications/' \
'api/v1/notifications/{target}' \
if self.version < 21 else \
'{schema}://{host}/ocs/v2.php/'\
'{schema}://{host}/{url_prefix}/ocs/v2.php/'\
'apps/notifications/'\
'api/v2/admin_notifications/{target}'
@ -210,6 +219,7 @@ class NotifyNextcloud(NotifyBase):
schema='https' if self.secure else 'http',
host=self.host if not isinstance(self.port, int)
else '{}:{}'.format(self.host, self.port),
url_prefix=self.url_prefix,
target=target,
)
@ -279,6 +289,9 @@ class NotifyNextcloud(NotifyBase):
# Set our version
params['version'] = str(self.version)
if self.url_prefix:
params['url_prefix'] = self.url_prefix
# Extend our parameters
params.update(self.url_parameters(privacy=privacy, *args, **kwargs))
@ -312,6 +325,13 @@ class NotifyNextcloud(NotifyBase):
params=NotifyNextcloud.urlencode(params),
)
def __len__(self):
"""
Returns the number of targets associated with this notification
"""
targets = len(self.targets)
return targets if targets else 1
@staticmethod
def parse_url(url):
"""
@ -339,6 +359,12 @@ class NotifyNextcloud(NotifyBase):
results['version'] = \
NotifyNextcloud.unquote(results['qsd']['version'])
# Support URL Prefixes
if 'url_prefix' in results['qsd'] \
and len(results['qsd']['url_prefix']):
results['url_prefix'] = \
NotifyNextcloud.unquote(results['qsd']['url_prefix'])
# Add our headers that the user can potentially over-ride if they wish
# to to our returned result set and tidy entries by unquoting them
results['headers'] = {

View file

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
# BSD 3-Clause License
# BSD 2-Clause License
#
# Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
# 3. Neither the name of the copyright holder nor the names of its
# contributors may be used to endorse or promote products derived from
# this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
@ -96,6 +92,11 @@ class NotifyNextcloudTalk(NotifyBase):
'private': True,
'required': True,
},
'target_room_id': {
'name': _('Room ID'),
'type': 'string',
'map_to': 'targets',
},
'targets': {
'name': _('Targets'),
'type': 'list:string',
@ -103,6 +104,14 @@ class NotifyNextcloudTalk(NotifyBase):
},
})
# Define our template arguments
template_args = dict(NotifyBase.template_args, **{
'url_prefix': {
'name': _('URL Prefix'),
'type': 'string',
},
})
# Define any kwargs we're using
template_kwargs = {
'headers': {
@ -111,7 +120,7 @@ class NotifyNextcloudTalk(NotifyBase):
},
}
def __init__(self, targets=None, headers=None, **kwargs):
def __init__(self, targets=None, headers=None, url_prefix=None, **kwargs):
"""
Initialize Nextcloud Talk Object
"""
@ -122,11 +131,12 @@ class NotifyNextcloudTalk(NotifyBase):
self.logger.warning(msg)
raise TypeError(msg)
# Store our targets
self.targets = parse_list(targets)
if len(self.targets) == 0:
msg = 'At least one Nextcloud Talk Room ID must be specified.'
self.logger.warning(msg)
raise TypeError(msg)
# Support URL Prefix
self.url_prefix = '' if not url_prefix \
else url_prefix.strip('/')
self.headers = {}
if headers:
@ -140,6 +150,12 @@ class NotifyNextcloudTalk(NotifyBase):
Perform Nextcloud Talk Notification
"""
if len(self.targets) == 0:
# There were no services to notify
self.logger.warning(
'There were no Nextcloud Talk targets to notify.')
return False
# Prepare our Header
headers = {
'User-Agent': self.app_id,
@ -171,13 +187,14 @@ class NotifyNextcloudTalk(NotifyBase):
}
# Nextcloud Talk URL
notify_url = '{schema}://{host}'\
notify_url = '{schema}://{host}/{url_prefix}'\
'/ocs/v2.php/apps/spreed/api/v1/chat/{target}'
notify_url = notify_url.format(
schema='https' if self.secure else 'http',
host=self.host if not isinstance(self.port, int)
else '{}:{}'.format(self.host, self.port),
url_prefix=self.url_prefix,
target=target,
)
@ -200,7 +217,8 @@ class NotifyNextcloudTalk(NotifyBase):
verify=self.verify_certificate,
timeout=self.request_timeout,
)
if r.status_code != requests.codes.created:
if r.status_code not in (
requests.codes.created, requests.codes.ok):
# We had a problem
status_str = \
NotifyNextcloudTalk.http_response_code_lookup(
@ -240,6 +258,14 @@ class NotifyNextcloudTalk(NotifyBase):
Returns the URL built dynamically based on specified arguments.
"""
# Our default set of parameters
params = self.url_parameters(privacy=privacy, *args, **kwargs)
# Append our headers into our parameters
params.update({'+{}'.format(k): v for k, v in self.headers.items()})
if self.url_prefix:
params['url_prefix'] = self.url_prefix
# Determine Authentication
auth = '{user}:{password}@'.format(
user=NotifyNextcloudTalk.quote(self.user, safe=''),
@ -249,7 +275,7 @@ class NotifyNextcloudTalk(NotifyBase):
default_port = 443 if self.secure else 80
return '{schema}://{auth}{hostname}{port}/{targets}' \
return '{schema}://{auth}{hostname}{port}/{targets}?{params}' \
.format(
schema=self.secure_protocol
if self.secure else self.protocol,
@ -261,8 +287,16 @@ class NotifyNextcloudTalk(NotifyBase):
else ':{}'.format(self.port),
targets='/'.join([NotifyNextcloudTalk.quote(x)
for x in self.targets]),
params=NotifyNextcloudTalk.urlencode(params),
)
def __len__(self):
"""
Returns the number of targets associated with this notification
"""
targets = len(self.targets)
return targets if targets else 1
@staticmethod
def parse_url(url):
"""
@ -280,6 +314,12 @@ class NotifyNextcloudTalk(NotifyBase):
results['targets'] = \
NotifyNextcloudTalk.split_path(results['fullpath'])
# Support URL Prefixes
if 'url_prefix' in results['qsd'] \
and len(results['qsd']['url_prefix']):
results['url_prefix'] = \
NotifyNextcloudTalk.unquote(results['qsd']['url_prefix'])
# Add our headers that the user can potentially over-ride if they wish
# to to our returned result set and tidy entries by unquoting them
results['headers'] = {

View file

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
# BSD 3-Clause License
# BSD 2-Clause License
#
# Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
# 3. Neither the name of the copyright holder nor the names of its
# contributors may be used to endorse or promote products derived from
# this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
@ -112,12 +108,12 @@ class NotifyNotica(NotifyBase):
'{schema}://{user}:{password}@{host}:{port}/{token}',
# Self-hosted notica servers (with custom path)
'{schema}://{host}{path}{token}',
'{schema}://{host}:{port}{path}{token}',
'{schema}://{user}@{host}{path}{token}',
'{schema}://{user}@{host}:{port}{path}{token}',
'{schema}://{user}:{password}@{host}{path}{token}',
'{schema}://{user}:{password}@{host}:{port}{path}{token}',
'{schema}://{host}{path}/{token}',
'{schema}://{host}:{port}/{path}/{token}',
'{schema}://{user}@{host}/{path}/{token}',
'{schema}://{user}@{host}:{port}{path}/{token}',
'{schema}://{user}:{password}@{host}{path}/{token}',
'{schema}://{user}:{password}@{host}:{port}/{path}/{token}',
)
# Define our template tokens

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 -*-
# BSD 3-Clause License
# BSD 2-Clause License
#
# Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
# 3. Neither the name of the copyright holder nor the names of its
# contributors may be used to endorse or promote products derived from
# this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE

View file

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

View file

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

View file

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
# BSD 3-Clause License
# BSD 2-Clause License
#
# Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
# 3. Neither the name of the copyright holder nor the names of its
# contributors may be used to endorse or promote products derived from
# this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
@ -51,7 +47,7 @@ from ..utils import is_email
from ..AppriseLocale import gettext_lazy as _
class OneSignalCategory(NotifyBase):
class OneSignalCategory:
"""
We define the different category types that we can notify via OneSignal
"""
@ -92,7 +88,7 @@ class NotifyOneSignal(NotifyBase):
image_size = NotifyImageSize.XY_72
# The maximum allowable batch sizes per message
maximum_batch_size = 2000
default_batch_size = 2000
# Define object templates
templates = (
@ -121,7 +117,7 @@ class NotifyOneSignal(NotifyBase):
'private': True,
'required': True,
},
'target_device': {
'target_player': {
'name': _('Target Player ID'),
'type': 'string',
'map_to': 'targets',
@ -146,6 +142,7 @@ class NotifyOneSignal(NotifyBase):
'targets': {
'name': _('Targets'),
'type': 'list:string',
'required': True,
},
})
@ -204,7 +201,7 @@ class NotifyOneSignal(NotifyBase):
raise TypeError(msg)
# Prepare Batch Mode Flag
self.batch_size = self.maximum_batch_size if batch else 1
self.batch_size = self.default_batch_size if batch else 1
# Place a thumbnail image inline with the message body
self.include_image = include_image
@ -432,6 +429,26 @@ class NotifyOneSignal(NotifyBase):
params=NotifyOneSignal.urlencode(params),
)
def __len__(self):
"""
Returns the number of targets associated with this notification
"""
#
# Factor batch into calculation
#
if self.batch_size > 1:
# Batches can only be sent by group (you can't combine groups into
# a single batch)
total_targets = 0
for k, m in self.targets.items():
targets = len(m)
total_targets += int(targets / self.batch_size) + \
(1 if targets % self.batch_size else 0)
return total_targets
# Normal batch count; just count the targets
return sum([len(m) for _, m in self.targets.items()])
@staticmethod
def parse_url(url):
"""

View file

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
# BSD 3-Clause License
# BSD 2-Clause License
#
# Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
# 3. Neither the name of the copyright holder nor the names of its
# contributors may be used to endorse or promote products derived from
# this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
@ -172,7 +168,7 @@ class NotifyOpsgenie(NotifyBase):
opsgenie_default_region = OpsgenieRegion.US
# The maximum allowable targets within a notification
maximum_batch_size = 50
default_batch_size = 50
# Define object templates
templates = (
@ -308,7 +304,7 @@ class NotifyOpsgenie(NotifyBase):
self.details.update(details)
# Prepare Batch Mode Flag
self.batch_size = self.maximum_batch_size if batch else 1
self.batch_size = self.default_batch_size if batch else 1
# Assign our tags (if defined)
self.__tags = parse_list(tags)
@ -536,6 +532,20 @@ class NotifyOpsgenie(NotifyBase):
for x in self.targets]),
params=NotifyOpsgenie.urlencode(params))
def __len__(self):
"""
Returns the number of targets associated with this notification
"""
#
# Factor batch into calculation
#
targets = len(self.targets)
if self.batch_size > 1:
targets = int(targets / self.batch_size) + \
(1 if targets % self.batch_size else 0)
return targets if targets > 0 else 1
@staticmethod
def parse_url(url):
"""

View file

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

View file

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
# BSD 3-Clause License
# BSD 2-Clause License
#
# Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
# 3. Neither the name of the copyright holder nor the names of its
# contributors may be used to endorse or promote products derived from
# this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE

View file

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
# BSD 3-Clause License
# BSD 2-Clause License
#
# Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
# 3. Neither the name of the copyright holder nor the names of its
# contributors may be used to endorse or promote products derived from
# this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
@ -30,8 +26,6 @@
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
# Official API reference: https://developer.gitter.im/docs/user-resource
import re
import requests
from json import dumps

View file

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
# BSD 3-Clause License
# BSD 2-Clause License
#
# Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
# 3. Neither the name of the copyright holder nor the names of its
# contributors may be used to endorse or promote products derived from
# this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
@ -93,6 +89,7 @@ class NotifyPopcornNotify(NotifyBase):
'targets': {
'name': _('Targets'),
'type': 'list:string',
'required': True,
}
})
@ -265,6 +262,21 @@ class NotifyPopcornNotify(NotifyBase):
[NotifyPopcornNotify.quote(x, safe='') for x in self.targets]),
params=NotifyPopcornNotify.urlencode(params))
def __len__(self):
"""
Returns the number of targets associated with this notification
"""
#
# Factor batch into calculation
#
batch_size = 1 if not self.batch else self.default_batch_size
targets = len(self.targets)
if batch_size > 1:
targets = int(targets / batch_size) + \
(1 if targets % batch_size else 0)
return targets
@staticmethod
def parse_url(url):
"""

View file

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
# BSD 3-Clause License
# BSD 2-Clause License
#
# Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
# 3. Neither the name of the copyright holder nor the names of its
# contributors may be used to endorse or promote products derived from
# this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE

View file

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
# BSD 3-Clause License
# BSD 2-Clause License
#
# Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
# 3. Neither the name of the copyright holder nor the names of its
# contributors may be used to endorse or promote products derived from
# this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
@ -75,6 +71,9 @@ class NotifyPushBullet(NotifyBase):
# PushBullet uses the http protocol with JSON requests
notify_url = 'https://api.pushbullet.com/v2/{}'
# Support attachments
attachment_support = True
# Define object templates
templates = (
'{schema}://{accesstoken}',
@ -150,7 +149,7 @@ class NotifyPushBullet(NotifyBase):
# Build a list of our attachments
attachments = []
if attach:
if attach and self.attachment_support:
# We need to upload our payload first so that we can source it
# in remaining messages
for attachment in attach:
@ -261,14 +260,15 @@ class NotifyPushBullet(NotifyBase):
"PushBullet recipient {} parsed as a device"
.format(recipient))
okay, response = self._send(
self.notify_url.format('pushes'), payload)
if not okay:
has_error = True
continue
if body:
okay, response = self._send(
self.notify_url.format('pushes'), payload)
if not okay:
has_error = True
continue
self.logger.info(
'Sent PushBullet notification to "%s".' % (recipient))
self.logger.info(
'Sent PushBullet notification to "%s".' % (recipient))
for attach_payload in attachments:
# Send our attachments to our same user (already prepared as
@ -406,6 +406,12 @@ class NotifyPushBullet(NotifyBase):
targets=targets,
params=NotifyPushBullet.urlencode(params))
def __len__(self):
"""
Returns the number of targets associated with this notification
"""
return len(self.targets)
@staticmethod
def parse_url(url):
"""

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

View file

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
# BSD 3-Clause License
# BSD 2-Clause License
#
# Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
# 3. Neither the name of the copyright holder nor the names of its
# contributors may be used to endorse or promote products derived from
# this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
@ -329,6 +325,13 @@ class NotifyPushed(NotifyBase):
)]),
params=NotifyPushed.urlencode(params))
def __len__(self):
"""
Returns the number of targets associated with this notification
"""
targets = len(self.channels) + len(self.users)
return targets if targets > 0 else 1
@staticmethod
def parse_url(url):
"""

View file

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
# BSD 3-Clause License
# BSD 2-Clause License
#
# Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
# 3. Neither the name of the copyright holder nor the names of its
# contributors may be used to endorse or promote products derived from
# this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE

View file

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

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

View file

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
# BSD 3-Clause License
# BSD 2-Clause License
#
# Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
# 3. Neither the name of the copyright holder nor the names of its
# contributors may be used to endorse or promote products derived from
# this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
@ -348,6 +344,13 @@ class NotifyRocketChat(NotifyBase):
params=NotifyRocketChat.urlencode(params),
)
def __len__(self):
"""
Returns the number of targets associated with this notification
"""
targets = len(self.channels) + len(self.rooms) + len(self.users)
return targets if targets > 0 else 1
def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs):
"""
wrapper to _send since we can alert more then one channel

View file

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

View file

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

View file

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
# BSD 3-Clause License
# BSD 2-Clause License
#
# Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
# 3. Neither the name of the copyright holder nor the names of its
# contributors may be used to endorse or promote products derived from
# this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
@ -73,6 +69,22 @@ SMSEAGLE_PRIORITY_MAP = {
}
class SMSEagleCategory:
"""
We define the different category types that we can notify via SMS Eagle
"""
PHONE = 'phone'
GROUP = 'group'
CONTACT = 'contact'
SMSEAGLE_CATEGORIES = (
SMSEagleCategory.PHONE,
SMSEagleCategory.GROUP,
SMSEagleCategory.CONTACT,
)
class NotifySMSEagle(NotifyBase):
"""
A wrapper for SMSEagle Notifications
@ -96,6 +108,9 @@ class NotifySMSEagle(NotifyBase):
# The path we send our notification to
notify_path = '/jsonrpc/sms'
# Support attachments
attachment_support = True
# The maxumum length of the text message
# The actual limit is 160 but SMSEagle looks after the handling
# of large messages in it's upstream service
@ -129,6 +144,7 @@ class NotifySMSEagle(NotifyBase):
'token': {
'name': _('Access Token'),
'type': 'string',
'required': True,
},
'target_phone': {
'name': _('Target Phone No'),
@ -154,6 +170,7 @@ class NotifySMSEagle(NotifyBase):
'targets': {
'name': _('Targets'),
'type': 'list:string',
'required': True,
}
})
@ -322,7 +339,7 @@ class NotifySMSEagle(NotifyBase):
has_error = False
attachments = []
if attach:
if attach and self.attachment_support:
for attachment in attach:
# Perform some simple error checking
if not attachment:
@ -403,15 +420,15 @@ class NotifySMSEagle(NotifyBase):
batch_size = 1 if not self.batch else self.default_batch_size
notify_by = {
'phone': {
SMSEagleCategory.PHONE: {
"method": "sms.send_sms",
'target': 'to',
},
'group': {
SMSEagleCategory.GROUP: {
"method": "sms.send_togroup",
'target': 'groupname',
},
'contact': {
SMSEagleCategory.CONTACT: {
"method": "sms.send_tocontact",
'target': 'contactname',
},
@ -420,7 +437,7 @@ class NotifySMSEagle(NotifyBase):
# categories separated into a tuple since notify_by.keys()
# returns an unpredicable list in Python 2.7 which causes
# tests to fail every so often
for category in ('phone', 'group', 'contact'):
for category in SMSEAGLE_CATEGORIES:
# Create a copy of our template
payload = {
'method': notify_by[category]['method'],
@ -596,6 +613,28 @@ class NotifySMSEagle(NotifyBase):
params=NotifySMSEagle.urlencode(params),
)
def __len__(self):
"""
Returns the number of targets associated with this notification
"""
#
# Factor batch into calculation
#
batch_size = 1 if not self.batch else self.default_batch_size
if batch_size > 1:
# Batches can only be sent by group (you can't combine groups into
# a single batch)
total_targets = 0
for c in SMSEAGLE_CATEGORIES:
targets = len(getattr(self, f'target_{c}s'))
total_targets += int(targets / batch_size) + \
(1 if targets % batch_size else 0)
return total_targets
# Normal batch count; just count the targets
return len(self.target_phones) + len(self.target_contacts) + \
len(self.target_groups)
@staticmethod
def parse_url(url):
"""

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