Updated to apprise v1.6.0

Changelog: https://github.com/caronc/apprise/releases

Highlights:

* v1.6.0
    * Notifiarr
* v1.5.0
    * Pushy
    * PushDeer
    * PushMe
    * RSyslog
* v1.4.5
    * WhatsApp
    * Burst SMS
pull/2315/head
Jeff Byrnes 5 months ago committed by GitHub
parent cb2023d94e
commit 55c5384f9c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -0,0 +1,33 @@
# This is a stub package designed to roughly emulate the _yaml
# extension module, which previously existed as a standalone module
# and has been moved into the `yaml` package namespace.
# It does not perfectly mimic its old counterpart, but should get
# close enough for anyone who's relying on it even when they shouldn't.
import yaml
# in some circumstances, the yaml module we imoprted may be from a different version, so we need
# to tread carefully when poking at it here (it may not have the attributes we expect)
if not getattr(yaml, '__with_libyaml__', False):
from sys import version_info
exc = ModuleNotFoundError if version_info >= (3, 6) else ImportError
raise exc("No module named '_yaml'")
else:
from yaml._yaml import *
import warnings
warnings.warn(
'The _yaml extension module is now located at yaml._yaml'
' and its location is subject to change. To use the'
' LibYAML-based parser and emitter, import from `yaml`:'
' `from yaml import CLoader as Loader, CDumper as Dumper`.',
DeprecationWarning
)
del warnings
# Don't `del yaml` here because yaml is actually an existing
# namespace member of _yaml.
__name__ = '_yaml'
# If the module is top-level (i.e. not a part of any specific package)
# then the attribute should be set to ''.
# https://docs.python.org/3.8/library/types.html
__package__ = ''

@ -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
@ -458,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)
@ -498,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:
#
@ -526,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')
@ -543,8 +543,8 @@ 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
@ -685,6 +685,11 @@ class Apprise:
# 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)
@ -810,6 +815,36 @@ class Apprise:
# If we reach here, then we indexed out of range
raise IndexError('list index out of range')
def __getstate__(self):
"""
Pickle Support dumps()
"""
attributes = {
'asset': self.asset,
# Prepare our URL list as we need to extract the associated tags
# and asset details associated with it
'urls': [{
'url': server.url(privacy=False),
'tag': server.tags if server.tags else None,
'asset': server.asset} for server in self.servers],
'locale': self.locale,
'debug': self.debug,
'location': self.location,
}
return attributes
def __setstate__(self, state):
"""
Pickle Support loads()
"""
self.servers = list()
self.asset = state['asset']
self.locale = state['locale']
self.location = state['location']
for entry in state['urls']:
self.add(entry['url'], asset=entry['asset'], tag=entry['tag'])
def __bool__(self):
"""
Allows the Apprise object to be wrapped in an 'if statement'.

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

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

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

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
# BSD 3-Clause License
# BSD 2-Clause License
#
# Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
# 3. Neither the name of the copyright holder nor the names of its
# contributors may be used to endorse or promote products derived from
# this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
@ -33,14 +29,13 @@
import ctypes
import locale
import contextlib
import os
import re
from os.path import join
from os.path import dirname
from os.path import abspath
from .logger import logger
# Define our translation domain
DOMAIN = 'apprise'
LOCALE_DIR = abspath(join(dirname(__file__), 'i18n'))
# This gets toggled to True if we succeed
GETTEXT_LOADED = False
@ -49,51 +44,41 @@ 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 LazyTranslation:
class AppriseLocale:
"""
Doesn't translate anything until str() or unicode() references
are made.
A wrapper class to gettext so that we can manipulate multiple lanaguages
on the fly if required.
"""
def __init__(self, text, *args, **kwargs):
"""
Store our text
"""
self.text = text
super().__init__(*args, **kwargs)
# Define our translation domain
_domain = 'apprise'
def __str__(self):
return gettext.gettext(self.text)
# 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)
# Lazy translation handling
def gettext_lazy(text):
"""
A dummy function that can be referenced
"""
return LazyTranslation(text=text)
# Define our default encoding
_default_encoding = 'utf-8'
# The function to assign `_` by default
_fn = 'gettext'
class AppriseLocale:
"""
A wrapper class to gettext so that we can manipulate multiple lanaguages
on the fly if required.
"""
# The language we should fall back to if all else fails
_default_language = 'en'
def __init__(self, language=None):
"""
@ -110,25 +95,55 @@ class AppriseLocale:
# 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
if self.lang:
# 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[self.lang] = gettext.translation(
DOMAIN, localedir=LOCALE_DIR, languages=[self.lang])
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)
# Install our language
self._gtobjs[self.lang].install()
except FileNotFoundError:
# The translation directory does not exist
logger.debug(
'Could not load translation path: %s',
join(self._locale_dir, lang))
except IOError:
# This occurs if we can't access/load our translations
pass
# 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):
def lang_at(self, lang, mapto=_fn):
"""
The syntax works as:
with at.lang_at('fr'):
@ -138,50 +153,36 @@ class AppriseLocale:
"""
if GETTEXT_LOADED is False:
# yield
yield
# Do nothing
yield None
# 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()
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
yield getattr(self._gtobjs[lang], mapto)
except (IOError, KeyError):
# This occurs if we can't access/load our translations
# Yield reguardless
yield
return
finally:
# Fall back to our previous language
if lang != self.lang and lang in self._gtobjs:
# Install our language
self._gtobjs[self.lang].install()
@property
def gettext(self):
"""
Return the current language gettext() function
return
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)
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.
@ -190,6 +191,17 @@ class AppriseLocale:
# 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:
@ -203,11 +215,12 @@ class AppriseLocale:
# Fallback to posix detection
pass
# Built in locale library check
try:
# Detect language
lang = locale.getdefaultlocale()[0]
# Acquire our locale
lang = locale.getlocale()[0]
except ValueError as e:
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
@ -217,9 +230,57 @@ class AppriseLocale:
'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()
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:
"""
Doesn't translate anything until str() or unicode() references
are made.
"""
def __init__(self, text, *args, **kwargs):
"""
Store our text
"""
self.text = text
super().__init__(*args, **kwargs)
def __str__(self):
return LOCALE.gettext(self.text) if GETTEXT_LOADED else self.text
# Lazy translation handling
def gettext_lazy(text):
"""
A dummy function that can be referenced
"""
return LazyTranslation(text=text)

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

@ -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.4.0'
__version__ = '1.6.0'
__author__ = 'Chris Caron'
__license__ = 'BSD'
__copywrite__ = 'Copyright (C) 2023 Chris Caron <lead2gold@gmail.com>'

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

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

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

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

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

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

@ -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
@ -356,6 +352,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.
@ -541,6 +608,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()
@ -549,6 +619,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()
@ -556,7 +632,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:
@ -582,8 +658,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
@ -603,6 +684,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)
@ -615,25 +723,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
@ -665,6 +805,12 @@ class ConfigBase(URLBase):
# the include keyword
configs = list()
# Group Assignments
group_tags = {}
# Track our entries to preload
preloaded = []
try:
# Load our data (safely)
result = yaml.load(content, Loader=yaml.SafeLoader)
@ -746,7 +892,45 @@ class ConfigBase(URLBase):
tags = result.get('tag', None)
if tags and isinstance(tags, (list, tuple, str)):
# Store any preset tags
global_tags = set(parse_list(tags))
global_tags = set(parse_list(tags, cast=str))
#
# groups root directive
#
groups = result.get('groups', None)
if not isinstance(groups, (list, tuple)):
# Not a problem; we simply have no group entry
groups = list()
# Iterate over each group defined and store it
for no, entry in enumerate(groups):
if not isinstance(entry, dict):
ConfigBase.logger.warning(
'No assignment for group {}, entry #{}'.format(
entry, no + 1))
continue
for _groups, tags in entry.items():
for group in parse_list(_groups, cast=str):
if isinstance(tags, (list, tuple)):
_tags = set()
for e in tags:
if isinstance(e, dict):
_tags |= set(e.keys())
else:
_tags |= set(parse_list(e, cast=str))
# Final assignment
tags = _tags
else:
tags = set(parse_list(tags, cast=str))
if group not in group_tags:
group_tags[group] = tags
else:
group_tags[group] |= tags
#
# include root directive
@ -938,8 +1122,8 @@ class ConfigBase(URLBase):
# The below ensures our tags are set correctly
if 'tag' in _results:
# Tidy our list up
_results['tag'] = \
set(parse_list(_results['tag'])) | global_tags
_results['tag'] = set(
parse_list(_results['tag'], cast=str)) | global_tags
else:
# Just use the global settings
@ -965,29 +1149,59 @@ class ConfigBase(URLBase):
# Prepare our Asset Object
_results['asset'] = asset
# Now we generate our plugin
try:
# Attempt to create an instance of our plugin using the
# parsed URL information
plugin = common.\
NOTIFY_SCHEMA_MAP[_results['schema']](**_results)
# Store our preloaded entries
preloaded.append({
'results': _results,
'entry': no + 1,
'item': entry,
})
# Create log entry of loaded URL
ConfigBase.logger.debug(
'Loaded URL: {}'.format(
plugin.url(privacy=asset.secure_logging)))
#
# Normalize Tag Groups
# - Expand Groups of Groups so that they don't exist
#
ConfigBase.__normalize_tag_groups(group_tags)
except Exception as e:
# the arguments are invalid or can not be used.
ConfigBase.logger.warning(
'Could not load Apprise YAML configuration '
'entry #{}, item #{}'
.format(no + 1, entry))
ConfigBase.logger.debug('Loading Exception: %s' % str(e))
continue
#
# 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)
# Now we generate our plugin
try:
# Attempt to create an instance of our plugin using the
# parsed URL information
plugin = common.\
NOTIFY_SCHEMA_MAP[results['schema']](**results)
# if we reach here, we successfully loaded our data
servers.append(plugin)
# Create log entry of loaded URL
ConfigBase.logger.debug(
'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 Apprise YAML configuration '
'entry #{}, item #{}'
.format(entry['entry'], entry['item']))
ConfigBase.logger.debug('Loading Exception: %s' % str(e))
continue
# if we reach here, we successfully loaded our data
servers.append(plugin)
return (servers, configs)

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

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

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

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

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

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

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

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

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

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
# BSD 3-Clause License
# BSD 2-Clause License
#
# Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
# 3. Neither the name of the copyright holder nor the names of its
# contributors may be used to endorse or promote products derived from
# this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
@ -77,6 +73,9 @@ 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_read_timeout = 30.0
@ -164,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:
@ -260,7 +255,7 @@ class NotifyAppriseAPI(NotifyBase):
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:
@ -310,7 +305,10 @@ class NotifyAppriseAPI(NotifyBase):
if self.method == AppriseAPIMethod.JSON:
headers['Content-Type'] = 'application/json'
payload['attachments'] = attachments
if attachments:
payload['attachments'] = attachments
payload = dumps(payload)
if self.__tags:
@ -328,8 +326,8 @@ 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({

@ -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,
},
})

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

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
# BSD 3-Clause License
# BSD 2-Clause License
#
# Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
# 3. Neither the name of the copyright holder nor the names of its
# contributors may be used to endorse or promote products derived from
# this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
@ -151,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,
@ -234,8 +236,7 @@ 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}
@ -381,6 +382,16 @@ class NotifyBoxcar(NotifyBase):
results['targets'] += \
NotifyBoxcar.parse_list(results['qsd'].get('to'))
# Access
if 'access' in results['qsd'] and results['qsd']['access']:
results['access'] = NotifyBoxcar.unquote(
results['qsd']['access'].strip())
# Secret
if 'secret' in results['qsd'] and results['qsd']['secret']:
results['secret'] = NotifyBoxcar.unquote(
results['qsd']['secret'].strip())
# Include images with our message
results['include_image'] = \
parse_bool(results['qsd'].get('image', True))

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

@ -0,0 +1,460 @@
# -*- coding: utf-8 -*-
# BSD 2-Clause License
#
# Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# 1. Redistributions of source code must retain the above copyright notice,
# this list of conditions and the following disclaimer.
#
# 2. Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
# Sign-up with https://burstsms.com/
#
# Define your API Secret here and acquire your API Key
# - https://can.transmitsms.com/profile
#
import requests
from .NotifyBase import NotifyBase
from ..URLBase import PrivacyMode
from ..common import NotifyType
from ..utils import is_phone_no
from ..utils import parse_phone_no
from ..utils import parse_bool
from ..utils import validate_regex
from ..AppriseLocale import gettext_lazy as _
class BurstSMSCountryCode:
# Australia
AU = 'au'
# New Zeland
NZ = 'nz'
# United Kingdom
UK = 'gb'
# United States
US = 'us'
BURST_SMS_COUNTRY_CODES = (
BurstSMSCountryCode.AU,
BurstSMSCountryCode.NZ,
BurstSMSCountryCode.UK,
BurstSMSCountryCode.US,
)
class NotifyBurstSMS(NotifyBase):
"""
A wrapper for Burst SMS Notifications
"""
# The default descriptive name associated with the Notification
service_name = 'Burst SMS'
# The services URL
service_url = 'https://burstsms.com/'
# The default protocol
secure_protocol = 'burstsms'
# The maximum amount of SMS Messages that can reside within a single
# batch transfer based on:
# https://developer.transmitsms.com/#74911cf8-dec6-4319-a499-7f535a7fd08c
default_batch_size = 500
# A URL that takes you to the setup/help of the specific protocol
setup_url = 'https://github.com/caronc/apprise/wiki/Notify_burst_sms'
# Burst SMS uses the http protocol with JSON requests
notify_url = 'https://api.transmitsms.com/send-sms.json'
# The maximum length of the body
body_maxlen = 160
# A title can not be used for SMS Messages. Setting this to zero will
# cause any title (if defined) to get placed into the message body.
title_maxlen = 0
# Define object templates
templates = (
'{schema}://{apikey}:{secret}@{sender_id}/{targets}',
)
# Define our template tokens
template_tokens = dict(NotifyBase.template_tokens, **{
'apikey': {
'name': _('API Key'),
'type': 'string',
'required': True,
'regex': (r'^[a-z0-9]+$', 'i'),
'private': True,
},
'secret': {
'name': _('API Secret'),
'type': 'string',
'private': True,
'required': True,
'regex': (r'^[a-z0-9]+$', 'i'),
},
'sender_id': {
'name': _('Sender ID'),
'type': 'string',
'required': True,
'map_to': 'source',
},
'target_phone': {
'name': _('Target Phone No'),
'type': 'string',
'prefix': '+',
'regex': (r'^[0-9\s)(+-]+$', 'i'),
'map_to': 'targets',
},
'targets': {
'name': _('Targets'),
'type': 'list:string',
'required': True,
},
})
# Define our template arguments
template_args = dict(NotifyBase.template_args, **{
'to': {
'alias_of': 'targets',
},
'from': {
'alias_of': 'sender_id',
},
'key': {
'alias_of': 'apikey',
},
'secret': {
'alias_of': 'secret',
},
'country': {
'name': _('Country'),
'type': 'choice:string',
'values': BURST_SMS_COUNTRY_CODES,
'default': BurstSMSCountryCode.US,
},
# Validity
# Expire a message send if it is undeliverable (defined in minutes)
# If set to Zero (0); this is the default and sets the max validity
# period
'validity': {
'name': _('validity'),
'type': 'int',
'default': 0
},
'batch': {
'name': _('Batch Mode'),
'type': 'bool',
'default': False,
},
})
def __init__(self, apikey, secret, source, targets=None, country=None,
validity=None, batch=None, **kwargs):
"""
Initialize Burst SMS Object
"""
super().__init__(**kwargs)
# API Key (associated with project)
self.apikey = validate_regex(
apikey, *self.template_tokens['apikey']['regex'])
if not self.apikey:
msg = 'An invalid Burst SMS API Key ' \
'({}) was specified.'.format(apikey)
self.logger.warning(msg)
raise TypeError(msg)
# API Secret (associated with project)
self.secret = validate_regex(
secret, *self.template_tokens['secret']['regex'])
if not self.secret:
msg = 'An invalid Burst SMS API Secret ' \
'({}) was specified.'.format(secret)
self.logger.warning(msg)
raise TypeError(msg)
if not country:
self.country = self.template_args['country']['default']
else:
self.country = country.lower().strip()
if country not in BURST_SMS_COUNTRY_CODES:
msg = 'An invalid Burst SMS country ' \
'({}) was specified.'.format(country)
self.logger.warning(msg)
raise TypeError(msg)
# Set our Validity
self.validity = self.template_args['validity']['default']
if validity:
try:
self.validity = int(validity)
except (ValueError, TypeError):
msg = 'The Burst SMS Validity specified ({}) is invalid.'\
.format(validity)
self.logger.warning(msg)
raise TypeError(msg)
# Prepare Batch Mode Flag
self.batch = self.template_args['batch']['default'] \
if batch is None else batch
# The Sender ID
self.source = validate_regex(source)
if not self.source:
msg = 'The Account Sender ID specified ' \
'({}) is invalid.'.format(source)
self.logger.warning(msg)
raise TypeError(msg)
# Parse our targets
self.targets = list()
for target in parse_phone_no(targets):
# Validate targets and drop bad ones:
result = is_phone_no(target)
if not result:
self.logger.warning(
'Dropped invalid phone # '
'({}) specified.'.format(target),
)
continue
# store valid phone number
self.targets.append(result['full'])
return
def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs):
"""
Perform Burst SMS Notification
"""
if not self.targets:
self.logger.warning(
'There are no valid Burst SMS targets to notify.')
return False
# error tracking (used for function return)
has_error = False
# Prepare our headers
headers = {
'User-Agent': self.app_id,
'Accept': 'application/json',
}
# Prepare our authentication
auth = (self.apikey, self.secret)
# Prepare our payload
payload = {
'countrycode': self.country,
'message': body,
# Sender ID
'from': self.source,
# The to gets populated in the loop below
'to': None,
}
# Send in batches if identified to do so
batch_size = 1 if not self.batch else self.default_batch_size
# Create a copy of the targets list
targets = list(self.targets)
for index in range(0, len(targets), batch_size):
# Prepare our user
payload['to'] = ','.join(self.targets[index:index + batch_size])
# Some Debug Logging
self.logger.debug('Burst SMS POST URL: {} (cert_verify={})'.format(
self.notify_url, self.verify_certificate))
self.logger.debug('Burst SMS Payload: {}' .format(payload))
# Always call throttle before any remote server i/o is made
self.throttle()
try:
r = requests.post(
self.notify_url,
data=payload,
headers=headers,
auth=auth,
verify=self.verify_certificate,
timeout=self.request_timeout,
)
if r.status_code != requests.codes.ok:
# We had a problem
status_str = \
NotifyBurstSMS.http_response_code_lookup(
r.status_code)
self.logger.warning(
'Failed to send Burst SMS notification to {} '
'target(s): {}{}error={}.'.format(
len(self.targets[index:index + batch_size]),
status_str,
', ' if status_str else '',
r.status_code))
self.logger.debug(
'Response Details:\r\n{}'.format(r.content))
# Mark our failure
has_error = True
continue
else:
self.logger.info(
'Sent Burst SMS notification to %d target(s).' %
len(self.targets[index:index + batch_size]))
except requests.RequestException as e:
self.logger.warning(
'A Connection error occurred sending Burst SMS '
'notification to %d target(s).' %
len(self.targets[index:index + batch_size]))
self.logger.debug('Socket Exception: %s' % str(e))
# Mark our failure
has_error = True
continue
return not has_error
def url(self, privacy=False, *args, **kwargs):
"""
Returns the URL built dynamically based on specified arguments.
"""
# Define any URL parameters
params = {
'country': self.country,
'batch': 'yes' if self.batch else 'no',
}
if self.validity:
params['validity'] = str(self.validity)
# Extend our parameters
params.update(self.url_parameters(privacy=privacy, *args, **kwargs))
return '{schema}://{key}:{secret}@{source}/{targets}/?{params}'.format(
schema=self.secure_protocol,
key=self.pprint(self.apikey, privacy, safe=''),
secret=self.pprint(
self.secret, privacy, mode=PrivacyMode.Secret, safe=''),
source=NotifyBurstSMS.quote(self.source, safe=''),
targets='/'.join(
[NotifyBurstSMS.quote(x, safe='') for x in self.targets]),
params=NotifyBurstSMS.urlencode(params))
def __len__(self):
"""
Returns the number of targets associated with this notification
"""
#
# Factor batch into calculation
#
batch_size = 1 if not self.batch else self.default_batch_size
targets = len(self.targets)
if batch_size > 1:
targets = int(targets / batch_size) + \
(1 if targets % batch_size else 0)
return targets if targets > 0 else 1
@staticmethod
def parse_url(url):
"""
Parses the URL and returns enough arguments that can allow
us to re-instantiate this object.
"""
results = NotifyBase.parse_url(url, verify_host=False)
if not results:
# We're done early as we couldn't load the results
return results
# The hostname is our source (Sender ID)
results['source'] = NotifyBurstSMS.unquote(results['host'])
# Get any remaining targets
results['targets'] = NotifyBurstSMS.split_path(results['fullpath'])
# Get our account_side and auth_token from the user/pass config
results['apikey'] = NotifyBurstSMS.unquote(results['user'])
results['secret'] = NotifyBurstSMS.unquote(results['password'])
# API Key
if 'key' in results['qsd'] and len(results['qsd']['key']):
# Extract the API Key from an argument
results['apikey'] = \
NotifyBurstSMS.unquote(results['qsd']['key'])
# API Secret
if 'secret' in results['qsd'] and len(results['qsd']['secret']):
# Extract the API Secret from an argument
results['secret'] = \
NotifyBurstSMS.unquote(results['qsd']['secret'])
# Support the 'from' and 'source' variable so that we can support
# targets this way too.
# The 'from' makes it easier to use yaml configuration
if 'from' in results['qsd'] and len(results['qsd']['from']):
results['source'] = \
NotifyBurstSMS.unquote(results['qsd']['from'])
if 'source' in results['qsd'] and len(results['qsd']['source']):
results['source'] = \
NotifyBurstSMS.unquote(results['qsd']['source'])
# Support country
if 'country' in results['qsd'] and len(results['qsd']['country']):
results['country'] = \
NotifyBurstSMS.unquote(results['qsd']['country'])
# Support validity value
if 'validity' in results['qsd'] and len(results['qsd']['validity']):
results['validity'] = \
NotifyBurstSMS.unquote(results['qsd']['validity'])
# Get Batch Mode Flag
if 'batch' in results['qsd'] and len(results['qsd']['batch']):
results['batch'] = parse_bool(results['qsd']['batch'])
# Support the 'to' variable so that we can support rooms this way too
# The 'to' makes it easier to use yaml configuration
if 'to' in results['qsd'] and len(results['qsd']['to']):
results['targets'] += \
NotifyBurstSMS.parse_phone_no(results['qsd']['to'])
return results

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

@ -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,
},
})

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

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

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

@ -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,61 +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.avatar and (image_url or self.avatar_url):
payload['avatar_url'] = \
self.avatar_url if self.avatar_url else image_url
@ -298,22 +273,84 @@ class NotifyDiscord(NotifyBase):
# Optionally override the default username of the webhook
payload['username'] = self.user
# Associate our thread_id with our message
params = {'thread_id': self.thread_id} if self.thread_id else None
if 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 body:
# 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.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:]
if attach:
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({
@ -336,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, params=None, **kwargs):
def _send(self, payload, attach=None, params=None, rate_limit=1,
**kwargs):
"""
Wrapper to the requests (post) object
"""
@ -365,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):
@ -401,6 +456,22 @@ class NotifyDiscord(NotifyBase):
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):
@ -408,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(
@ -465,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
@ -536,10 +624,23 @@ class NotifyDiscord(NotifyBase):
results['avatar_url'] = \
NotifyDiscord.unquote(results['qsd']['avatar_url'])
# Extract url if it was specified
if 'href' in results['qsd']:
results['href'] = \
NotifyDiscord.unquote(results['qsd']['href'])
elif 'url' in results['qsd']:
results['href'] = \
NotifyDiscord.unquote(results['qsd']['url'])
# Markdown is implied
results['format'] = NotifyFormat.MARKDOWN
# Extract thread id if it was specified
if 'thread' in results['qsd']:
results['thread'] = \
NotifyDiscord.unquote(results['qsd']['thread'])
# Markdown is implied
results['format'] = NotifyFormat.MARKDOWN
return results

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

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

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

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

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

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

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

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

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

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

@ -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
@ -99,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
@ -345,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:

@ -1,425 +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))
def __len__(self):
"""
Returns the number of targets associated with this notification
"""
return len(self.targets)
@staticmethod
def parse_url(url):
"""
Parses the URL and returns enough arguments that can allow
us to re-instantiate this object.
"""
results = NotifyBase.parse_url(url, verify_host=False)
if not results:
# We're done early as we couldn't load the results
return results
results['token'] = NotifyGitter.unquote(results['host'])
# Get our entries; split_path() looks after unquoting content for us
# by default
results['targets'] = NotifyGitter.split_path(results['fullpath'])
# Support the 'to' variable so that we can support targets this way too
# The 'to' makes it easier to use yaml configuration
if 'to' in results['qsd'] and len(results['qsd']['to']):
results['targets'] += NotifyGitter.parse_list(results['qsd']['to'])
# Include images with our message
results['include_image'] = \
parse_bool(results['qsd'].get('image', False))
return results

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

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

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

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

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

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

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

@ -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
@ -80,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
@ -179,19 +178,6 @@ class NotifyJSON(NotifyBase):
self.logger.warning(msg)
raise TypeError(msg)
# 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 = {
JSONPayloadField.VERSION: JSONPayloadField.VERSION,
JSONPayloadField.TITLE: JSONPayloadField.TITLE,
JSONPayloadField.MESSAGE: JSONPayloadField.MESSAGE,
JSONPayloadField.ATTACHMENTS: JSONPayloadField.ATTACHMENTS,
JSONPayloadField.MESSAGETYPE: JSONPayloadField.MESSAGETYPE,
}
self.params = {}
if params:
# Store our extra headers
@ -202,21 +188,10 @@ class NotifyJSON(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].strip()
self.payload_overrides[key] = \
self.payload_extras[key].strip()
del self.payload_extras[key]
return
@ -242,8 +217,6 @@ class NotifyJSON(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()})
# Determine Authentication
auth = ''
@ -289,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:
@ -317,22 +290,30 @@ class NotifyJSON(NotifyBase):
self.logger.debug('I/O Exception: %s' % str(e))
return False
# prepare JSON Object
payload = {}
for key, value in (
(JSONPayloadField.VERSION, self.json_version),
(JSONPayloadField.TITLE, title),
(JSONPayloadField.MESSAGE, body),
(JSONPayloadField.ATTACHMENTS, attachments),
(JSONPayloadField.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)
# Prepare JSON Object
payload = {
JSONPayloadField.VERSION: self.json_version,
JSONPayloadField.TITLE: title,
JSONPayloadField.MESSAGE: body,
JSONPayloadField.ATTACHMENTS: attachments,
JSONPayloadField.MESSAGETYPE: notify_type,
}
for key, value in self.payload_extras.items():
if key in payload:
if not value:
# Do not store element in payload response
del payload[key]
else:
# Re-map
payload[value] = payload[key]
del payload[key]
else:
# Append entry
payload[key] = value
auth = None
if self.user:

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

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

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

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

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

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

@ -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)
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
if short_url is None:
self.short_url = self.template_args['short_url']['default']
# Store our sender
self.sender = sender
else:
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,17 +317,20 @@ 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]),
@ -333,7 +340,8 @@ class NotifyMSG91(NotifyBase):
"""
Returns the number of targets associated with this notification
"""
return len(self.targets)
targets = len(self.targets)
return targets if targets > 0 else 1
@staticmethod
def parse_url(url):
@ -355,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
@ -367,4 +375,10 @@ class NotifyMSG91(NotifyBase):
results['targets'] += \
NotifyMSG91.parse_phone_no(results['qsd']['to'])
# store any additional payload extra's defined
results['template_mapping'] = {
NotifyMSG91.unquote(x): NotifyMSG91.unquote(y)
for x, y in results['qsd:'].items()
}
return results

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

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

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

@ -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
@ -413,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
@ -577,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))
@ -834,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
@ -892,8 +892,9 @@ class NotifyMastodon(NotifyBase):
# Capture rate limiting if possible
self.ratelimit_remaining = \
int(r.headers.get('X-RateLimit-Remaining'))
self.ratelimit_reset = datetime.utcfromtimestamp(
int(r.headers.get('X-RateLimit-Limit')))
self.ratelimit_reset = datetime.fromtimestamp(
int(r.headers.get('X-RateLimit-Limit')), timezone.utc
).replace(tzinfo=None)
except (TypeError, ValueError):
# This is returned if we could not retrieve this information

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

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

@ -0,0 +1,372 @@
# -*- 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.
# Create an incoming webhook; the website will provide you with something like:
# http://localhost:8065/hooks/yobjmukpaw3r3urc5h6i369yima
# ^^^^^^^^^^^^^^^^^^^^^^^^^^^
# |-- this is the webhook --|
#
# You can effectively turn the url above to read this:
# mmost://localhost:8065/yobjmukpaw3r3urc5h6i369yima
# - swap http with mmost
# - drop /hooks/ reference
import requests
from json import dumps
from .NotifyBase import NotifyBase
from ..common import NotifyImageSize
from ..common import NotifyType
from ..utils import parse_bool
from ..utils import parse_list
from ..utils import validate_regex
from ..AppriseLocale import gettext_lazy as _
# Some Reference Locations:
# - https://docs.mattermost.com/developer/webhooks-incoming.html
# - https://docs.mattermost.com/administration/config-settings.html
class NotifyMattermost(NotifyBase):
"""
A wrapper for Mattermost Notifications
"""
# The default descriptive name associated with the Notification
service_name = 'Mattermost'
# The services URL
service_url = 'https://mattermost.com/'
# The default protocol
protocol = 'mmost'
# The default secure protocol
secure_protocol = 'mmosts'
# A URL that takes you to the setup/help of the specific protocol
setup_url = 'https://github.com/caronc/apprise/wiki/Notify_mattermost'
# The default Mattermost port
default_port = 8065
# Allows the user to specify the NotifyImageSize object
image_size = NotifyImageSize.XY_72
# The maximum allowable characters allowed in the body per message
body_maxlen = 4000
# Mattermost does not have a title
title_maxlen = 0
# Define object templates
templates = (
'{schema}://{host}/{token}',
'{schema}://{host}:{port}/{token}',
'{schema}://{host}/{fullpath}/{token}',
'{schema}://{host}:{port}/{fullpath}/{token}',
'{schema}://{botname}@{host}/{token}',
'{schema}://{botname}@{host}:{port}/{token}',
'{schema}://{botname}@{host}/{fullpath}/{token}',
'{schema}://{botname}@{host}:{port}/{fullpath}/{token}',
)
# Define our template tokens
template_tokens = dict(NotifyBase.template_tokens, **{
'host': {
'name': _('Hostname'),
'type': 'string',
'required': True,
},
'token': {
'name': _('Webhook Token'),
'type': 'string',
'private': True,
'required': True,
},
'fullpath': {
'name': _('Path'),
'type': 'string',
},
'botname': {
'name': _('Bot Name'),
'type': 'string',
'map_to': 'user',
},
'port': {
'name': _('Port'),
'type': 'int',
'min': 1,
'max': 65535,
},
})
# Define our template arguments
template_args = dict(NotifyBase.template_args, **{
'channels': {
'name': _('Channels'),
'type': 'list:string',
},
'image': {
'name': _('Include Image'),
'type': 'bool',
'default': True,
'map_to': 'include_image',
},
'to': {
'alias_of': 'channels',
},
})
def __init__(self, token, fullpath=None, channels=None,
include_image=False, **kwargs):
"""
Initialize Mattermost Object
"""
super().__init__(**kwargs)
if self.secure:
self.schema = 'https'
else:
self.schema = 'http'
# our full path
self.fullpath = '' if not isinstance(
fullpath, str) else fullpath.strip()
# Authorization Token (associated with project)
self.token = validate_regex(token)
if not self.token:
msg = 'An invalid Mattermost Authorization Token ' \
'({}) was specified.'.format(token)
self.logger.warning(msg)
raise TypeError(msg)
# Optional Channels (strip off any channel prefix entries if present)
self.channels = [x.lstrip('#') for x in parse_list(channels)]
if not self.port:
self.port = self.default_port
# Place a thumbnail image inline with the message body
self.include_image = include_image
return
def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs):
"""
Perform Mattermost Notification
"""
# Create a copy of our channels, otherwise place a dummy entry
channels = list(self.channels) if self.channels else [None, ]
headers = {
'User-Agent': self.app_id,
'Content-Type': 'application/json'
}
# prepare JSON Object
payload = {
'text': body,
'icon_url': None,
}
# Acquire our image url if configured to do so
image_url = None if not self.include_image \
else self.image_url(notify_type)
if image_url:
# Set our image configuration if told to do so
payload['icon_url'] = image_url
# Set our user
payload['username'] = self.user if self.user else self.app_id
# For error tracking
has_error = False
while len(channels):
# Pop a channel off of the list
channel = channels.pop(0)
if channel:
payload['channel'] = channel
url = '{}://{}:{}{}/hooks/{}'.format(
self.schema, self.host, self.port, self.fullpath,
self.token)
self.logger.debug('Mattermost POST URL: %s (cert_verify=%r)' % (
url, self.verify_certificate,
))
self.logger.debug('Mattermost Payload: %s' % str(payload))
# Always call throttle before any remote server i/o is made
self.throttle()
try:
r = requests.post(
url,
data=dumps(payload),
headers=headers,
verify=self.verify_certificate,
timeout=self.request_timeout,
)
if r.status_code != requests.codes.ok:
# We had a problem
status_str = \
NotifyMattermost.http_response_code_lookup(
r.status_code)
self.logger.warning(
'Failed to send Mattermost notification{}: '
'{}{}error={}.'.format(
'' if not channel
else ' to channel {}'.format(channel),
status_str,
', ' if status_str else '',
r.status_code))
self.logger.debug(
'Response Details:\r\n{}'.format(r.content))
# Flag our error
has_error = True
continue
else:
self.logger.info(
'Sent Mattermost notification{}.'.format(
'' if not channel
else ' to channel {}'.format(channel)))
except requests.RequestException as e:
self.logger.warning(
'A Connection error occurred sending Mattermost '
'notification{}.'.format(
'' if not channel
else ' to channel {}'.format(channel)))
self.logger.debug('Socket Exception: %s' % str(e))
# Flag our error
has_error = True
continue
# Return our overall status
return not has_error
def url(self, privacy=False, *args, **kwargs):
"""
Returns the URL built dynamically based on specified arguments.
"""
# Define any URL parameters
params = {
'image': 'yes' if self.include_image else 'no',
}
# Extend our parameters
params.update(self.url_parameters(privacy=privacy, *args, **kwargs))
if self.channels:
# historically the value only accepted one channel and is
# therefore identified as 'channel'. Channels have always been
# optional, so that is why this setting is nested in an if block
params['channel'] = ','.join(
[NotifyMattermost.quote(x, safe='') for x in self.channels])
default_port = 443 if self.secure else self.default_port
default_schema = self.secure_protocol if self.secure else self.protocol
# Determine if there is a botname present
botname = ''
if self.user:
botname = '{botname}@'.format(
botname=NotifyMattermost.quote(self.user, safe=''),
)
return \
'{schema}://{botname}{hostname}{port}{fullpath}{token}' \
'/?{params}'.format(
schema=default_schema,
botname=botname,
# never encode hostname since we're expecting it to be a valid
# one
hostname=self.host,
port='' if not self.port or self.port == default_port
else ':{}'.format(self.port),
fullpath='/' if not self.fullpath else '{}/'.format(
NotifyMattermost.quote(self.fullpath, safe='/')),
token=self.pprint(self.token, privacy, safe=''),
params=NotifyMattermost.urlencode(params),
)
@staticmethod
def parse_url(url):
"""
Parses the URL and returns enough arguments that can allow
us to re-instantiate this object.
"""
results = NotifyBase.parse_url(url)
if not results:
# We're done early as we couldn't load the results
return results
# Acquire our tokens; the last one will always be our token
# all entries before it will be our path
tokens = NotifyMattermost.split_path(results['fullpath'])
results['token'] = None if not tokens else tokens.pop()
# Store our path
results['fullpath'] = '' if not tokens \
else '/{}'.format('/'.join(tokens))
# Define our optional list of channels to notify
results['channels'] = list()
# Support both 'to' (for yaml configuration) and channel=
if 'to' in results['qsd'] and len(results['qsd']['to']):
# Allow the user to specify the channel to post to
results['channels'].append(
NotifyMattermost.parse_list(results['qsd']['to']))
if 'channel' in results['qsd'] and len(results['qsd']['channel']):
# Allow the user to specify the channel to post to
results['channels'].append(
NotifyMattermost.parse_list(results['qsd']['channel']))
# Image manipulation
results['include_image'] = \
parse_bool(results['qsd'].get('image', False))
return results

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

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

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

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

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

@ -0,0 +1,472 @@
# -*- coding: utf-8 -*-
# BSD 2-Clause License
#
# Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# 1. Redistributions of source code must retain the above copyright notice,
# this list of conditions and the following disclaimer.
#
# 2. Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
import re
import requests
from json import dumps
from itertools import chain
from .NotifyBase import NotifyBase
from ..common import NotifyType
from ..AppriseLocale import gettext_lazy as _
from ..common import NotifyImageSize
from ..utils import parse_list, parse_bool
from ..utils import validate_regex
# Used to break path apart into list of channels
CHANNEL_LIST_DELIM = re.compile(r'[ \t\r\n,#\\/]+')
CHANNEL_REGEX = re.compile(
r'^\s*(\#|\%35)?(?P<channel>[0-9]+)', re.I)
# For API Details see:
# https://notifiarr.wiki/Client/Installation
# Another good example:
# https://notifiarr.wiki/en/Website/ \
# Integrations/Passthrough#payload-example-1
class NotifyNotifiarr(NotifyBase):
"""
A wrapper for Notifiarr Notifications
"""
# The default descriptive name associated with the Notification
service_name = 'Notifiarr'
# The services URL
service_url = 'https://notifiarr.com/'
# The default secure protocol
secure_protocol = 'notifiarr'
# A URL that takes you to the setup/help of the specific protocol
setup_url = 'https://github.com/caronc/apprise/wiki/Notify_notifiarr'
# The Notification URL
notify_url = 'https://notifiarr.com/api/v1/notification/apprise'
# Notifiarr Throttling (knowing in advance reduces 429 responses)
# define('NOTIFICATION_LIMIT_SECOND_USER', 5);
# define('NOTIFICATION_LIMIT_SECOND_PATRON', 15);
# Throttle requests ever so slightly
request_rate_per_sec = 0.04
# Allows the user to specify the NotifyImageSize object
image_size = NotifyImageSize.XY_256
# Define object templates
templates = (
'{schema}://{apikey}/{targets}',
)
# Define our apikeys; these are the minimum apikeys required required to
# be passed into this function (as arguments). The syntax appends any
# previously defined in the base package and builds onto them
template_tokens = dict(NotifyBase.template_tokens, **{
'apikey': {
'name': _('Token'),
'type': 'string',
'required': True,
'private': True,
},
'target_channel': {
'name': _('Target Channel'),
'type': 'string',
'prefix': '#',
'map_to': 'targets',
},
'targets': {
'name': _('Targets'),
'type': 'list:string',
'required': True,
},
})
# Define our template arguments
template_args = dict(NotifyBase.template_args, **{
'key': {
'alias_of': 'apikey',
},
'apikey': {
'alias_of': 'apikey',
},
'discord_user': {
'name': _('Ping Discord User'),
'type': 'int',
},
'discord_role': {
'name': _('Ping Discord Role'),
'type': 'int',
},
'event': {
'name': _('Discord Event ID'),
'type': 'int',
},
'image': {
'name': _('Include Image'),
'type': 'bool',
'default': False,
'map_to': 'include_image',
},
'source': {
'name': _('Source'),
'type': 'string',
},
'from': {
'alias_of': 'source'
},
'to': {
'alias_of': 'targets',
},
})
def __init__(self, apikey=None, include_image=None,
discord_user=None, discord_role=None,
event=None, targets=None, source=None, **kwargs):
"""
Initialize Notifiarr Object
headers can be a dictionary of key/value pairs that you want to
additionally include as part of the server headers to post with
"""
super().__init__(**kwargs)
self.apikey = apikey
if not self.apikey:
msg = 'An invalid Notifiarr APIKey ' \
'({}) was specified.'.format(apikey)
self.logger.warning(msg)
raise TypeError(msg)
# Place a thumbnail image inline with the message body
self.include_image = include_image \
if isinstance(include_image, bool) \
else self.template_args['image']['default']
# Set up our user if specified
self.discord_user = 0
if discord_user:
try:
self.discord_user = int(discord_user)
except (ValueError, TypeError):
msg = 'An invalid Notifiarr User ID ' \
'({}) was specified.'.format(discord_user)
self.logger.warning(msg)
raise TypeError(msg)
# Set up our role if specified
self.discord_role = 0
if discord_role:
try:
self.discord_role = int(discord_role)
except (ValueError, TypeError):
msg = 'An invalid Notifiarr Role ID ' \
'({}) was specified.'.format(discord_role)
self.logger.warning(msg)
raise TypeError(msg)
# Prepare our source (if set)
self.source = validate_regex(source)
self.event = 0
if event:
try:
self.event = int(event)
except (ValueError, TypeError):
msg = 'An invalid Notifiarr Discord Event ID ' \
'({}) was specified.'.format(event)
self.logger.warning(msg)
raise TypeError(msg)
# Prepare our targets
self.targets = {
'channels': [],
'invalid': [],
}
for target in parse_list(targets):
result = CHANNEL_REGEX.match(target)
if result:
# Store role information
self.targets['channels'].append(int(result.group('channel')))
continue
self.logger.warning(
'Dropped invalid channel '
'({}) specified.'.format(target),
)
self.targets['invalid'].append(target)
return
def url(self, privacy=False, *args, **kwargs):
"""
Returns the URL built dynamically based on specified arguments.
"""
# Define any URL parameters
params = {
'image': 'yes' if self.include_image else 'no',
}
if self.source:
params['source'] = self.source
if self.discord_user:
params['discord_user'] = self.discord_user
if self.discord_role:
params['discord_role'] = self.discord_role
if self.event:
params['event'] = self.event
# Extend our parameters
params.update(self.url_parameters(privacy=privacy, *args, **kwargs))
return '{schema}://{apikey}' \
'/{targets}?{params}'.format(
schema=self.secure_protocol,
apikey=self.pprint(self.apikey, privacy, safe=''),
targets='/'.join(
[NotifyNotifiarr.quote(x, safe='+#@') for x in chain(
# Channels
['#{}'.format(x) for x in self.targets['channels']],
# Pass along the same invalid entries as were provided
self.targets['invalid'],
)]),
params=NotifyNotifiarr.urlencode(params),
)
def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs):
"""
Perform Notifiarr Notification
"""
if not self.targets['channels']:
# There were no services to notify
self.logger.warning(
'There were no Notifiarr channels to notify.')
return False
# No error to start with
has_error = False
# Acquire image_url
image_url = self.image_url(notify_type)
for idx, channel in enumerate(self.targets['channels']):
# prepare Notifiarr Object
payload = {
'source': self.source if self.source else self.app_id,
'type': notify_type,
'notification': {
'update': True if self.event else False,
'name': self.app_id,
'event': str(self.event)
if self.event else "",
},
'discord': {
'color': self.color(notify_type),
'ping': {
'pingUser': self.discord_user
if not idx and self.discord_user else 0,
'pingRole': self.discord_role
if not idx and self.discord_role else 0,
},
'text': {
'title': title,
'content': '',
'description': body,
'footer': self.app_desc,
},
'ids': {
'channel': channel,
}
}
}
if self.include_image and image_url:
payload['discord']['text']['icon'] = image_url
payload['discord']['images'] = {
'thumbnail': image_url,
}
if not self._send(payload):
has_error = True
return not has_error
def _send(self, payload):
"""
Send notification
"""
self.logger.debug('Notifiarr POST URL: %s (cert_verify=%r)' % (
self.notify_url, self.verify_certificate,
))
self.logger.debug('Notifiarr Payload: %s' % str(payload))
# Prepare HTTP Headers
headers = {
'User-Agent': self.app_id,
'Content-Type': 'application/json',
'Accept': 'text/plain',
'X-api-Key': self.apikey,
}
# Always call throttle before any remote server i/o is made
self.throttle()
try:
r = requests.post(
self.notify_url,
data=dumps(payload),
headers=headers,
verify=self.verify_certificate,
timeout=self.request_timeout,
)
if r.status_code < 200 or r.status_code >= 300:
# We had a problem
status_str = \
NotifyNotifiarr.http_response_code_lookup(r.status_code)
self.logger.warning(
'Failed to send Notifiarr %s notification: '
'%serror=%s.',
status_str,
', ' if status_str else '',
str(r.status_code))
self.logger.debug('Response Details:\r\n{}'.format(r.content))
# Return; we're done
return False
else:
self.logger.info('Sent Notifiarr notification.')
except requests.RequestException as e:
self.logger.warning(
'A Connection error occurred sending Notifiarr '
'Chat notification to %s.' % self.host)
self.logger.debug('Socket Exception: %s' % str(e))
# Return; we're done
return False
return True
def __len__(self):
"""
Returns the number of targets associated with this notification
"""
targets = len(self.targets['channels']) + len(self.targets['invalid'])
return targets if targets > 0 else 1
@staticmethod
def parse_url(url):
"""
Parses the URL and returns enough arguments that can allow
us to re-instantiate this object.
"""
results = NotifyBase.parse_url(url, verify_host=False)
if not results:
# We're done early as we couldn't load the results
return results
# Get channels
results['targets'] = NotifyNotifiarr.split_path(results['fullpath'])
if 'discord_user' in results['qsd'] and \
len(results['qsd']['discord_user']):
results['discord_user'] = \
NotifyNotifiarr.unquote(
results['qsd']['discord_user'])
if 'discord_role' in results['qsd'] and \
len(results['qsd']['discord_role']):
results['discord_role'] = \
NotifyNotifiarr.unquote(results['qsd']['discord_role'])
if 'event' in results['qsd'] and \
len(results['qsd']['event']):
results['event'] = \
NotifyNotifiarr.unquote(results['qsd']['event'])
# Include images with our message
results['include_image'] = \
parse_bool(results['qsd'].get('image', False))
# Track if we need to extract the hostname as a target
host_is_potential_target = False
if 'source' in results['qsd'] and len(results['qsd']['source']):
results['source'] = \
NotifyNotifiarr.unquote(results['qsd']['source'])
elif 'from' in results['qsd'] and len(results['qsd']['from']):
results['source'] = \
NotifyNotifiarr.unquote(results['qsd']['from'])
# Set our apikey if found as an argument
if 'apikey' in results['qsd'] and len(results['qsd']['apikey']):
results['apikey'] = \
NotifyNotifiarr.unquote(results['qsd']['apikey'])
host_is_potential_target = True
elif 'key' in results['qsd'] and len(results['qsd']['key']):
results['apikey'] = \
NotifyNotifiarr.unquote(results['qsd']['key'])
host_is_potential_target = True
else:
# Pop the first element (this is the api key)
results['apikey'] = \
NotifyNotifiarr.unquote(results['host'])
if host_is_potential_target is True and results['host']:
results['targets'].append(NotifyNotifiarr.unquote(results['host']))
# Support the 'to' variable so that we can support rooms this way too
# The 'to' makes it easier to use yaml configuration
if 'to' in results['qsd'] and len(results['qsd']['to']):
results['targets'] += [x for x in filter(
bool, CHANNEL_LIST_DELIM.split(
NotifyNotifiarr.unquote(results['qsd']['to'])))]
return results

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

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

@ -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',
},
})

@ -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
@ -146,6 +142,7 @@ class NotifyOneSignal(NotifyBase):
'targets': {
'name': _('Targets'),
'type': 'list:string',
'required': True,
},
})

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

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

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

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

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

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

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

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

@ -0,0 +1,221 @@
# -*- coding: utf-8 -*-
# BSD 2-Clause License
#
# Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# 1. Redistributions of source code must retain the above copyright notice,
# this list of conditions and the following disclaimer.
#
# 2. Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
import requests
from .NotifyBase import NotifyBase
from ..common import NotifyType
from ..common import NotifyFormat
from ..utils import validate_regex
from ..utils import parse_bool
from ..AppriseLocale import gettext_lazy as _
class NotifyPushMe(NotifyBase):
"""
A wrapper for PushMe Notifications
"""
# The default descriptive name associated with the Notification
service_name = 'PushMe'
# The services URL
service_url = 'https://push.i-i.me/'
# Insecure protocol (for those self hosted requests)
protocol = 'pushme'
# A URL that takes you to the setup/help of the specific protocol
setup_url = 'https://github.com/caronc/apprise/wiki/Notify_pushme'
# PushMe URL
notify_url = 'https://push.i-i.me/'
# Define object templates
templates = (
'{schema}://{token}',
)
# Define our template tokens
template_tokens = dict(NotifyBase.template_tokens, **{
'token': {
'name': _('Token'),
'type': 'string',
'private': True,
'required': True,
},
})
# Define our template arguments
template_args = dict(NotifyBase.template_args, **{
'token': {
'alias_of': 'token',
},
'push_key': {
'alias_of': 'token',
},
'status': {
'name': _('Show Status'),
'type': 'bool',
'default': True,
},
})
def __init__(self, token, status=None, **kwargs):
"""
Initialize PushMe Object
"""
super().__init__(**kwargs)
# Token (associated with project)
self.token = validate_regex(token)
if not self.token:
msg = 'An invalid PushMe Token ' \
'({}) was specified.'.format(token)
self.logger.warning(msg)
raise TypeError(msg)
# Set Status type
self.status = status
return
def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs):
"""
Perform PushMe Notification
"""
headers = {
'User-Agent': self.app_id,
}
# Prepare our payload
params = {
'push_key': self.token,
'title': title if not self.status
else '{} {}'.format(self.asset.ascii(notify_type), title),
'content': body,
'type': 'markdown'
if self.notify_format == NotifyFormat.MARKDOWN else 'text'
}
self.logger.debug('PushMe POST URL: %s (cert_verify=%r)' % (
self.notify_url, self.verify_certificate,
))
self.logger.debug('PushMe Payload: %s' % str(params))
# Always call throttle before any remote server i/o is made
self.throttle()
try:
r = requests.post(
self.notify_url,
params=params,
headers=headers,
verify=self.verify_certificate,
timeout=self.request_timeout,
)
if r.status_code != requests.codes.ok:
# We had a problem
status_str = \
NotifyPushMe.http_response_code_lookup(r.status_code)
self.logger.warning(
'Failed to send PushMe notification:'
'{}{}error={}.'.format(
status_str,
', ' if status_str else '',
r.status_code))
self.logger.debug('Response Details:\r\n{}'.format(r.content))
# Return; we're done
return False
else:
self.logger.info('Sent PushMe notification.')
except requests.RequestException as e:
self.logger.warning(
'A Connection error occurred sending PushMe notification.',
)
self.logger.debug('Socket Exception: %s' % str(e))
# Return; we're done
return False
return True
def url(self, privacy=False, *args, **kwargs):
"""
Returns the URL built dynamically based on specified arguments.
"""
# Define any URL parameters
params = {
'status': 'yes' if self.status else 'no',
}
# Extend our parameters
params.update(self.url_parameters(privacy=privacy, *args, **kwargs))
# Official URLs are easy to assemble
return '{schema}://{token}/?{params}'.format(
schema=self.protocol,
token=self.pprint(self.token, privacy, safe=''),
params=NotifyPushMe.urlencode(params),
)
@staticmethod
def parse_url(url):
"""
Parses the URL and returns enough arguments that can allow
us to re-instantiate this object.
"""
results = NotifyBase.parse_url(url, verify_host=False)
if not results:
# We're done early as we couldn't load the results
return results
# Store our token using the host
results['token'] = NotifyPushMe.unquote(results['host'])
# The 'token' makes it easier to use yaml configuration
if 'token' in results['qsd'] and len(results['qsd']['token']):
results['token'] = NotifyPushMe.unquote(results['qsd']['token'])
elif 'push_key' in results['qsd'] and len(results['qsd']['push_key']):
# Support 'push_key' if specified
results['token'] = NotifyPushMe.unquote(results['qsd']['push_key'])
# Get status switch
results['status'] = \
parse_bool(results['qsd'].get('status', True))
return results

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

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

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

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

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

@ -0,0 +1,376 @@
# -*- coding: utf-8 -*-
# BSD 2-Clause License
#
# Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# 1. Redistributions of source code must retain the above copyright notice,
# this list of conditions and the following disclaimer.
#
# 2. Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
import os
import socket
from .NotifyBase import NotifyBase
from ..common import NotifyType
from ..utils import parse_bool
from ..AppriseLocale import gettext_lazy as _
class syslog:
"""
Extrapoloated information from the syslog library so that this plugin
would not be dependent on it.
"""
# Notification Categories
LOG_KERN = 0
LOG_USER = 8
LOG_MAIL = 16
LOG_DAEMON = 24
LOG_AUTH = 32
LOG_SYSLOG = 40
LOG_LPR = 48
LOG_NEWS = 56
LOG_UUCP = 64
LOG_CRON = 72
LOG_LOCAL0 = 128
LOG_LOCAL1 = 136
LOG_LOCAL2 = 144
LOG_LOCAL3 = 152
LOG_LOCAL4 = 160
LOG_LOCAL5 = 168
LOG_LOCAL6 = 176
LOG_LOCAL7 = 184
# Notification Types
LOG_INFO = 6
LOG_NOTICE = 5
LOG_WARNING = 4
LOG_CRIT = 2
class SyslogFacility:
"""
All of the supported facilities
"""
KERN = 'kern'
USER = 'user'
MAIL = 'mail'
DAEMON = 'daemon'
AUTH = 'auth'
SYSLOG = 'syslog'
LPR = 'lpr'
NEWS = 'news'
UUCP = 'uucp'
CRON = 'cron'
LOCAL0 = 'local0'
LOCAL1 = 'local1'
LOCAL2 = 'local2'
LOCAL3 = 'local3'
LOCAL4 = 'local4'
LOCAL5 = 'local5'
LOCAL6 = 'local6'
LOCAL7 = 'local7'
SYSLOG_FACILITY_MAP = {
SyslogFacility.KERN: syslog.LOG_KERN,
SyslogFacility.USER: syslog.LOG_USER,
SyslogFacility.MAIL: syslog.LOG_MAIL,
SyslogFacility.DAEMON: syslog.LOG_DAEMON,
SyslogFacility.AUTH: syslog.LOG_AUTH,
SyslogFacility.SYSLOG: syslog.LOG_SYSLOG,
SyslogFacility.LPR: syslog.LOG_LPR,
SyslogFacility.NEWS: syslog.LOG_NEWS,
SyslogFacility.UUCP: syslog.LOG_UUCP,
SyslogFacility.CRON: syslog.LOG_CRON,
SyslogFacility.LOCAL0: syslog.LOG_LOCAL0,
SyslogFacility.LOCAL1: syslog.LOG_LOCAL1,
SyslogFacility.LOCAL2: syslog.LOG_LOCAL2,
SyslogFacility.LOCAL3: syslog.LOG_LOCAL3,
SyslogFacility.LOCAL4: syslog.LOG_LOCAL4,
SyslogFacility.LOCAL5: syslog.LOG_LOCAL5,
SyslogFacility.LOCAL6: syslog.LOG_LOCAL6,
SyslogFacility.LOCAL7: syslog.LOG_LOCAL7,
}
SYSLOG_FACILITY_RMAP = {
syslog.LOG_KERN: SyslogFacility.KERN,
syslog.LOG_USER: SyslogFacility.USER,
syslog.LOG_MAIL: SyslogFacility.MAIL,
syslog.LOG_DAEMON: SyslogFacility.DAEMON,
syslog.LOG_AUTH: SyslogFacility.AUTH,
syslog.LOG_SYSLOG: SyslogFacility.SYSLOG,
syslog.LOG_LPR: SyslogFacility.LPR,
syslog.LOG_NEWS: SyslogFacility.NEWS,
syslog.LOG_UUCP: SyslogFacility.UUCP,
syslog.LOG_CRON: SyslogFacility.CRON,
syslog.LOG_LOCAL0: SyslogFacility.LOCAL0,
syslog.LOG_LOCAL1: SyslogFacility.LOCAL1,
syslog.LOG_LOCAL2: SyslogFacility.LOCAL2,
syslog.LOG_LOCAL3: SyslogFacility.LOCAL3,
syslog.LOG_LOCAL4: SyslogFacility.LOCAL4,
syslog.LOG_LOCAL5: SyslogFacility.LOCAL5,
syslog.LOG_LOCAL6: SyslogFacility.LOCAL6,
syslog.LOG_LOCAL7: SyslogFacility.LOCAL7,
}
# Used as a lookup when handling the Apprise -> Syslog Mapping
SYSLOG_PUBLISH_MAP = {
NotifyType.INFO: syslog.LOG_INFO,
NotifyType.SUCCESS: syslog.LOG_NOTICE,
NotifyType.FAILURE: syslog.LOG_CRIT,
NotifyType.WARNING: syslog.LOG_WARNING,
}
class NotifyRSyslog(NotifyBase):
"""
A wrapper for Remote Syslog Notifications
"""
# The default descriptive name associated with the Notification
service_name = 'Remote Syslog'
# The services URL
service_url = 'https://tools.ietf.org/html/rfc5424'
# The default protocol
protocol = 'rsyslog'
# A URL that takes you to the setup/help of the specific protocol
setup_url = 'https://github.com/caronc/apprise/wiki/Notify_rsyslog'
# Disable throttle rate for RSyslog requests
request_rate_per_sec = 0
# Define object templates
templates = (
'{schema}://{host}',
'{schema}://{host}:{port}',
'{schema}://{host}/{facility}',
'{schema}://{host}:{port}/{facility}',
)
# Define our template tokens
template_tokens = dict(NotifyBase.template_tokens, **{
'facility': {
'name': _('Facility'),
'type': 'choice:string',
'values': [k for k in SYSLOG_FACILITY_MAP.keys()],
'default': SyslogFacility.USER,
'required': True,
},
'host': {
'name': _('Hostname'),
'type': 'string',
'required': True,
},
'port': {
'name': _('Port'),
'type': 'int',
'min': 1,
'max': 65535,
'default': 514,
},
})
# Define our template arguments
template_args = dict(NotifyBase.template_args, **{
'facility': {
# We map back to the same element defined in template_tokens
'alias_of': 'facility',
},
'logpid': {
'name': _('Log PID'),
'type': 'bool',
'default': True,
'map_to': 'log_pid',
},
})
def __init__(self, facility=None, log_pid=True, **kwargs):
"""
Initialize RSyslog Object
"""
super().__init__(**kwargs)
if facility:
try:
self.facility = SYSLOG_FACILITY_MAP[facility]
except KeyError:
msg = 'An invalid syslog facility ' \
'({}) was specified.'.format(facility)
self.logger.warning(msg)
raise TypeError(msg)
else:
self.facility = \
SYSLOG_FACILITY_MAP[
self.template_tokens['facility']['default']]
# Include PID with each message.
self.log_pid = log_pid
return
def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs):
"""
Perform RSyslog Notification
"""
if title:
# Format title
body = '{}: {}'.format(title, body)
# Always call throttle before any remote server i/o is made
self.throttle()
host = self.host
port = self.port if self.port \
else self.template_tokens['port']['default']
if self.log_pid:
payload = '<%d>- %d - %s' % (
SYSLOG_PUBLISH_MAP[notify_type] + self.facility * 8,
os.getpid(), body)
else:
payload = '<%d>- %s' % (
SYSLOG_PUBLISH_MAP[notify_type] + self.facility * 8, body)
# send UDP packet to upstream server
self.logger.debug(
'RSyslog Host: %s:%d/%s',
host, port, SYSLOG_FACILITY_RMAP[self.facility])
self.logger.debug('RSyslog Payload: %s' % str(payload))
# our sent bytes
sent = 0
try:
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.settimeout(self.socket_connect_timeout)
sent = sock.sendto(payload.encode('utf-8'), (host, port))
sock.close()
except socket.gaierror as e:
self.logger.warning(
'A connection error occurred sending RSyslog '
'notification to %s:%d/%s', host, port,
SYSLOG_FACILITY_RMAP[self.facility]
)
self.logger.debug('Socket Exception: %s' % str(e))
return False
except socket.timeout as e:
self.logger.warning(
'A connection timeout occurred sending RSyslog '
'notification to %s:%d/%s', host, port,
SYSLOG_FACILITY_RMAP[self.facility]
)
self.logger.debug('Socket Exception: %s' % str(e))
return False
if sent < len(payload):
self.logger.warning(
'RSyslog sent %d byte(s) but intended to send %d byte(s)',
sent, len(payload))
return False
self.logger.info('Sent RSyslog notification.')
return True
def url(self, privacy=False, *args, **kwargs):
"""
Returns the URL built dynamically based on specified arguments.
"""
# Define any URL parameters
params = {
'logpid': 'yes' if self.log_pid else 'no',
}
# Extend our parameters
params.update(self.url_parameters(privacy=privacy, *args, **kwargs))
return '{schema}://{hostname}{port}/{facility}/?{params}'.format(
schema=self.protocol,
hostname=NotifyRSyslog.quote(self.host, safe=''),
port='' if self.port is None
or self.port == self.template_tokens['port']['default']
else ':{}'.format(self.port),
facility=self.template_tokens['facility']['default']
if self.facility not in SYSLOG_FACILITY_RMAP
else SYSLOG_FACILITY_RMAP[self.facility],
params=NotifyRSyslog.urlencode(params),
)
@staticmethod
def parse_url(url):
"""
Parses the URL and returns enough arguments that can allow
us to re-instantiate this object.
"""
results = NotifyBase.parse_url(url, verify_host=False)
if not results:
# We're done early as we couldn't load the results
return results
tokens = []
# Get our path values
tokens.extend(NotifyRSyslog.split_path(results['fullpath']))
# Initialization
facility = None
if tokens:
# Store the last entry as the facility
facility = tokens[-1].lower()
# However if specified on the URL, that will over-ride what was
# identified
if 'facility' in results['qsd'] and len(results['qsd']['facility']):
facility = results['qsd']['facility'].lower()
if facility and facility not in SYSLOG_FACILITY_MAP:
# Find first match; if no match is found we set the result
# to the matching key. This allows us to throw a TypeError
# during the __init__() call. The benifit of doing this
# check here is if we do have a valid match, we can support
# short form matches like 'u' which will match against user
facility = next((f for f in SYSLOG_FACILITY_MAP.keys()
if f.startswith(facility)), facility)
# Save facility if set
if facility:
results['facility'] = facility
# Include PID as part of the message logged
results['log_pid'] = parse_bool(
results['qsd'].get(
'logpid',
NotifyRSyslog.template_args['logpid']['default']))
return results

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
# BSD 3-Clause License
# BSD 2-Clause License
#
# Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
# 3. Neither the name of the copyright holder nor the names of its
# contributors may be used to endorse or promote products derived from
# this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
@ -30,7 +26,6 @@
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
#
# 1. Visit https://www.reddit.com/prefs/apps and scroll to the bottom
# 2. Click on the button that reads 'are you a developer? create an app...'
# 3. Set the mode to `script`,
@ -56,6 +51,7 @@ import requests
from json import loads
from datetime import timedelta
from datetime import datetime
from datetime import timezone
from .NotifyBase import NotifyBase
from ..URLBase import PrivacyMode
@ -133,12 +129,6 @@ class NotifyReddit(NotifyBase):
# still allow to make.
request_rate_per_sec = 0
# For Tracking Purposes
ratelimit_reset = datetime.utcnow()
# Default to 1.0
ratelimit_remaining = 1.0
# Taken right from google.auth.helpers:
clock_skew = timedelta(seconds=10)
@ -185,6 +175,7 @@ class NotifyReddit(NotifyBase):
'targets': {
'name': _('Targets'),
'type': 'list:string',
'required': True,
},
})
@ -275,7 +266,7 @@ class NotifyReddit(NotifyBase):
# Our keys we build using the provided content
self.__refresh_token = None
self.__access_token = None
self.__access_token_expiry = datetime.utcnow()
self.__access_token_expiry = datetime.now(timezone.utc)
self.kind = kind.strip().lower() \
if isinstance(kind, str) \
@ -324,6 +315,13 @@ class NotifyReddit(NotifyBase):
if not self.subreddits:
self.logger.warning(
'No subreddits were identified to be notified')
# For Rate Limit Tracking Purposes
self.ratelimit_reset = datetime.now(timezone.utc).replace(tzinfo=None)
# Default to 1.0
self.ratelimit_remaining = 1.0
return
def url(self, privacy=False, *args, **kwargs):
@ -417,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(
@ -544,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(
@ -671,8 +669,9 @@ class NotifyReddit(NotifyBase):
self.ratelimit_remaining = \
float(r.headers.get(
'X-RateLimit-Remaining'))
self.ratelimit_reset = datetime.utcfromtimestamp(
int(r.headers.get('X-RateLimit-Reset')))
self.ratelimit_reset = datetime.fromtimestamp(
int(r.headers.get('X-RateLimit-Reset')), timezone.utc
).replace(tzinfo=None)
except (TypeError, ValueError):
# This is returned if we could not retrieve this information

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

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

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

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

Loading…
Cancel
Save