summaryrefslogtreecommitdiffhomepage
path: root/libs/apprise
diff options
context:
space:
mode:
authorJeff Byrnes <[email protected]>2023-11-28 16:12:04 -0500
committerGitHub <[email protected]>2023-11-28 16:12:04 -0500
commit55c5384f9cd3b168167343a8177a07732cf1af94 (patch)
treed8e44cd8ffe8564929cdd9925b95ed8e0aac9692 /libs/apprise
parentcb2023d94e0603aa28c258c4f201300285a74b68 (diff)
downloadbazarr-55c5384f9cd3b168167343a8177a07732cf1af94.tar.gz
bazarr-55c5384f9cd3b168167343a8177a07732cf1af94.zip
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
Diffstat (limited to 'libs/apprise')
-rw-r--r--libs/apprise/Apprise.py89
-rw-r--r--libs/apprise/AppriseAsset.py6
-rw-r--r--libs/apprise/AppriseAttachment.py6
-rw-r--r--libs/apprise/AppriseConfig.py6
-rw-r--r--libs/apprise/AppriseLocale.py231
-rw-r--r--libs/apprise/URLBase.py100
-rw-r--r--libs/apprise/__init__.py8
-rw-r--r--libs/apprise/attachment/AttachBase.py9
-rw-r--r--libs/apprise/attachment/AttachFile.py6
-rw-r--r--libs/apprise/attachment/AttachHTTP.py6
-rw-r--r--libs/apprise/attachment/__init__.py6
-rw-r--r--libs/apprise/cli.py6
-rw-r--r--libs/apprise/common.py6
-rw-r--r--libs/apprise/config/ConfigBase.py284
-rw-r--r--libs/apprise/config/ConfigFile.py6
-rw-r--r--libs/apprise/config/ConfigHTTP.py6
-rw-r--r--libs/apprise/config/ConfigMemory.py6
-rw-r--r--libs/apprise/config/__init__.py6
-rw-r--r--libs/apprise/conversion.py6
-rw-r--r--libs/apprise/decorators/CustomNotifyPlugin.py7
-rw-r--r--libs/apprise/decorators/__init__.py6
-rw-r--r--libs/apprise/decorators/notify.py6
-rw-r--r--libs/apprise/i18n/en/LC_MESSAGES/apprise.mobin455 -> 3959 bytes
-rw-r--r--libs/apprise/logger.py6
-rw-r--r--libs/apprise/plugins/NotifyAppriseAPI.py24
-rw-r--r--libs/apprise/plugins/NotifyBark.py9
-rw-r--r--libs/apprise/plugins/NotifyBase.py42
-rw-r--r--libs/apprise/plugins/NotifyBoxcar.py25
-rw-r--r--libs/apprise/plugins/NotifyBulkSMS.py9
-rw-r--r--libs/apprise/plugins/NotifyBurstSMS.py460
-rw-r--r--libs/apprise/plugins/NotifyClickSend.py6
-rw-r--r--libs/apprise/plugins/NotifyD7Networks.py7
-rw-r--r--libs/apprise/plugins/NotifyDBus.py6
-rw-r--r--libs/apprise/plugins/NotifyDapnet.py6
-rw-r--r--libs/apprise/plugins/NotifyDingTalk.py15
-rw-r--r--libs/apprise/plugins/NotifyDiscord.py257
-rw-r--r--libs/apprise/plugins/NotifyEmail.py30
-rw-r--r--libs/apprise/plugins/NotifyEmby.py6
-rw-r--r--libs/apprise/plugins/NotifyEnigma2.py6
-rw-r--r--libs/apprise/plugins/NotifyFCM/__init__.py8
-rw-r--r--libs/apprise/plugins/NotifyFCM/color.py6
-rw-r--r--libs/apprise/plugins/NotifyFCM/common.py6
-rw-r--r--libs/apprise/plugins/NotifyFCM/oauth.py17
-rw-r--r--libs/apprise/plugins/NotifyFCM/priority.py6
-rw-r--r--libs/apprise/plugins/NotifyFaast.py6
-rw-r--r--libs/apprise/plugins/NotifyFlock.py13
-rw-r--r--libs/apprise/plugins/NotifyForm.py11
-rw-r--r--libs/apprise/plugins/NotifyGitter.py425
-rw-r--r--libs/apprise/plugins/NotifyGnome.py6
-rw-r--r--libs/apprise/plugins/NotifyGoogleChat.py6
-rw-r--r--libs/apprise/plugins/NotifyGotify.py7
-rw-r--r--libs/apprise/plugins/NotifyGrowl.py6
-rw-r--r--libs/apprise/plugins/NotifyGuilded.py6
-rw-r--r--libs/apprise/plugins/NotifyHomeAssistant.py6
-rw-r--r--libs/apprise/plugins/NotifyIFTTT.py7
-rw-r--r--libs/apprise/plugins/NotifyJSON.py77
-rw-r--r--libs/apprise/plugins/NotifyJoin.py7
-rw-r--r--libs/apprise/plugins/NotifyKavenegar.py6
-rw-r--r--libs/apprise/plugins/NotifyKumulos.py6
-rw-r--r--libs/apprise/plugins/NotifyLametric.py8
-rw-r--r--libs/apprise/plugins/NotifyLine.py7
-rw-r--r--libs/apprise/plugins/NotifyMQTT.py10
-rw-r--r--libs/apprise/plugins/NotifyMSG91.py226
-rw-r--r--libs/apprise/plugins/NotifyMSTeams.py6
-rw-r--r--libs/apprise/plugins/NotifyMacOSX.py9
-rw-r--r--libs/apprise/plugins/NotifyMailgun.py18
-rw-r--r--libs/apprise/plugins/NotifyMastodon.py25
-rw-r--r--libs/apprise/plugins/NotifyMatrix.py256
-rw-r--r--libs/apprise/plugins/NotifyMatterMost.py12
-rw-r--r--libs/apprise/plugins/NotifyMattermost.py372
-rw-r--r--libs/apprise/plugins/NotifyMessageBird.py6
-rw-r--r--libs/apprise/plugins/NotifyMisskey.py7
-rw-r--r--libs/apprise/plugins/NotifyNextcloud.py46
-rw-r--r--libs/apprise/plugins/NotifyNextcloudTalk.py62
-rw-r--r--libs/apprise/plugins/NotifyNotica.py18
-rw-r--r--libs/apprise/plugins/NotifyNotifiarr.py472
-rw-r--r--libs/apprise/plugins/NotifyNotifico.py6
-rw-r--r--libs/apprise/plugins/NotifyNtfy.py59
-rw-r--r--libs/apprise/plugins/NotifyOffice365.py13
-rw-r--r--libs/apprise/plugins/NotifyOneSignal.py7
-rw-r--r--libs/apprise/plugins/NotifyOpsgenie.py6
-rw-r--r--libs/apprise/plugins/NotifyPagerDuty.py8
-rw-r--r--libs/apprise/plugins/NotifyPagerTree.py6
-rw-r--r--libs/apprise/plugins/NotifyParsePlatform.py8
-rw-r--r--libs/apprise/plugins/NotifyPopcornNotify.py7
-rw-r--r--libs/apprise/plugins/NotifyProwl.py6
-rw-r--r--libs/apprise/plugins/NotifyPushBullet.py26
-rw-r--r--libs/apprise/plugins/NotifyPushDeer.py218
-rw-r--r--libs/apprise/plugins/NotifyPushMe.py221
-rw-r--r--libs/apprise/plugins/NotifyPushSafer.py11
-rw-r--r--libs/apprise/plugins/NotifyPushed.py6
-rw-r--r--libs/apprise/plugins/NotifyPushjet.py6
-rw-r--r--libs/apprise/plugins/NotifyPushover.py166
-rw-r--r--libs/apprise/plugins/NotifyPushy.py384
-rw-r--r--libs/apprise/plugins/NotifyRSyslog.py376
-rw-r--r--libs/apprise/plugins/NotifyReddit.py37
-rw-r--r--libs/apprise/plugins/NotifyRocketChat.py6
-rw-r--r--libs/apprise/plugins/NotifyRyver.py11
-rw-r--r--libs/apprise/plugins/NotifySES.py22
-rw-r--r--libs/apprise/plugins/NotifySMSEagle.py13
-rw-r--r--libs/apprise/plugins/NotifySMTP2Go.py13
-rw-r--r--libs/apprise/plugins/NotifySNS.py13
-rw-r--r--libs/apprise/plugins/NotifySendGrid.py7
-rw-r--r--libs/apprise/plugins/NotifyServerChan.py8
-rw-r--r--libs/apprise/plugins/NotifySignalAPI.py13
-rw-r--r--libs/apprise/plugins/NotifySimplePush.py10
-rw-r--r--libs/apprise/plugins/NotifySinch.py6
-rw-r--r--libs/apprise/plugins/NotifySlack.py25
-rw-r--r--libs/apprise/plugins/NotifySparkPost.py16
-rw-r--r--libs/apprise/plugins/NotifySpontit.py7
-rw-r--r--libs/apprise/plugins/NotifyStreamlabs.py9
-rw-r--r--libs/apprise/plugins/NotifySyslog.py193
-rw-r--r--libs/apprise/plugins/NotifyTechulusPush.py6
-rw-r--r--libs/apprise/plugins/NotifyTelegram.py110
-rw-r--r--libs/apprise/plugins/NotifyTwilio.py6
-rw-r--r--libs/apprise/plugins/NotifyTwist.py8
-rw-r--r--libs/apprise/plugins/NotifyTwitter.py28
-rw-r--r--libs/apprise/plugins/NotifyVoipms.py8
-rw-r--r--libs/apprise/plugins/NotifyVonage.py6
-rw-r--r--libs/apprise/plugins/NotifyWebexTeams.py6
-rw-r--r--libs/apprise/plugins/NotifyWhatsApp.py559
-rw-r--r--libs/apprise/plugins/NotifyWindows.py6
-rw-r--r--libs/apprise/plugins/NotifyXBMC.py6
-rw-r--r--libs/apprise/plugins/NotifyXML.py22
-rw-r--r--libs/apprise/plugins/NotifyZulip.py7
-rw-r--r--libs/apprise/plugins/__init__.py37
-rw-r--r--libs/apprise/utils.py28
127 files changed, 4886 insertions, 1836 deletions
diff --git a/libs/apprise/Apprise.py b/libs/apprise/Apprise.py
index 8c2cf5330..4c83c481f 100644
--- a/libs/apprise/Apprise.py
+++ b/libs/apprise/Apprise.py
@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
-# BSD 3-Clause License
+# BSD 2-Clause License
#
# Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <[email protected]>
@@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
-# 3. Neither the name of the copyright holder nor the names of its
-# contributors may be used to endorse or promote products derived from
-# this software without specific prior written permission.
-#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "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'.
diff --git a/libs/apprise/AppriseAsset.py b/libs/apprise/AppriseAsset.py
index 34821e278..835c3b6ad 100644
--- a/libs/apprise/AppriseAsset.py
+++ b/libs/apprise/AppriseAsset.py
@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
-# BSD 3-Clause License
+# BSD 2-Clause License
#
# Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <[email protected]>
@@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
-# 3. Neither the name of the copyright holder nor the names of its
-# contributors may be used to endorse or promote products derived from
-# this software without specific prior written permission.
-#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
diff --git a/libs/apprise/AppriseAttachment.py b/libs/apprise/AppriseAttachment.py
index 0a3913ed0..e00645d2d 100644
--- a/libs/apprise/AppriseAttachment.py
+++ b/libs/apprise/AppriseAttachment.py
@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
-# BSD 3-Clause License
+# BSD 2-Clause License
#
# Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <[email protected]>
@@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
-# 3. Neither the name of the copyright holder nor the names of its
-# contributors may be used to endorse or promote products derived from
-# this software without specific prior written permission.
-#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
diff --git a/libs/apprise/AppriseConfig.py b/libs/apprise/AppriseConfig.py
index 8f2857776..07e7b48ed 100644
--- a/libs/apprise/AppriseConfig.py
+++ b/libs/apprise/AppriseConfig.py
@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
-# BSD 3-Clause License
+# BSD 2-Clause License
#
# Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <[email protected]>
@@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
-# 3. Neither the name of the copyright holder nor the names of its
-# contributors may be used to endorse or promote products derived from
-# this software without specific prior written permission.
-#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
diff --git a/libs/apprise/AppriseLocale.py b/libs/apprise/AppriseLocale.py
index ce61d0c9b..c80afae27 100644
--- a/libs/apprise/AppriseLocale.py
+++ b/libs/apprise/AppriseLocale.py
@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
-# BSD 3-Clause License
+# BSD 2-Clause License
#
# Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <[email protected]>
@@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
-# 3. Neither the name of the copyright holder nor the names of its
-# contributors may be used to endorse or promote products derived from
-# this software without specific prior written permission.
-#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "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)
diff --git a/libs/apprise/URLBase.py b/libs/apprise/URLBase.py
index 4b33920ea..1cea66d15 100644
--- a/libs/apprise/URLBase.py
+++ b/libs/apprise/URLBase.py
@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
-# BSD 3-Clause License
+# BSD 2-Clause License
#
# Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <[email protected]>
@@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
-# 3. Neither the name of the copyright holder nor the names of its
-# contributors may be used to endorse or promote products derived from
-# this software without specific prior written permission.
-#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "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@[email protected]
+ # - 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']
diff --git a/libs/apprise/__init__.py b/libs/apprise/__init__.py
index 3a9136e96..f8bb5c752 100644
--- a/libs/apprise/__init__.py
+++ b/libs/apprise/__init__.py
@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
-# BSD 3-Clause License
+# BSD 2-Clause License
#
# Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <[email protected]>
@@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
-# 3. Neither the name of the copyright holder nor the names of its
-# contributors may be used to endorse or promote products derived from
-# this software without specific prior written permission.
-#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "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 <[email protected]>'
diff --git a/libs/apprise/attachment/AttachBase.py b/libs/apprise/attachment/AttachBase.py
index 2b05c8497..c1cadbf91 100644
--- a/libs/apprise/attachment/AttachBase.py
+++ b/libs/apprise/attachment/AttachBase.py
@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
-# BSD 3-Clause License
+# BSD 2-Clause License
#
# Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <[email protected]>
@@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
-# 3. Neither the name of the copyright holder nor the names of its
-# contributors may be used to endorse or promote products derived from
-# this software without specific prior written permission.
-#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "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
diff --git a/libs/apprise/attachment/AttachFile.py b/libs/apprise/attachment/AttachFile.py
index f89b915eb..d30855553 100644
--- a/libs/apprise/attachment/AttachFile.py
+++ b/libs/apprise/attachment/AttachFile.py
@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
-# BSD 3-Clause License
+# BSD 2-Clause License
#
# Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <[email protected]>
@@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
-# 3. Neither the name of the copyright holder nor the names of its
-# contributors may be used to endorse or promote products derived from
-# this software without specific prior written permission.
-#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
diff --git a/libs/apprise/attachment/AttachHTTP.py b/libs/apprise/attachment/AttachHTTP.py
index d8b46ff28..0c859477e 100644
--- a/libs/apprise/attachment/AttachHTTP.py
+++ b/libs/apprise/attachment/AttachHTTP.py
@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
-# BSD 3-Clause License
+# BSD 2-Clause License
#
# Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <[email protected]>
@@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
-# 3. Neither the name of the copyright holder nor the names of its
-# contributors may be used to endorse or promote products derived from
-# this software without specific prior written permission.
-#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
diff --git a/libs/apprise/attachment/__init__.py b/libs/apprise/attachment/__init__.py
index 1b0e1bfe6..ba7620a45 100644
--- a/libs/apprise/attachment/__init__.py
+++ b/libs/apprise/attachment/__init__.py
@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
-# BSD 3-Clause License
+# BSD 2-Clause License
#
# Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <[email protected]>
@@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
-# 3. Neither the name of the copyright holder nor the names of its
-# contributors may be used to endorse or promote products derived from
-# this software without specific prior written permission.
-#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
diff --git a/libs/apprise/cli.py b/libs/apprise/cli.py
index a3335bbb5..130351802 100644
--- a/libs/apprise/cli.py
+++ b/libs/apprise/cli.py
@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
-# BSD 3-Clause License
+# BSD 2-Clause License
#
# Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <[email protected]>
@@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
-# 3. Neither the name of the copyright holder nor the names of its
-# contributors may be used to endorse or promote products derived from
-# this software without specific prior written permission.
-#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
diff --git a/libs/apprise/common.py b/libs/apprise/common.py
index 8380c405e..aaf746eaa 100644
--- a/libs/apprise/common.py
+++ b/libs/apprise/common.py
@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
-# BSD 3-Clause License
+# BSD 2-Clause License
#
# Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <[email protected]>
@@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
-# 3. Neither the name of the copyright holder nor the names of its
-# contributors may be used to endorse or promote products derived from
-# this software without specific prior written permission.
-#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
diff --git a/libs/apprise/config/ConfigBase.py b/libs/apprise/config/ConfigBase.py
index 5eb73ebcb..adddc4f56 100644
--- a/libs/apprise/config/ConfigBase.py
+++ b/libs/apprise/config/ConfigBase.py
@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
-# BSD 3-Clause License
+# BSD 2-Clause License
#
# Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <[email protected]>
@@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
-# 3. Neither the name of the copyright holder nor the names of its
-# contributors may be used to endorse or promote products derived from
-# this software without specific prior written permission.
-#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
@@ -357,6 +353,77 @@ class ConfigBase(URLBase):
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)
diff --git a/libs/apprise/config/ConfigFile.py b/libs/apprise/config/ConfigFile.py
index b2c211766..719355130 100644
--- a/libs/apprise/config/ConfigFile.py
+++ b/libs/apprise/config/ConfigFile.py
@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
-# BSD 3-Clause License
+# BSD 2-Clause License
#
# Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <[email protected]>
@@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
-# 3. Neither the name of the copyright holder nor the names of its
-# contributors may be used to endorse or promote products derived from
-# this software without specific prior written permission.
-#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
diff --git a/libs/apprise/config/ConfigHTTP.py b/libs/apprise/config/ConfigHTTP.py
index 5813b90a9..8e8677c24 100644
--- a/libs/apprise/config/ConfigHTTP.py
+++ b/libs/apprise/config/ConfigHTTP.py
@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
-# BSD 3-Clause License
+# BSD 2-Clause License
#
# Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <[email protected]>
@@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
-# 3. Neither the name of the copyright holder nor the names of its
-# contributors may be used to endorse or promote products derived from
-# this software without specific prior written permission.
-#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
diff --git a/libs/apprise/config/ConfigMemory.py b/libs/apprise/config/ConfigMemory.py
index ec44e9b4f..110e04a3c 100644
--- a/libs/apprise/config/ConfigMemory.py
+++ b/libs/apprise/config/ConfigMemory.py
@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
-# BSD 3-Clause License
+# BSD 2-Clause License
#
# Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <[email protected]>
@@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
-# 3. Neither the name of the copyright holder nor the names of its
-# contributors may be used to endorse or promote products derived from
-# this software without specific prior written permission.
-#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
diff --git a/libs/apprise/config/__init__.py b/libs/apprise/config/__init__.py
index 7d03a34a8..4b7e3fd78 100644
--- a/libs/apprise/config/__init__.py
+++ b/libs/apprise/config/__init__.py
@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
-# BSD 3-Clause License
+# BSD 2-Clause License
#
# Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <[email protected]>
@@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
-# 3. Neither the name of the copyright holder nor the names of its
-# contributors may be used to endorse or promote products derived from
-# this software without specific prior written permission.
-#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
diff --git a/libs/apprise/conversion.py b/libs/apprise/conversion.py
index 77c9aa5e5..d3781f606 100644
--- a/libs/apprise/conversion.py
+++ b/libs/apprise/conversion.py
@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
-# BSD 3-Clause License
+# BSD 2-Clause License
#
# Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <[email protected]>
@@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
-# 3. Neither the name of the copyright holder nor the names of its
-# contributors may be used to endorse or promote products derived from
-# this software without specific prior written permission.
-#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
diff --git a/libs/apprise/decorators/CustomNotifyPlugin.py b/libs/apprise/decorators/CustomNotifyPlugin.py
index 9c8e7cb1d..5ccfded55 100644
--- a/libs/apprise/decorators/CustomNotifyPlugin.py
+++ b/libs/apprise/decorators/CustomNotifyPlugin.py
@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
-# BSD 3-Clause License
+# BSD 2-Clause License
#
# Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <[email protected]>
@@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
-# 3. Neither the name of the copyright holder nor the names of its
-# contributors may be used to endorse or promote products derived from
-# this software without specific prior written permission.
-#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "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
diff --git a/libs/apprise/decorators/__init__.py b/libs/apprise/decorators/__init__.py
index 699fd0da4..5b089bbf5 100644
--- a/libs/apprise/decorators/__init__.py
+++ b/libs/apprise/decorators/__init__.py
@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
-# BSD 3-Clause License
+# BSD 2-Clause License
#
# Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <[email protected]>
@@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
-# 3. Neither the name of the copyright holder nor the names of its
-# contributors may be used to endorse or promote products derived from
-# this software without specific prior written permission.
-#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
diff --git a/libs/apprise/decorators/notify.py b/libs/apprise/decorators/notify.py
index 36842b419..07b4ceb1e 100644
--- a/libs/apprise/decorators/notify.py
+++ b/libs/apprise/decorators/notify.py
@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
-# BSD 3-Clause License
+# BSD 2-Clause License
#
# Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <[email protected]>
@@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
-# 3. Neither the name of the copyright holder nor the names of its
-# contributors may be used to endorse or promote products derived from
-# this software without specific prior written permission.
-#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
diff --git a/libs/apprise/i18n/en/LC_MESSAGES/apprise.mo b/libs/apprise/i18n/en/LC_MESSAGES/apprise.mo
index 925d178f0..0236722fd 100644
--- a/libs/apprise/i18n/en/LC_MESSAGES/apprise.mo
+++ b/libs/apprise/i18n/en/LC_MESSAGES/apprise.mo
Binary files differ
diff --git a/libs/apprise/logger.py b/libs/apprise/logger.py
index 005a3e0d7..6a594ec60 100644
--- a/libs/apprise/logger.py
+++ b/libs/apprise/logger.py
@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
-# BSD 3-Clause License
+# BSD 2-Clause License
#
# Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <[email protected]>
@@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
-# 3. Neither the name of the copyright holder nor the names of its
-# contributors may be used to endorse or promote products derived from
-# this software without specific prior written permission.
-#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
diff --git a/libs/apprise/plugins/NotifyAppriseAPI.py b/libs/apprise/plugins/NotifyAppriseAPI.py
index b8765496f..3c85b8ac6 100644
--- a/libs/apprise/plugins/NotifyAppriseAPI.py
+++ b/libs/apprise/plugins/NotifyAppriseAPI.py
@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
-# BSD 3-Clause License
+# BSD 2-Clause License
#
# Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <[email protected]>
@@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
-# 3. Neither the name of the copyright holder nor the names of its
-# contributors may be used to endorse or promote products derived from
-# this software without specific prior written permission.
-#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "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({
diff --git a/libs/apprise/plugins/NotifyBark.py b/libs/apprise/plugins/NotifyBark.py
index f1c6d7bf9..edef82bd8 100644
--- a/libs/apprise/plugins/NotifyBark.py
+++ b/libs/apprise/plugins/NotifyBark.py
@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
-# BSD 3-Clause License
+# BSD 2-Clause License
#
# Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <[email protected]>
@@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
-# 3. Neither the name of the copyright holder nor the names of its
-# contributors may be used to endorse or promote products derived from
-# this software without specific prior written permission.
-#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "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,
},
})
diff --git a/libs/apprise/plugins/NotifyBase.py b/libs/apprise/plugins/NotifyBase.py
index 1b07baa71..5138c15c8 100644
--- a/libs/apprise/plugins/NotifyBase.py
+++ b/libs/apprise/plugins/NotifyBase.py
@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
-# BSD 3-Clause License
+# BSD 2-Clause License
#
# Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <[email protected]>
@@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
-# 3. Neither the name of the copyright holder nor the names of its
-# contributors may be used to endorse or promote products derived from
-# this software without specific prior written permission.
-#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "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
diff --git a/libs/apprise/plugins/NotifyBoxcar.py b/libs/apprise/plugins/NotifyBoxcar.py
index 8e7045c7b..9d3be6aec 100644
--- a/libs/apprise/plugins/NotifyBoxcar.py
+++ b/libs/apprise/plugins/NotifyBoxcar.py
@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
-# BSD 3-Clause License
+# BSD 2-Clause License
#
# Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <[email protected]>
@@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
-# 3. Neither the name of the copyright holder nor the names of its
-# contributors may be used to endorse or promote products derived from
-# this software without specific prior written permission.
-#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "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))
diff --git a/libs/apprise/plugins/NotifyBulkSMS.py b/libs/apprise/plugins/NotifyBulkSMS.py
index 814badaef..cf82a87a4 100644
--- a/libs/apprise/plugins/NotifyBulkSMS.py
+++ b/libs/apprise/plugins/NotifyBulkSMS.py
@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
-# BSD 3-Clause License
+# BSD 2-Clause License
#
# Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <[email protected]>
@@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
-# 3. Neither the name of the copyright holder nor the names of its
-# contributors may be used to endorse or promote products derived from
-# this software without specific prior written permission.
-#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "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,
},
})
diff --git a/libs/apprise/plugins/NotifyBurstSMS.py b/libs/apprise/plugins/NotifyBurstSMS.py
new file mode 100644
index 000000000..59219b3d1
--- /dev/null
+++ b/libs/apprise/plugins/NotifyBurstSMS.py
@@ -0,0 +1,460 @@
+# -*- coding: utf-8 -*-
+# BSD 2-Clause License
+#
+# Apprise - Push Notification Library.
+# Copyright (c) 2023, Chris Caron <[email protected]>
+#
+# 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
diff --git a/libs/apprise/plugins/NotifyClickSend.py b/libs/apprise/plugins/NotifyClickSend.py
index ed6e462fc..670e74e80 100644
--- a/libs/apprise/plugins/NotifyClickSend.py
+++ b/libs/apprise/plugins/NotifyClickSend.py
@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
-# BSD 3-Clause License
+# BSD 2-Clause License
#
# Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <[email protected]>
@@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
-# 3. Neither the name of the copyright holder nor the names of its
-# contributors may be used to endorse or promote products derived from
-# this software without specific prior written permission.
-#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
diff --git a/libs/apprise/plugins/NotifyD7Networks.py b/libs/apprise/plugins/NotifyD7Networks.py
index 3d0ee8aa4..3e7787da8 100644
--- a/libs/apprise/plugins/NotifyD7Networks.py
+++ b/libs/apprise/plugins/NotifyD7Networks.py
@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
-# BSD 3-Clause License
+# BSD 2-Clause License
#
# Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <[email protected]>
@@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
-# 3. Neither the name of the copyright holder nor the names of its
-# contributors may be used to endorse or promote products derived from
-# this software without specific prior written permission.
-#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "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,
},
})
diff --git a/libs/apprise/plugins/NotifyDBus.py b/libs/apprise/plugins/NotifyDBus.py
index 62a1093c8..7d357aa75 100644
--- a/libs/apprise/plugins/NotifyDBus.py
+++ b/libs/apprise/plugins/NotifyDBus.py
@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
-# BSD 3-Clause License
+# BSD 2-Clause License
#
# Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <[email protected]>
@@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
-# 3. Neither the name of the copyright holder nor the names of its
-# contributors may be used to endorse or promote products derived from
-# this software without specific prior written permission.
-#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
diff --git a/libs/apprise/plugins/NotifyDapnet.py b/libs/apprise/plugins/NotifyDapnet.py
index 1b718286a..5848b6886 100644
--- a/libs/apprise/plugins/NotifyDapnet.py
+++ b/libs/apprise/plugins/NotifyDapnet.py
@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
-# BSD 3-Clause License
+# BSD 2-Clause License
#
# Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <[email protected]>
@@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
-# 3. Neither the name of the copyright holder nor the names of its
-# contributors may be used to endorse or promote products derived from
-# this software without specific prior written permission.
-#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
diff --git a/libs/apprise/plugins/NotifyDingTalk.py b/libs/apprise/plugins/NotifyDingTalk.py
index ae2a9b499..91bfcd6fb 100644
--- a/libs/apprise/plugins/NotifyDingTalk.py
+++ b/libs/apprise/plugins/NotifyDingTalk.py
@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
-# BSD 3-Clause License
+# BSD 2-Clause License
#
# Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <[email protected]>
@@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
-# 3. Neither the name of the copyright holder nor the names of its
-# contributors may be used to endorse or promote products derived from
-# this software without specific prior written permission.
-#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "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',
},
})
diff --git a/libs/apprise/plugins/NotifyDiscord.py b/libs/apprise/plugins/NotifyDiscord.py
index fff76eef2..f87b66944 100644
--- a/libs/apprise/plugins/NotifyDiscord.py
+++ b/libs/apprise/plugins/NotifyDiscord.py
@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
-# BSD 3-Clause License
+# BSD 2-Clause License
#
# Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <[email protected]>
@@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
-# 3. Neither the name of the copyright holder nor the names of its
-# contributors may be used to endorse or promote products derived from
-# this software without specific prior written permission.
-#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "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
diff --git a/libs/apprise/plugins/NotifyEmail.py b/libs/apprise/plugins/NotifyEmail.py
index e55de7314..db70c8ef6 100644
--- a/libs/apprise/plugins/NotifyEmail.py
+++ b/libs/apprise/plugins/NotifyEmail.py
@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
-# BSD 3-Clause License
+# BSD 2-Clause License
#
# Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <[email protected]>
@@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
-# 3. Neither the name of the copyright holder nor the names of its
-# contributors may be used to endorse or promote products derived from
-# this software without specific prior written permission.
-#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "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
diff --git a/libs/apprise/plugins/NotifyEmby.py b/libs/apprise/plugins/NotifyEmby.py
index 23d4c6114..99f3a9ab1 100644
--- a/libs/apprise/plugins/NotifyEmby.py
+++ b/libs/apprise/plugins/NotifyEmby.py
@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
-# BSD 3-Clause License
+# BSD 2-Clause License
#
# Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <[email protected]>
@@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
-# 3. Neither the name of the copyright holder nor the names of its
-# contributors may be used to endorse or promote products derived from
-# this software without specific prior written permission.
-#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
diff --git a/libs/apprise/plugins/NotifyEnigma2.py b/libs/apprise/plugins/NotifyEnigma2.py
index 10d581792..054726469 100644
--- a/libs/apprise/plugins/NotifyEnigma2.py
+++ b/libs/apprise/plugins/NotifyEnigma2.py
@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
-# BSD 3-Clause License
+# BSD 2-Clause License
#
# Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <[email protected]>
@@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
-# 3. Neither the name of the copyright holder nor the names of its
-# contributors may be used to endorse or promote products derived from
-# this software without specific prior written permission.
-#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
diff --git a/libs/apprise/plugins/NotifyFCM/__init__.py b/libs/apprise/plugins/NotifyFCM/__init__.py
index d8857d340..57b03499b 100644
--- a/libs/apprise/plugins/NotifyFCM/__init__.py
+++ b/libs/apprise/plugins/NotifyFCM/__init__.py
@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
-# BSD 3-Clause License
+# BSD 2-Clause License
#
# Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <[email protected]>
@@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
-# 3. Neither the name of the copyright holder nor the names of its
-# contributors may be used to endorse or promote products derived from
-# this software without specific prior written permission.
-#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "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,
},
})
diff --git a/libs/apprise/plugins/NotifyFCM/color.py b/libs/apprise/plugins/NotifyFCM/color.py
index 46d0f2a71..69474a30c 100644
--- a/libs/apprise/plugins/NotifyFCM/color.py
+++ b/libs/apprise/plugins/NotifyFCM/color.py
@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
-# BSD 3-Clause License
+# BSD 2-Clause License
#
# Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <[email protected]>
@@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
-# 3. Neither the name of the copyright holder nor the names of its
-# contributors may be used to endorse or promote products derived from
-# this software without specific prior written permission.
-#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
diff --git a/libs/apprise/plugins/NotifyFCM/common.py b/libs/apprise/plugins/NotifyFCM/common.py
index 0ec10eec6..af71f8817 100644
--- a/libs/apprise/plugins/NotifyFCM/common.py
+++ b/libs/apprise/plugins/NotifyFCM/common.py
@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
-# BSD 3-Clause License
+# BSD 2-Clause License
#
# Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <[email protected]>
@@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
-# 3. Neither the name of the copyright holder nor the names of its
-# contributors may be used to endorse or promote products derived from
-# this software without specific prior written permission.
-#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
diff --git a/libs/apprise/plugins/NotifyFCM/oauth.py b/libs/apprise/plugins/NotifyFCM/oauth.py
index a76bc6987..f0961039d 100644
--- a/libs/apprise/plugins/NotifyFCM/oauth.py
+++ b/libs/apprise/plugins/NotifyFCM/oauth.py
@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
-# BSD 3-Clause License
+# BSD 2-Clause License
#
# Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <[email protected]>
@@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
-# 3. Neither the name of the copyright holder nor the names of its
-# contributors may be used to endorse or promote products derived from
-# this software without specific prior written permission.
-#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "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
diff --git a/libs/apprise/plugins/NotifyFCM/priority.py b/libs/apprise/plugins/NotifyFCM/priority.py
index 81976cb63..966a0e149 100644
--- a/libs/apprise/plugins/NotifyFCM/priority.py
+++ b/libs/apprise/plugins/NotifyFCM/priority.py
@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
-# BSD 3-Clause License
+# BSD 2-Clause License
#
# Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <[email protected]>
@@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
-# 3. Neither the name of the copyright holder nor the names of its
-# contributors may be used to endorse or promote products derived from
-# this software without specific prior written permission.
-#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
diff --git a/libs/apprise/plugins/NotifyFaast.py b/libs/apprise/plugins/NotifyFaast.py
index 3e55e1200..be3eff28d 100644
--- a/libs/apprise/plugins/NotifyFaast.py
+++ b/libs/apprise/plugins/NotifyFaast.py
@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
-# BSD 3-Clause License
+# BSD 2-Clause License
#
# Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <[email protected]>
@@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
-# 3. Neither the name of the copyright holder nor the names of its
-# contributors may be used to endorse or promote products derived from
-# this software without specific prior written permission.
-#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
diff --git a/libs/apprise/plugins/NotifyFlock.py b/libs/apprise/plugins/NotifyFlock.py
index 60b337e82..71a15da53 100644
--- a/libs/apprise/plugins/NotifyFlock.py
+++ b/libs/apprise/plugins/NotifyFlock.py
@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
-# BSD 3-Clause License
+# BSD 2-Clause License
#
# Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <[email protected]>
@@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
-# 3. Neither the name of the copyright holder nor the names of its
-# contributors may be used to endorse or promote products derived from
-# this software without specific prior written permission.
-#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "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'),
diff --git a/libs/apprise/plugins/NotifyForm.py b/libs/apprise/plugins/NotifyForm.py
index 3ef8d21b4..066f299b2 100644
--- a/libs/apprise/plugins/NotifyForm.py
+++ b/libs/apprise/plugins/NotifyForm.py
@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
-# BSD 3-Clause License
+# BSD 2-Clause License
#
# Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <[email protected]>
@@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
-# 3. Neither the name of the copyright holder nor the names of its
-# contributors may be used to endorse or promote products derived from
-# this software without specific prior written permission.
-#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "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:
diff --git a/libs/apprise/plugins/NotifyGitter.py b/libs/apprise/plugins/NotifyGitter.py
deleted file mode 100644
index 805d69c8c..000000000
--- a/libs/apprise/plugins/NotifyGitter.py
+++ /dev/null
@@ -1,425 +0,0 @@
-# -*- coding: utf-8 -*-
-# BSD 3-Clause License
-#
-# Apprise - Push Notification Library.
-# Copyright (c) 2023, Chris Caron <[email protected]>
-#
-# 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
diff --git a/libs/apprise/plugins/NotifyGnome.py b/libs/apprise/plugins/NotifyGnome.py
index 9476c78a3..f27c286cb 100644
--- a/libs/apprise/plugins/NotifyGnome.py
+++ b/libs/apprise/plugins/NotifyGnome.py
@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
-# BSD 3-Clause License
+# BSD 2-Clause License
#
# Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <[email protected]>
@@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
-# 3. Neither the name of the copyright holder nor the names of its
-# contributors may be used to endorse or promote products derived from
-# this software without specific prior written permission.
-#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
diff --git a/libs/apprise/plugins/NotifyGoogleChat.py b/libs/apprise/plugins/NotifyGoogleChat.py
index f65b6541e..7119e7429 100644
--- a/libs/apprise/plugins/NotifyGoogleChat.py
+++ b/libs/apprise/plugins/NotifyGoogleChat.py
@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
-# BSD 3-Clause License
+# BSD 2-Clause License
#
# Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <[email protected]>
@@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
-# 3. Neither the name of the copyright holder nor the names of its
-# contributors may be used to endorse or promote products derived from
-# this software without specific prior written permission.
-#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
diff --git a/libs/apprise/plugins/NotifyGotify.py b/libs/apprise/plugins/NotifyGotify.py
index 379225681..e20aa03da 100644
--- a/libs/apprise/plugins/NotifyGotify.py
+++ b/libs/apprise/plugins/NotifyGotify.py
@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
-# BSD 3-Clause License
+# BSD 2-Clause License
#
# Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <[email protected]>
@@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
-# 3. Neither the name of the copyright holder nor the names of its
-# contributors may be used to endorse or promote products derived from
-# this software without specific prior written permission.
-#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "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'),
diff --git a/libs/apprise/plugins/NotifyGrowl.py b/libs/apprise/plugins/NotifyGrowl.py
index 9240d62c5..790945f00 100644
--- a/libs/apprise/plugins/NotifyGrowl.py
+++ b/libs/apprise/plugins/NotifyGrowl.py
@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
-# BSD 3-Clause License
+# BSD 2-Clause License
#
# Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <[email protected]>
@@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
-# 3. Neither the name of the copyright holder nor the names of its
-# contributors may be used to endorse or promote products derived from
-# this software without specific prior written permission.
-#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
diff --git a/libs/apprise/plugins/NotifyGuilded.py b/libs/apprise/plugins/NotifyGuilded.py
index 8bb9aeeaa..066cddee8 100644
--- a/libs/apprise/plugins/NotifyGuilded.py
+++ b/libs/apprise/plugins/NotifyGuilded.py
@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
-# BSD 3-Clause License
+# BSD 2-Clause License
#
# Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <[email protected]>
@@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
-# 3. Neither the name of the copyright holder nor the names of its
-# contributors may be used to endorse or promote products derived from
-# this software without specific prior written permission.
-#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
diff --git a/libs/apprise/plugins/NotifyHomeAssistant.py b/libs/apprise/plugins/NotifyHomeAssistant.py
index a403356ab..25d8f5fb4 100644
--- a/libs/apprise/plugins/NotifyHomeAssistant.py
+++ b/libs/apprise/plugins/NotifyHomeAssistant.py
@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
-# BSD 3-Clause License
+# BSD 2-Clause License
#
# Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <[email protected]>
@@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
-# 3. Neither the name of the copyright holder nor the names of its
-# contributors may be used to endorse or promote products derived from
-# this software without specific prior written permission.
-#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
diff --git a/libs/apprise/plugins/NotifyIFTTT.py b/libs/apprise/plugins/NotifyIFTTT.py
index 04c6911ef..2c386c6b6 100644
--- a/libs/apprise/plugins/NotifyIFTTT.py
+++ b/libs/apprise/plugins/NotifyIFTTT.py
@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
-# BSD 3-Clause License
+# BSD 2-Clause License
#
# Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <[email protected]>
@@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
-# 3. Neither the name of the copyright holder nor the names of its
-# contributors may be used to endorse or promote products derived from
-# this software without specific prior written permission.
-#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "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
diff --git a/libs/apprise/plugins/NotifyJSON.py b/libs/apprise/plugins/NotifyJSON.py
index f1a9cc04e..a8ab7adc3 100644
--- a/libs/apprise/plugins/NotifyJSON.py
+++ b/libs/apprise/plugins/NotifyJSON.py
@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
-# BSD 3-Clause License
+# BSD 2-Clause License
#
# Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <[email protected]>
@@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
-# 3. Neither the name of the copyright holder nor the names of its
-# contributors may be used to endorse or promote products derived from
-# this software without specific prior written permission.
-#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "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:
diff --git a/libs/apprise/plugins/NotifyJoin.py b/libs/apprise/plugins/NotifyJoin.py
index e6210a5f3..92af6c3f1 100644
--- a/libs/apprise/plugins/NotifyJoin.py
+++ b/libs/apprise/plugins/NotifyJoin.py
@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
-# BSD 3-Clause License
+# BSD 2-Clause License
#
# Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <[email protected]>
@@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
-# 3. Neither the name of the copyright holder nor the names of its
-# contributors may be used to endorse or promote products derived from
-# this software without specific prior written permission.
-#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "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,
},
})
diff --git a/libs/apprise/plugins/NotifyKavenegar.py b/libs/apprise/plugins/NotifyKavenegar.py
index 8905e2431..d1df47c9e 100644
--- a/libs/apprise/plugins/NotifyKavenegar.py
+++ b/libs/apprise/plugins/NotifyKavenegar.py
@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
-# BSD 3-Clause License
+# BSD 2-Clause License
#
# Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <[email protected]>
@@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
-# 3. Neither the name of the copyright holder nor the names of its
-# contributors may be used to endorse or promote products derived from
-# this software without specific prior written permission.
-#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
diff --git a/libs/apprise/plugins/NotifyKumulos.py b/libs/apprise/plugins/NotifyKumulos.py
index 27e0995c9..6072340f8 100644
--- a/libs/apprise/plugins/NotifyKumulos.py
+++ b/libs/apprise/plugins/NotifyKumulos.py
@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
-# BSD 3-Clause License
+# BSD 2-Clause License
#
# Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <[email protected]>
@@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
-# 3. Neither the name of the copyright holder nor the names of its
-# contributors may be used to endorse or promote products derived from
-# this software without specific prior written permission.
-#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
diff --git a/libs/apprise/plugins/NotifyLametric.py b/libs/apprise/plugins/NotifyLametric.py
index 1b98b6946..516ec27ca 100644
--- a/libs/apprise/plugins/NotifyLametric.py
+++ b/libs/apprise/plugins/NotifyLametric.py
@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
-# BSD 3-Clause License
+# BSD 2-Clause License
#
# Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <[email protected]>
@@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
-# 3. Neither the name of the copyright holder nor the names of its
-# contributors may be used to endorse or promote products derived from
-# this software without specific prior written permission.
-#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "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'),
diff --git a/libs/apprise/plugins/NotifyLine.py b/libs/apprise/plugins/NotifyLine.py
index 817a998c8..09d72fed8 100644
--- a/libs/apprise/plugins/NotifyLine.py
+++ b/libs/apprise/plugins/NotifyLine.py
@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
-# BSD 3-Clause License
+# BSD 2-Clause License
#
# Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <[email protected]>
@@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
-# 3. Neither the name of the copyright holder nor the names of its
-# contributors may be used to endorse or promote products derived from
-# this software without specific prior written permission.
-#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "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
},
})
diff --git a/libs/apprise/plugins/NotifyMQTT.py b/libs/apprise/plugins/NotifyMQTT.py
index c8ee7cbce..2372c8b45 100644
--- a/libs/apprise/plugins/NotifyMQTT.py
+++ b/libs/apprise/plugins/NotifyMQTT.py
@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
-# BSD 3-Clause License
+# BSD 2-Clause License
#
# Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <[email protected]>
@@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
-# 3. Neither the name of the copyright holder nor the names of its
-# contributors may be used to endorse or promote products derived from
-# this software without specific prior written permission.
-#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "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):
diff --git a/libs/apprise/plugins/NotifyMSG91.py b/libs/apprise/plugins/NotifyMSG91.py
index 75834c399..225a2d3d9 100644
--- a/libs/apprise/plugins/NotifyMSG91.py
+++ b/libs/apprise/plugins/NotifyMSG91.py
@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
-# BSD 3-Clause License
+# BSD 2-Clause License
#
# Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <[email protected]>
@@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
-# 3. Neither the name of the copyright holder nor the names of its
-# contributors may be used to endorse or promote products derived from
-# this software without specific prior written permission.
-#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "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
diff --git a/libs/apprise/plugins/NotifyMSTeams.py b/libs/apprise/plugins/NotifyMSTeams.py
index 19f9fe34f..e82fdb8ca 100644
--- a/libs/apprise/plugins/NotifyMSTeams.py
+++ b/libs/apprise/plugins/NotifyMSTeams.py
@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
-# BSD 3-Clause License
+# BSD 2-Clause License
#
# Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <[email protected]>
@@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
-# 3. Neither the name of the copyright holder nor the names of its
-# contributors may be used to endorse or promote products derived from
-# this software without specific prior written permission.
-#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
diff --git a/libs/apprise/plugins/NotifyMacOSX.py b/libs/apprise/plugins/NotifyMacOSX.py
index 59c0620a3..ae08da112 100644
--- a/libs/apprise/plugins/NotifyMacOSX.py
+++ b/libs/apprise/plugins/NotifyMacOSX.py
@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
-# BSD 3-Clause License
+# BSD 2-Clause License
#
# Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <[email protected]>
@@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
-# 3. Neither the name of the copyright holder nor the names of its
-# contributors may be used to endorse or promote products derived from
-# this software without specific prior written permission.
-#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "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()
diff --git a/libs/apprise/plugins/NotifyMailgun.py b/libs/apprise/plugins/NotifyMailgun.py
index 3139e3416..5afebc52b 100644
--- a/libs/apprise/plugins/NotifyMailgun.py
+++ b/libs/apprise/plugins/NotifyMailgun.py
@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
-# BSD 3-Clause License
+# BSD 2-Clause License
#
# Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <[email protected]>
@@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
-# 3. Neither the name of the copyright holder nor the names of its
-# contributors may be used to endorse or promote products derived from
-# this software without specific prior written permission.
-#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "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:
diff --git a/libs/apprise/plugins/NotifyMastodon.py b/libs/apprise/plugins/NotifyMastodon.py
index 74d13952a..90c39e14b 100644
--- a/libs/apprise/plugins/NotifyMastodon.py
+++ b/libs/apprise/plugins/NotifyMastodon.py
@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
-# BSD 3-Clause License
+# BSD 2-Clause License
#
# Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <[email protected]>
@@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
-# 3. Neither the name of the copyright holder nor the names of its
-# contributors may be used to endorse or promote products derived from
-# this software without specific prior written permission.
-#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "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
diff --git a/libs/apprise/plugins/NotifyMatrix.py b/libs/apprise/plugins/NotifyMatrix.py
index c0b524a0d..8f3e77ff9 100644
--- a/libs/apprise/plugins/NotifyMatrix.py
+++ b/libs/apprise/plugins/NotifyMatrix.py
@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
-# BSD 3-Clause License
+# BSD 2-Clause License
#
# Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <[email protected]>
@@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
-# 3. Neither the name of the copyright holder nor the names of its
-# contributors may be used to endorse or promote products derived from
-# this software without specific prior written permission.
-#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "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)
diff --git a/libs/apprise/plugins/NotifyMatterMost.py b/libs/apprise/plugins/NotifyMatterMost.py
index e62f653c4..859fed311 100644
--- a/libs/apprise/plugins/NotifyMatterMost.py
+++ b/libs/apprise/plugins/NotifyMatterMost.py
@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
-# BSD 3-Clause License
+# BSD 2-Clause License
#
# Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <[email protected]>
@@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
-# 3. Neither the name of the copyright holder nor the names of its
-# contributors may be used to endorse or promote products derived from
-# this software without specific prior written permission.
-#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "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}',
)
diff --git a/libs/apprise/plugins/NotifyMattermost.py b/libs/apprise/plugins/NotifyMattermost.py
new file mode 100644
index 000000000..859fed311
--- /dev/null
+++ b/libs/apprise/plugins/NotifyMattermost.py
@@ -0,0 +1,372 @@
+# -*- coding: utf-8 -*-
+# BSD 2-Clause License
+#
+# Apprise - Push Notification Library.
+# Copyright (c) 2023, Chris Caron <[email protected]>
+#
+# 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
diff --git a/libs/apprise/plugins/NotifyMessageBird.py b/libs/apprise/plugins/NotifyMessageBird.py
index f477df489..4cb9d7b56 100644
--- a/libs/apprise/plugins/NotifyMessageBird.py
+++ b/libs/apprise/plugins/NotifyMessageBird.py
@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
-# BSD 3-Clause License
+# BSD 2-Clause License
#
# Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <[email protected]>
@@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
-# 3. Neither the name of the copyright holder nor the names of its
-# contributors may be used to endorse or promote products derived from
-# this software without specific prior written permission.
-#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
diff --git a/libs/apprise/plugins/NotifyMisskey.py b/libs/apprise/plugins/NotifyMisskey.py
index 54c4e628a..57633a515 100644
--- a/libs/apprise/plugins/NotifyMisskey.py
+++ b/libs/apprise/plugins/NotifyMisskey.py
@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
-# BSD 3-Clause License
+# BSD 2-Clause License
#
# Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <[email protected]>
@@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
-# 3. Neither the name of the copyright holder nor the names of its
-# contributors may be used to endorse or promote products derived from
-# this software without specific prior written permission.
-#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "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
diff --git a/libs/apprise/plugins/NotifyNextcloud.py b/libs/apprise/plugins/NotifyNextcloud.py
index 6bb79a7ef..b1d623d0b 100644
--- a/libs/apprise/plugins/NotifyNextcloud.py
+++ b/libs/apprise/plugins/NotifyNextcloud.py
@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
-# BSD 3-Clause License
+# BSD 2-Clause License
#
# Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <[email protected]>
@@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
-# 3. Neither the name of the copyright holder nor the names of its
-# contributors may be used to endorse or promote products derived from
-# this software without specific prior written permission.
-#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "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'] = {
diff --git a/libs/apprise/plugins/NotifyNextcloudTalk.py b/libs/apprise/plugins/NotifyNextcloudTalk.py
index 8a1dc4294..4f6dc0541 100644
--- a/libs/apprise/plugins/NotifyNextcloudTalk.py
+++ b/libs/apprise/plugins/NotifyNextcloudTalk.py
@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
-# BSD 3-Clause License
+# BSD 2-Clause License
#
# Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <[email protected]>
@@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
-# 3. Neither the name of the copyright holder nor the names of its
-# contributors may be used to endorse or promote products derived from
-# this software without specific prior written permission.
-#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "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'] = {
diff --git a/libs/apprise/plugins/NotifyNotica.py b/libs/apprise/plugins/NotifyNotica.py
index 90bf7ef1c..f95baba3f 100644
--- a/libs/apprise/plugins/NotifyNotica.py
+++ b/libs/apprise/plugins/NotifyNotica.py
@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
-# BSD 3-Clause License
+# BSD 2-Clause License
#
# Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <[email protected]>
@@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
-# 3. Neither the name of the copyright holder nor the names of its
-# contributors may be used to endorse or promote products derived from
-# this software without specific prior written permission.
-#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "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
diff --git a/libs/apprise/plugins/NotifyNotifiarr.py b/libs/apprise/plugins/NotifyNotifiarr.py
new file mode 100644
index 000000000..748e3b7aa
--- /dev/null
+++ b/libs/apprise/plugins/NotifyNotifiarr.py
@@ -0,0 +1,472 @@
+# -*- coding: utf-8 -*-
+# BSD 2-Clause License
+#
+# Apprise - Push Notification Library.
+# Copyright (c) 2023, Chris Caron <[email protected]>
+#
+# 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
diff --git a/libs/apprise/plugins/NotifyNotifico.py b/libs/apprise/plugins/NotifyNotifico.py
index 9b1661bf6..8636e2e00 100644
--- a/libs/apprise/plugins/NotifyNotifico.py
+++ b/libs/apprise/plugins/NotifyNotifico.py
@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
-# BSD 3-Clause License
+# BSD 2-Clause License
#
# Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <[email protected]>
@@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
-# 3. Neither the name of the copyright holder nor the names of its
-# contributors may be used to endorse or promote products derived from
-# this software without specific prior written permission.
-#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
diff --git a/libs/apprise/plugins/NotifyNtfy.py b/libs/apprise/plugins/NotifyNtfy.py
index 87587c041..ceab5a2a3 100644
--- a/libs/apprise/plugins/NotifyNtfy.py
+++ b/libs/apprise/plugins/NotifyNtfy.py
@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
-# BSD 3-Clause License
+# BSD 2-Clause License
#
# Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <[email protected]>
@@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
-# 3. Neither the name of the copyright holder nor the names of its
-# contributors may be used to endorse or promote products derived from
-# this software without specific prior written permission.
-#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "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):
"""
diff --git a/libs/apprise/plugins/NotifyOffice365.py b/libs/apprise/plugins/NotifyOffice365.py
index 658a21526..f445bc49d 100644
--- a/libs/apprise/plugins/NotifyOffice365.py
+++ b/libs/apprise/plugins/NotifyOffice365.py
@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
-# BSD 3-Clause License
+# BSD 2-Clause License
#
# Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <[email protected]>
@@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
-# 3. Neither the name of the copyright holder nor the names of its
-# contributors may be used to endorse or promote products derived from
-# this software without specific prior written permission.
-#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "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',
},
})
diff --git a/libs/apprise/plugins/NotifyOneSignal.py b/libs/apprise/plugins/NotifyOneSignal.py
index ce56bbdd9..39dd7f206 100644
--- a/libs/apprise/plugins/NotifyOneSignal.py
+++ b/libs/apprise/plugins/NotifyOneSignal.py
@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
-# BSD 3-Clause License
+# BSD 2-Clause License
#
# Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <[email protected]>
@@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
-# 3. Neither the name of the copyright holder nor the names of its
-# contributors may be used to endorse or promote products derived from
-# this software without specific prior written permission.
-#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "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,
},
})
diff --git a/libs/apprise/plugins/NotifyOpsgenie.py b/libs/apprise/plugins/NotifyOpsgenie.py
index 0639c1ed2..29cd0a202 100644
--- a/libs/apprise/plugins/NotifyOpsgenie.py
+++ b/libs/apprise/plugins/NotifyOpsgenie.py
@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
-# BSD 3-Clause License
+# BSD 2-Clause License
#
# Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <[email protected]>
@@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
-# 3. Neither the name of the copyright holder nor the names of its
-# contributors may be used to endorse or promote products derived from
-# this software without specific prior written permission.
-#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
diff --git a/libs/apprise/plugins/NotifyPagerDuty.py b/libs/apprise/plugins/NotifyPagerDuty.py
index a2417275b..1592f93c9 100644
--- a/libs/apprise/plugins/NotifyPagerDuty.py
+++ b/libs/apprise/plugins/NotifyPagerDuty.py
@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
-# BSD 3-Clause License
+# BSD 2-Clause License
#
# Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <[email protected]>
@@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
-# 3. Neither the name of the copyright holder nor the names of its
-# contributors may be used to endorse or promote products derived from
-# this software without specific prior written permission.
-#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "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
diff --git a/libs/apprise/plugins/NotifyPagerTree.py b/libs/apprise/plugins/NotifyPagerTree.py
index 65a19f613..a1579c30c 100644
--- a/libs/apprise/plugins/NotifyPagerTree.py
+++ b/libs/apprise/plugins/NotifyPagerTree.py
@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
-# BSD 3-Clause License
+# BSD 2-Clause License
#
# Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <[email protected]>
@@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
-# 3. Neither the name of the copyright holder nor the names of its
-# contributors may be used to endorse or promote products derived from
-# this software without specific prior written permission.
-#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
diff --git a/libs/apprise/plugins/NotifyParsePlatform.py b/libs/apprise/plugins/NotifyParsePlatform.py
index 69efb61c7..f3d7d635e 100644
--- a/libs/apprise/plugins/NotifyParsePlatform.py
+++ b/libs/apprise/plugins/NotifyParsePlatform.py
@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
-# BSD 3-Clause License
+# BSD 2-Clause License
#
# Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <[email protected]>
@@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
-# 3. Neither the name of the copyright holder nor the names of its
-# contributors may be used to endorse or promote products derived from
-# this software without specific prior written permission.
-#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "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
diff --git a/libs/apprise/plugins/NotifyPopcornNotify.py b/libs/apprise/plugins/NotifyPopcornNotify.py
index a36aed9f9..47a296147 100644
--- a/libs/apprise/plugins/NotifyPopcornNotify.py
+++ b/libs/apprise/plugins/NotifyPopcornNotify.py
@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
-# BSD 3-Clause License
+# BSD 2-Clause License
#
# Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <[email protected]>
@@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
-# 3. Neither the name of the copyright holder nor the names of its
-# contributors may be used to endorse or promote products derived from
-# this software without specific prior written permission.
-#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "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,
}
})
diff --git a/libs/apprise/plugins/NotifyProwl.py b/libs/apprise/plugins/NotifyProwl.py
index cebe07010..80f0aca3a 100644
--- a/libs/apprise/plugins/NotifyProwl.py
+++ b/libs/apprise/plugins/NotifyProwl.py
@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
-# BSD 3-Clause License
+# BSD 2-Clause License
#
# Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <[email protected]>
@@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
-# 3. Neither the name of the copyright holder nor the names of its
-# contributors may be used to endorse or promote products derived from
-# this software without specific prior written permission.
-#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
diff --git a/libs/apprise/plugins/NotifyPushBullet.py b/libs/apprise/plugins/NotifyPushBullet.py
index 07b2a43a0..61e8db2d5 100644
--- a/libs/apprise/plugins/NotifyPushBullet.py
+++ b/libs/apprise/plugins/NotifyPushBullet.py
@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
-# BSD 3-Clause License
+# BSD 2-Clause License
#
# Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <[email protected]>
@@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
-# 3. Neither the name of the copyright holder nor the names of its
-# contributors may be used to endorse or promote products derived from
-# this software without specific prior written permission.
-#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "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
diff --git a/libs/apprise/plugins/NotifyPushDeer.py b/libs/apprise/plugins/NotifyPushDeer.py
new file mode 100644
index 000000000..76805c34b
--- /dev/null
+++ b/libs/apprise/plugins/NotifyPushDeer.py
@@ -0,0 +1,218 @@
+# -*- coding: utf-8 -*-
+# BSD 2-Clause License
+#
+# Apprise - Push Notification Library.
+# Copyright (c) 2023, Chris Caron <[email protected]>
+#
+# 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
diff --git a/libs/apprise/plugins/NotifyPushMe.py b/libs/apprise/plugins/NotifyPushMe.py
new file mode 100644
index 000000000..8ef3c79c5
--- /dev/null
+++ b/libs/apprise/plugins/NotifyPushMe.py
@@ -0,0 +1,221 @@
+# -*- coding: utf-8 -*-
+# BSD 2-Clause License
+#
+# Apprise - Push Notification Library.
+# Copyright (c) 2023, Chris Caron <[email protected]>
+#
+# 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
diff --git a/libs/apprise/plugins/NotifyPushSafer.py b/libs/apprise/plugins/NotifyPushSafer.py
index 19bff2bd0..9873bd8e1 100644
--- a/libs/apprise/plugins/NotifyPushSafer.py
+++ b/libs/apprise/plugins/NotifyPushSafer.py
@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
-# BSD 3-Clause License
+# BSD 2-Clause License
#
# Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <[email protected]>
@@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
-# 3. Neither the name of the copyright holder nor the names of its
-# contributors may be used to endorse or promote products derived from
-# this software without specific prior written permission.
-#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "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:
diff --git a/libs/apprise/plugins/NotifyPushed.py b/libs/apprise/plugins/NotifyPushed.py
index b5ec3f6de..96e2e89d4 100644
--- a/libs/apprise/plugins/NotifyPushed.py
+++ b/libs/apprise/plugins/NotifyPushed.py
@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
-# BSD 3-Clause License
+# BSD 2-Clause License
#
# Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <[email protected]>
@@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
-# 3. Neither the name of the copyright holder nor the names of its
-# contributors may be used to endorse or promote products derived from
-# this software without specific prior written permission.
-#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
diff --git a/libs/apprise/plugins/NotifyPushjet.py b/libs/apprise/plugins/NotifyPushjet.py
index c6e36a393..50ee16e41 100644
--- a/libs/apprise/plugins/NotifyPushjet.py
+++ b/libs/apprise/plugins/NotifyPushjet.py
@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
-# BSD 3-Clause License
+# BSD 2-Clause License
#
# Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <[email protected]>
@@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
-# 3. Neither the name of the copyright holder nor the names of its
-# contributors may be used to endorse or promote products derived from
-# this software without specific prior written permission.
-#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
diff --git a/libs/apprise/plugins/NotifyPushover.py b/libs/apprise/plugins/NotifyPushover.py
index 64b94774c..4a76e7d54 100644
--- a/libs/apprise/plugins/NotifyPushover.py
+++ b/libs/apprise/plugins/NotifyPushover.py
@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
-# BSD 3-Clause License
+# BSD 2-Clause License
#
# Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <[email protected]>
@@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
-# 3. Neither the name of the copyright holder nor the names of its
-# contributors may be used to endorse or promote products derived from
-# this software without specific prior written permission.
-#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "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):
"""
diff --git a/libs/apprise/plugins/NotifyPushy.py b/libs/apprise/plugins/NotifyPushy.py
new file mode 100644
index 000000000..2a8a456b3
--- /dev/null
+++ b/libs/apprise/plugins/NotifyPushy.py
@@ -0,0 +1,384 @@
+# -*- coding: utf-8 -*-
+# BSD 2-Clause License
+#
+# Apprise - Push Notification Library.
+# Copyright (c) 2023, Chris Caron <[email protected]>
+#
+# 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
diff --git a/libs/apprise/plugins/NotifyRSyslog.py b/libs/apprise/plugins/NotifyRSyslog.py
new file mode 100644
index 000000000..473e4c5cb
--- /dev/null
+++ b/libs/apprise/plugins/NotifyRSyslog.py
@@ -0,0 +1,376 @@
+# -*- coding: utf-8 -*-
+# BSD 2-Clause License
+#
+# Apprise - Push Notification Library.
+# Copyright (c) 2023, Chris Caron <[email protected]>
+#
+# 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
diff --git a/libs/apprise/plugins/NotifyReddit.py b/libs/apprise/plugins/NotifyReddit.py
index 5cb22a726..b25e76d0b 100644
--- a/libs/apprise/plugins/NotifyReddit.py
+++ b/libs/apprise/plugins/NotifyReddit.py
@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
-# BSD 3-Clause License
+# BSD 2-Clause License
#
# Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <[email protected]>
@@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
-# 3. Neither the name of the copyright holder nor the names of its
-# contributors may be used to endorse or promote products derived from
-# this software without specific prior written permission.
-#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "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
diff --git a/libs/apprise/plugins/NotifyRocketChat.py b/libs/apprise/plugins/NotifyRocketChat.py
index ca6b5cd83..6384386e7 100644
--- a/libs/apprise/plugins/NotifyRocketChat.py
+++ b/libs/apprise/plugins/NotifyRocketChat.py
@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
-# BSD 3-Clause License
+# BSD 2-Clause License
#
# Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <[email protected]>
@@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
-# 3. Neither the name of the copyright holder nor the names of its
-# contributors may be used to endorse or promote products derived from
-# this software without specific prior written permission.
-#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
diff --git a/libs/apprise/plugins/NotifyRyver.py b/libs/apprise/plugins/NotifyRyver.py
index b8b34a3c4..70f2fa436 100644
--- a/libs/apprise/plugins/NotifyRyver.py
+++ b/libs/apprise/plugins/NotifyRyver.py
@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
-# BSD 3-Clause License
+# BSD 2-Clause License
#
# Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <[email protected]>
@@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
-# 3. Neither the name of the copyright holder nor the names of its
-# contributors may be used to endorse or promote products derived from
-# this software without specific prior written permission.
-#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "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',
},
})
diff --git a/libs/apprise/plugins/NotifySES.py b/libs/apprise/plugins/NotifySES.py
index fb0017036..37a0342ac 100644
--- a/libs/apprise/plugins/NotifySES.py
+++ b/libs/apprise/plugins/NotifySES.py
@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
-# BSD 3-Clause License
+# BSD 2-Clause License
#
# Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <[email protected]>
@@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
-# 3. Neither the name of the copyright holder nor the names of its
-# contributors may be used to endorse or promote products derived from
-# this software without specific prior written permission.
-#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "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))
diff --git a/libs/apprise/plugins/NotifySMSEagle.py b/libs/apprise/plugins/NotifySMSEagle.py
index 747831e10..3db131fbc 100644
--- a/libs/apprise/plugins/NotifySMSEagle.py
+++ b/libs/apprise/plugins/NotifySMSEagle.py
@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
-# BSD 3-Clause License
+# BSD 2-Clause License
#
# Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <[email protected]>
@@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
-# 3. Neither the name of the copyright holder nor the names of its
-# contributors may be used to endorse or promote products derived from
-# this software without specific prior written permission.
-#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "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,6 +108,9 @@ class NotifySMSEagle(NotifyBase):
# The path we send our notification to
notify_path = '/jsonrpc/sms'
+ # Support attachments
+ attachment_support = True
+
# The maxumum length of the text message
# The actual limit is 160 but SMSEagle looks after the handling
# of large messages in it's upstream service
@@ -145,6 +144,7 @@ class NotifySMSEagle(NotifyBase):
'token': {
'name': _('Access Token'),
'type': 'string',
+ 'required': True,
},
'target_phone': {
'name': _('Target Phone No'),
@@ -170,6 +170,7 @@ class NotifySMSEagle(NotifyBase):
'targets': {
'name': _('Targets'),
'type': 'list:string',
+ 'required': True,
}
})
@@ -338,7 +339,7 @@ class NotifySMSEagle(NotifyBase):
has_error = False
attachments = []
- if attach:
+ if attach and self.attachment_support:
for attachment in attach:
# Perform some simple error checking
if not attachment:
diff --git a/libs/apprise/plugins/NotifySMTP2Go.py b/libs/apprise/plugins/NotifySMTP2Go.py
index 3634ba6a8..45f6615cb 100644
--- a/libs/apprise/plugins/NotifySMTP2Go.py
+++ b/libs/apprise/plugins/NotifySMTP2Go.py
@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
-# BSD 3-Clause License
+# BSD 2-Clause License
#
# Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <[email protected]>
@@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
-# 3. Neither the name of the copyright holder nor the names of its
-# contributors may be used to endorse or promote products derived from
-# this software without specific prior written permission.
-#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "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,6 +87,9 @@ class NotifySMTP2Go(NotifyBase):
# Notify URL
notify_url = 'https://api.smtp2go.com/v3/email/send'
+ # Support attachments
+ attachment_support = True
+
# Default Notify Format
notify_format = NotifyFormat.HTML
@@ -294,8 +293,8 @@ class NotifySMTP2Go(NotifyBase):
# Track our potential attachments
attachments = []
- if attach:
- for idx, attachment in enumerate(attach):
+ if attach and self.attachment_support:
+ for attachment in attach:
# Perform some simple error checking
if not attachment:
# We could not access the attachment
diff --git a/libs/apprise/plugins/NotifySNS.py b/libs/apprise/plugins/NotifySNS.py
index c1d2ed932..5edac727c 100644
--- a/libs/apprise/plugins/NotifySNS.py
+++ b/libs/apprise/plugins/NotifySNS.py
@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
-# BSD 3-Clause License
+# BSD 2-Clause License
#
# Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <[email protected]>
@@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
-# 3. Neither the name of the copyright holder nor the names of its
-# contributors may be used to endorse or promote products derived from
-# this software without specific prior written permission.
-#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "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 hmac
import requests
from hashlib import sha256
from datetime import datetime
+from datetime import timezone
from collections import OrderedDict
from xml.etree import ElementTree
from itertools import chain
@@ -102,7 +99,7 @@ class NotifySNS(NotifyBase):
# Define object templates
templates = (
- '{schema}://{access_key_id}/{secret_access_key}{region}/{targets}',
+ '{schema}://{access_key_id}/{secret_access_key}/{region}/{targets}',
)
# Define our template tokens
@@ -124,6 +121,7 @@ class NotifySNS(NotifyBase):
'type': 'string',
'required': True,
'regex': (r'^[a-z]{2}-[a-z-]+?-[0-9]+$', 'i'),
+ 'required': True,
'map_to': 'region_name',
},
'target_phone_no': {
@@ -142,6 +140,7 @@ class NotifySNS(NotifyBase):
'targets': {
'name': _('Targets'),
'type': 'list:string',
+ 'required': True,
},
})
@@ -396,7 +395,7 @@ class NotifySNS(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))
diff --git a/libs/apprise/plugins/NotifySendGrid.py b/libs/apprise/plugins/NotifySendGrid.py
index d1ae8a4d4..b7f4a8a6c 100644
--- a/libs/apprise/plugins/NotifySendGrid.py
+++ b/libs/apprise/plugins/NotifySendGrid.py
@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
-# BSD 3-Clause License
+# BSD 2-Clause License
#
# Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <[email protected]>
@@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
-# 3. Neither the name of the copyright holder nor the names of its
-# contributors may be used to endorse or promote products derived from
-# this software without specific prior written permission.
-#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "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.
-#
# You will need an API Key for this plugin to work.
# From the Settings -> API Keys you can click "Create API Key" if you don't
# have one already. The key must have at least the "Mail Send" permission
diff --git a/libs/apprise/plugins/NotifyServerChan.py b/libs/apprise/plugins/NotifyServerChan.py
index 6fa8c5570..87a294a39 100644
--- a/libs/apprise/plugins/NotifyServerChan.py
+++ b/libs/apprise/plugins/NotifyServerChan.py
@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
-# BSD 3-Clause License
+# BSD 2-Clause License
#
# Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <[email protected]>
@@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
-# 3. Neither the name of the copyright holder nor the names of its
-# contributors may be used to endorse or promote products derived from
-# this software without specific prior written permission.
-#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "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,7 @@ class NotifyServerChan(NotifyBase):
# Define object templates
templates = (
- '{schema}://{token}/',
+ '{schema}://{token}',
)
# Define our template tokens
diff --git a/libs/apprise/plugins/NotifySignalAPI.py b/libs/apprise/plugins/NotifySignalAPI.py
index 589499f8d..a2a31de10 100644
--- a/libs/apprise/plugins/NotifySignalAPI.py
+++ b/libs/apprise/plugins/NotifySignalAPI.py
@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
-# BSD 3-Clause License
+# BSD 2-Clause License
#
# Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <[email protected]>
@@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
-# 3. Neither the name of the copyright holder nor the names of its
-# contributors may be used to endorse or promote products derived from
-# this software without specific prior written permission.
-#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "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,6 +64,9 @@ class NotifySignalAPI(NotifyBase):
# A URL that takes you to the setup/help of the specific protocol
setup_url = 'https://github.com/caronc/apprise/wiki/Notify_signal'
+ # Support attachments
+ attachment_support = True
+
# The maximum targets to include when doing batch transfers
default_batch_size = 10
@@ -234,7 +233,7 @@ class NotifySignalAPI(NotifyBase):
has_error = False
attachments = []
- if attach:
+ if attach and self.attachment_support:
for attachment in attach:
# Perform some simple error checking
if not attachment:
@@ -281,7 +280,7 @@ class NotifySignalAPI(NotifyBase):
payload = {
'message': "{}{}".format(
'' if not self.status else '{} '.format(
- self.asset.ascii(notify_type)), body),
+ self.asset.ascii(notify_type)), body).rstrip(),
"number": self.source,
"recipients": []
}
diff --git a/libs/apprise/plugins/NotifySimplePush.py b/libs/apprise/plugins/NotifySimplePush.py
index 25066067c..d6bd2ab6b 100644
--- a/libs/apprise/plugins/NotifySimplePush.py
+++ b/libs/apprise/plugins/NotifySimplePush.py
@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
-# BSD 3-Clause License
+# BSD 2-Clause License
#
# Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <[email protected]>
@@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
-# 3. Neither the name of the copyright holder nor the names of its
-# contributors may be used to endorse or promote products derived from
-# this software without specific prior written permission.
-#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
@@ -109,12 +105,12 @@ class NotifySimplePush(NotifyBase):
# Used for encrypted logins
'password': {
- 'name': _('Encrypted Password'),
+ 'name': _('Password'),
'type': 'string',
'private': True,
},
'salt': {
- 'name': _('Encrypted Salt'),
+ 'name': _('Salt'),
'type': 'string',
'private': True,
'map_to': 'user',
diff --git a/libs/apprise/plugins/NotifySinch.py b/libs/apprise/plugins/NotifySinch.py
index 0756f76b3..b2c5683fa 100644
--- a/libs/apprise/plugins/NotifySinch.py
+++ b/libs/apprise/plugins/NotifySinch.py
@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
-# BSD 3-Clause License
+# BSD 2-Clause License
#
# Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <[email protected]>
@@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
-# 3. Neither the name of the copyright holder nor the names of its
-# contributors may be used to endorse or promote products derived from
-# this software without specific prior written permission.
-#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
diff --git a/libs/apprise/plugins/NotifySlack.py b/libs/apprise/plugins/NotifySlack.py
index 0d85d25fe..bbd2bf242 100644
--- a/libs/apprise/plugins/NotifySlack.py
+++ b/libs/apprise/plugins/NotifySlack.py
@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
-# BSD 3-Clause License
+# BSD 2-Clause License
#
# Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <[email protected]>
@@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
-# 3. Neither the name of the copyright holder nor the names of its
-# contributors may be used to endorse or promote products derived from
-# this software without specific prior written permission.
-#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
@@ -143,6 +139,10 @@ class NotifySlack(NotifyBase):
# A URL that takes you to the setup/help of the specific protocol
setup_url = 'https://github.com/caronc/apprise/wiki/Notify_slack'
+ # Support attachments
+ attachment_support = True
+
+ # The maximum targets to include when doing batch transfers
# Slack Webhook URL
webhook_url = 'https://hooks.slack.com/services'
@@ -165,10 +165,10 @@ class NotifySlack(NotifyBase):
# Define object templates
templates = (
# Webhook
- '{schema}://{token_a}/{token_b}{token_c}',
+ '{schema}://{token_a}/{token_b}/{token_c}',
'{schema}://{botname}@{token_a}/{token_b}{token_c}',
- '{schema}://{token_a}/{token_b}{token_c}/{targets}',
- '{schema}://{botname}@{token_a}/{token_b}{token_c}/{targets}',
+ '{schema}://{token_a}/{token_b}/{token_c}/{targets}',
+ '{schema}://{botname}@{token_a}/{token_b}/{token_c}/{targets}',
# Bot
'{schema}://{access_token}/',
@@ -198,7 +198,6 @@ class NotifySlack(NotifyBase):
'name': _('Token A'),
'type': 'string',
'private': True,
- 'required': True,
'regex': (r'^[A-Z0-9]+$', 'i'),
},
# Token required as part of the Webhook request
@@ -207,7 +206,6 @@ class NotifySlack(NotifyBase):
'name': _('Token B'),
'type': 'string',
'private': True,
- 'required': True,
'regex': (r'^[A-Z0-9]+$', 'i'),
},
# Token required as part of the Webhook request
@@ -216,7 +214,6 @@ class NotifySlack(NotifyBase):
'name': _('Token C'),
'type': 'string',
'private': True,
- 'required': True,
'regex': (r'^[A-Za-z0-9]+$', 'i'),
},
'target_encoded_id': {
@@ -525,7 +522,8 @@ class NotifySlack(NotifyBase):
# Include the footer only if specified to do so
payload['attachments'][0]['footer'] = self.app_id
- if attach and self.mode is SlackMode.WEBHOOK:
+ if attach and self.attachment_support \
+ and self.mode is SlackMode.WEBHOOK:
# Be friendly; let the user know why they can't send their
# attachments if using the Webhook mode
self.logger.warning(
@@ -603,7 +601,8 @@ class NotifySlack(NotifyBase):
' to {}'.format(channel)
if channel is not None else ''))
- if attach and self.mode is SlackMode.BOT and attach_channel_list:
+ if attach and self.attachment_support and \
+ self.mode is SlackMode.BOT and attach_channel_list:
# Send our attachments (can only be done in bot mode)
for attachment in attach:
diff --git a/libs/apprise/plugins/NotifySparkPost.py b/libs/apprise/plugins/NotifySparkPost.py
index 25024bc5f..282f55093 100644
--- a/libs/apprise/plugins/NotifySparkPost.py
+++ b/libs/apprise/plugins/NotifySparkPost.py
@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
-# BSD 3-Clause License
+# BSD 2-Clause License
#
# Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <[email protected]>
@@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
-# 3. Neither the name of the copyright holder nor the names of its
-# contributors may be used to endorse or promote products derived from
-# this software without specific prior written permission.
-#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
@@ -118,6 +114,9 @@ class NotifySparkPost(NotifyBase):
# The services URL
service_url = 'https://sparkpost.com/'
+ # Support attachments
+ attachment_support = True
+
# All notification requests are secure
secure_protocol = 'sparkpost'
@@ -225,7 +224,7 @@ class NotifySparkPost(NotifyBase):
}
def __init__(self, apikey, targets, cc=None, bcc=None, from_name=None,
- region_name=None, headers=None, tokens=None, batch=False,
+ region_name=None, headers=None, tokens=None, batch=None,
**kwargs):
"""
Initialize SparkPost Object
@@ -296,7 +295,8 @@ class NotifySparkPost(NotifyBase):
self.tokens.update(tokens)
# Prepare Batch Mode Flag
- self.batch = batch
+ self.batch = self.template_args['batch']['default'] \
+ if batch is None else batch
if targets:
# Validate recipients (to:) and drop bad ones:
@@ -542,7 +542,7 @@ class NotifySparkPost(NotifyBase):
else:
payload['content']['text'] = body
- if attach:
+ if attach and self.attachment_support:
# Prepare ourselves an attachment object
payload['content']['attachments'] = []
diff --git a/libs/apprise/plugins/NotifySpontit.py b/libs/apprise/plugins/NotifySpontit.py
index 01d4e1980..4705fc058 100644
--- a/libs/apprise/plugins/NotifySpontit.py
+++ b/libs/apprise/plugins/NotifySpontit.py
@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
-# BSD 3-Clause License
+# BSD 2-Clause License
#
# Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <[email protected]>
@@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
-# 3. Neither the name of the copyright holder nor the names of its
-# contributors may be used to endorse or promote products derived from
-# this software without specific prior written permission.
-#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
@@ -135,7 +131,6 @@ class NotifySpontit(NotifyBase):
'targets': {
'name': _('Targets'),
'type': 'list:string',
- 'required': True,
},
})
diff --git a/libs/apprise/plugins/NotifyStreamlabs.py b/libs/apprise/plugins/NotifyStreamlabs.py
index 3489519a5..56b577e49 100644
--- a/libs/apprise/plugins/NotifyStreamlabs.py
+++ b/libs/apprise/plugins/NotifyStreamlabs.py
@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
-# BSD 3-Clause License
+# BSD 2-Clause License
#
# Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <[email protected]>
@@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
-# 3. Neither the name of the copyright holder nor the names of its
-# contributors may be used to endorse or promote products derived from
-# this software without specific prior written permission.
-#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
@@ -277,8 +273,7 @@ class NotifyStreamlabs(NotifyBase):
return
- def send(self, body, title='', notify_type=NotifyType.INFO, attach=None,
- **kwargs):
+ def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs):
"""
Perform Streamlabs notification call (either donation or alert)
"""
diff --git a/libs/apprise/plugins/NotifySyslog.py b/libs/apprise/plugins/NotifySyslog.py
index 433aab9c5..3ff1f2576 100644
--- a/libs/apprise/plugins/NotifySyslog.py
+++ b/libs/apprise/plugins/NotifySyslog.py
@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
-# BSD 3-Clause License
+# BSD 2-Clause License
#
# Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <[email protected]>
@@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
-# 3. Neither the name of the copyright holder nor the names of its
-# contributors may be used to endorse or promote products derived from
-# this software without specific prior written permission.
-#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "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,14 +26,11 @@
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
-import os
import syslog
-import socket
from .NotifyBase import NotifyBase
from ..common import NotifyType
from ..utils import parse_bool
-from ..utils import is_hostname
from ..AppriseLocale import gettext_lazy as _
@@ -107,20 +100,13 @@ SYSLOG_FACILITY_RMAP = {
syslog.LOG_LOCAL7: SyslogFacility.LOCAL7,
}
-
-class SyslogMode:
- # A local query
- LOCAL = "local"
-
- # A remote query
- REMOTE = "remote"
-
-
-# webhook modes are placed ito this list for validation purposes
-SYSLOG_MODES = (
- SyslogMode.LOCAL,
- SyslogMode.REMOTE,
-)
+# 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 NotifySyslog(NotifyBase):
@@ -134,8 +120,8 @@ class NotifySyslog(NotifyBase):
# The services URL
service_url = 'https://tools.ietf.org/html/rfc5424'
- # The default secure protocol
- secure_protocol = 'syslog'
+ # The default protocol
+ protocol = 'syslog'
# A URL that takes you to the setup/help of the specific protocol
setup_url = 'https://github.com/caronc/apprise/wiki/Notify_syslog'
@@ -148,10 +134,6 @@ class NotifySyslog(NotifyBase):
templates = (
'{schema}://',
'{schema}://{facility}',
- '{schema}://{host}',
- '{schema}://{host}:{port}',
- '{schema}://{host}/{facility}',
- '{schema}://{host}:{port}/{facility}',
)
# Define our template tokens
@@ -162,18 +144,6 @@ class NotifySyslog(NotifyBase):
'values': [k for k in SYSLOG_FACILITY_MAP.keys()],
'default': SyslogFacility.USER,
},
- 'host': {
- 'name': _('Hostname'),
- 'type': 'string',
- 'required': True,
- },
- 'port': {
- 'name': _('Port'),
- 'type': 'int',
- 'min': 1,
- 'max': 65535,
- 'default': 514,
- },
})
# Define our template arguments
@@ -182,12 +152,6 @@ class NotifySyslog(NotifyBase):
# We map back to the same element defined in template_tokens
'alias_of': 'facility',
},
- 'mode': {
- 'name': _('Syslog Mode'),
- 'type': 'choice:string',
- 'values': SYSLOG_MODES,
- 'default': SyslogMode.LOCAL,
- },
'logpid': {
'name': _('Log PID'),
'type': 'bool',
@@ -202,8 +166,8 @@ class NotifySyslog(NotifyBase):
},
})
- def __init__(self, facility=None, mode=None, log_pid=True,
- log_perror=False, **kwargs):
+ def __init__(self, facility=None, log_pid=True, log_perror=False,
+ **kwargs):
"""
Initialize Syslog Object
"""
@@ -223,14 +187,6 @@ class NotifySyslog(NotifyBase):
SYSLOG_FACILITY_MAP[
self.template_tokens['facility']['default']]
- self.mode = self.template_args['mode']['default'] \
- if not isinstance(mode, str) else mode.lower()
-
- if self.mode not in SYSLOG_MODES:
- msg = 'The mode specified ({}) is invalid.'.format(mode)
- self.logger.warning(msg)
- raise TypeError(msg)
-
# Logging Options
self.logoptions = 0
@@ -249,7 +205,7 @@ class NotifySyslog(NotifyBase):
if log_perror:
self.logoptions |= syslog.LOG_PERROR
- # Initialize our loggig
+ # Initialize our logging
syslog.openlog(
self.app_id, logoption=self.logoptions, facility=self.facility)
return
@@ -259,7 +215,7 @@ class NotifySyslog(NotifyBase):
Perform Syslog Notification
"""
- _pmap = {
+ SYSLOG_PUBLISH_MAP = {
NotifyType.INFO: syslog.LOG_INFO,
NotifyType.SUCCESS: syslog.LOG_NOTICE,
NotifyType.FAILURE: syslog.LOG_CRIT,
@@ -272,70 +228,17 @@ class NotifySyslog(NotifyBase):
# Always call throttle before any remote server i/o is made
self.throttle()
- if self.mode == SyslogMode.LOCAL:
- try:
- syslog.syslog(_pmap[notify_type], body)
+ try:
+ syslog.syslog(SYSLOG_PUBLISH_MAP[notify_type], body)
- except KeyError:
- # An invalid notification type was specified
- self.logger.warning(
- 'An invalid notification type '
- '({}) was specified.'.format(notify_type))
- return False
-
- else: # SyslogMode.REMOTE
-
- host = self.host
- port = self.port if self.port \
- else self.template_tokens['port']['default']
- if self.log_pid:
- payload = '<%d>- %d - %s' % (
- _pmap[notify_type] + self.facility * 8, os.getpid(), body)
-
- else:
- payload = '<%d>- %s' % (
- _pmap[notify_type] + self.facility * 8, body)
-
- # send UDP packet to upstream server
- self.logger.debug(
- 'Syslog Host: %s:%d/%s',
- host, port, SYSLOG_FACILITY_RMAP[self.facility])
- self.logger.debug('Syslog Payload: %s' % str(payload))
-
- # our sent bytes
- sent = 0
+ except KeyError:
+ # An invalid notification type was specified
+ self.logger.warning(
+ 'An invalid notification type '
+ '({}) was specified.'.format(notify_type))
+ return False
- 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 Syslog '
- '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 Syslog '
- '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(
- 'Syslog sent %d byte(s) but intended to send %d byte(s)',
- sent, len(payload))
- return False
-
- self.logger.info('Sent Syslog (%s) notification.', self.mode)
+ self.logger.info('Sent Syslog notification.')
return True
@@ -348,31 +251,16 @@ class NotifySyslog(NotifyBase):
params = {
'logperror': 'yes' if self.log_perror else 'no',
'logpid': 'yes' if self.log_pid else 'no',
- 'mode': self.mode,
}
# Extend our parameters
params.update(self.url_parameters(privacy=privacy, *args, **kwargs))
- if self.mode == SyslogMode.LOCAL:
- return '{schema}://{facility}/?{params}'.format(
- facility=self.template_tokens['facility']['default']
- if self.facility not in SYSLOG_FACILITY_RMAP
- else SYSLOG_FACILITY_RMAP[self.facility],
- schema=self.secure_protocol,
- params=NotifySyslog.urlencode(params),
- )
-
- # Remote mode:
- return '{schema}://{hostname}{port}/{facility}/?{params}'.format(
- schema=self.secure_protocol,
- hostname=NotifySyslog.quote(self.host, safe=''),
- port='' if self.port is None
- or self.port == self.template_tokens['port']['default']
- else ':{}'.format(self.port),
+ return '{schema}://{facility}/?{params}'.format(
facility=self.template_tokens['facility']['default']
if self.facility not in SYSLOG_FACILITY_RMAP
else SYSLOG_FACILITY_RMAP[self.facility],
+ schema=self.protocol,
params=NotifySyslog.urlencode(params),
)
@@ -395,21 +283,12 @@ class NotifySyslog(NotifyBase):
# Get our path values
tokens.extend(NotifySyslog.split_path(results['fullpath']))
+ # Initialization
facility = None
- if len(tokens) > 1 and is_hostname(tokens[0]):
- # syslog://hostname/facility
- results['mode'] = SyslogMode.REMOTE
-
- # Store our facility as the first path entry
- facility = tokens[-1]
- elif tokens:
- # This is a bit ambigious... it could be either:
- # syslog://facility -or- syslog://hostname
-
- # First lets test it as a facility; we'll correct this
- # later on if nessisary
- facility = tokens[-1]
+ 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
@@ -425,20 +304,6 @@ class NotifySyslog(NotifyBase):
facility = next((f for f in SYSLOG_FACILITY_MAP.keys()
if f.startswith(facility)), facility)
- # Attempt to solve our ambiguity
- if len(tokens) == 1 and is_hostname(tokens[0]) and (
- results['port'] or facility not in SYSLOG_FACILITY_MAP):
-
- # facility is likely hostname; update our guessed mode
- results['mode'] = SyslogMode.REMOTE
-
- # Reset our facility value
- facility = None
-
- # Set mode if not otherwise set
- if 'mode' in results['qsd'] and len(results['qsd']['mode']):
- results['mode'] = NotifySyslog.unquote(results['qsd']['mode'])
-
# Save facility if set
if facility:
results['facility'] = facility
diff --git a/libs/apprise/plugins/NotifyTechulusPush.py b/libs/apprise/plugins/NotifyTechulusPush.py
index 0f3e79e53..3e2085c53 100644
--- a/libs/apprise/plugins/NotifyTechulusPush.py
+++ b/libs/apprise/plugins/NotifyTechulusPush.py
@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
-# BSD 3-Clause License
+# BSD 2-Clause License
#
# Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <[email protected]>
@@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
-# 3. Neither the name of the copyright holder nor the names of its
-# contributors may be used to endorse or promote products derived from
-# this software without specific prior written permission.
-#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
diff --git a/libs/apprise/plugins/NotifyTelegram.py b/libs/apprise/plugins/NotifyTelegram.py
index d5a52be60..1727fe87d 100644
--- a/libs/apprise/plugins/NotifyTelegram.py
+++ b/libs/apprise/plugins/NotifyTelegram.py
@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
-# BSD 3-Clause License
+# BSD 2-Clause License
#
# Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <[email protected]>
@@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
-# 3. Neither the name of the copyright holder nor the names of its
-# contributors may be used to endorse or promote products derived from
-# this software without specific prior written permission.
-#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
@@ -84,6 +80,23 @@ IS_CHAT_ID_RE = re.compile(
)
+class TelegramContentPlacement:
+ """
+ The Telegram Content Placement
+ """
+ # Before Attachments
+ BEFORE = "before"
+ # After Attachments
+ AFTER = "after"
+
+
+# Identify Placement Categories
+TELEGRAM_CONTENT_PLACEMENT = (
+ TelegramContentPlacement.BEFORE,
+ TelegramContentPlacement.AFTER,
+)
+
+
class NotifyTelegram(NotifyBase):
"""
A wrapper for Telegram Notifications
@@ -106,6 +119,9 @@ class NotifyTelegram(NotifyBase):
# Telegram uses the http protocol with JSON requests
notify_url = 'https://api.telegram.org/bot'
+ # Support attachments
+ attachment_support = True
+
# Allows the user to specify the NotifyImageSize object
image_size = NotifyImageSize.XY_256
@@ -319,11 +335,17 @@ class NotifyTelegram(NotifyBase):
'to': {
'alias_of': 'targets',
},
+ 'content': {
+ 'name': _('Content Placement'),
+ 'type': 'choice:string',
+ 'values': TELEGRAM_CONTENT_PLACEMENT,
+ 'default': TelegramContentPlacement.BEFORE,
+ },
})
def __init__(self, bot_token, targets, detect_owner=True,
include_image=False, silent=None, preview=None, topic=None,
- **kwargs):
+ content=None, **kwargs):
"""
Initialize Telegram Object
"""
@@ -349,6 +371,15 @@ class NotifyTelegram(NotifyBase):
self.preview = self.template_args['preview']['default'] \
if preview is None else bool(preview)
+ # Setup our content placement
+ self.content = self.template_args['content']['default'] \
+ if not isinstance(content, str) else content.lower()
+ if self.content and self.content not in TELEGRAM_CONTENT_PLACEMENT:
+ msg = 'The content placement specified ({}) is invalid.'\
+ .format(content)
+ self.logger.warning(msg)
+ raise TypeError(msg)
+
if topic:
try:
self.topic = int(topic)
@@ -439,11 +470,14 @@ class NotifyTelegram(NotifyBase):
# content can arrive together.
self.throttle()
+ payload = {'chat_id': chat_id}
+ if self.topic:
+ payload['message_thread_id'] = self.topic
+
try:
with open(path, 'rb') as f:
# Configure file payload (for upload)
files = {key: (file_name, f)}
- payload = {'chat_id': chat_id}
self.logger.debug(
'Telegram attachment POST URL: %s (cert_verify=%r)' % (
@@ -680,6 +714,10 @@ class NotifyTelegram(NotifyBase):
# Prepare our payload based on HTML or TEXT
payload['text'] = body
+ # Handle payloads without a body specified (but an attachment present)
+ attach_content = \
+ TelegramContentPlacement.AFTER if not body else self.content
+
# Create a copy of the chat_ids list
targets = list(self.targets)
while len(targets):
@@ -713,6 +751,20 @@ class NotifyTelegram(NotifyBase):
'Failed to send Telegram type image to {}.',
payload['chat_id'])
+ if attach and self.attachment_support and \
+ attach_content == TelegramContentPlacement.AFTER:
+ # Send our attachments now (if specified and if it exists)
+ if not self._send_attachments(
+ chat_id=payload['chat_id'], notify_type=notify_type,
+ attach=attach):
+
+ has_error = True
+ continue
+
+ if not body:
+ # Nothing more to do; move along to the next attachment
+ continue
+
# Always call throttle before any remote server i/o is made;
# Telegram throttles to occur before sending the image so that
# content can arrive together.
@@ -775,19 +827,36 @@ class NotifyTelegram(NotifyBase):
self.logger.info('Sent Telegram notification.')
- if attach:
- # Send our attachments now (if specified and if it exists)
- for attachment in attach:
- if not self.send_media(
- payload['chat_id'], notify_type,
- attach=attachment):
+ if attach and self.attachment_support \
+ and attach_content == TelegramContentPlacement.BEFORE:
+ # Send our attachments now (if specified and if it exists) as
+ # it was identified to send the content before the attachments
+ # which is now done.
+ if not self._send_attachments(
+ chat_id=payload['chat_id'],
+ notify_type=notify_type,
+ attach=attach):
- # We failed; don't continue
- has_error = True
- break
+ has_error = True
+ continue
- self.logger.info(
- 'Sent Telegram attachment: {}.'.format(attachment))
+ return not has_error
+
+ def _send_attachments(self, chat_id, notify_type, attach):
+ """
+ Sends our attachments
+ """
+ has_error = False
+ # Send our attachments now (if specified and if it exists)
+ for attachment in attach:
+ if not self.send_media(chat_id, notify_type, attach=attachment):
+
+ # We failed; don't continue
+ has_error = True
+ break
+
+ self.logger.info(
+ 'Sent Telegram attachment: {}.'.format(attachment))
return not has_error
@@ -802,6 +871,7 @@ class NotifyTelegram(NotifyBase):
'detect': 'yes' if self.detect_owner else 'no',
'silent': 'yes' if self.silent else 'no',
'preview': 'yes' if self.preview else 'no',
+ 'content': self.content,
}
if self.topic:
@@ -885,6 +955,10 @@ class NotifyTelegram(NotifyBase):
# Store our chat ids (as these are the remaining entries)
results['targets'] = entries
+ # content to be displayed 'before' or 'after' attachments
+ if 'content' in results['qsd'] and len(results['qsd']['content']):
+ results['content'] = results['qsd']['content']
+
# 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']):
diff --git a/libs/apprise/plugins/NotifyTwilio.py b/libs/apprise/plugins/NotifyTwilio.py
index 08a3b2917..ab4c88e32 100644
--- a/libs/apprise/plugins/NotifyTwilio.py
+++ b/libs/apprise/plugins/NotifyTwilio.py
@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
-# BSD 3-Clause License
+# BSD 2-Clause License
#
# Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <[email protected]>
@@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
-# 3. Neither the name of the copyright holder nor the names of its
-# contributors may be used to endorse or promote products derived from
-# this software without specific prior written permission.
-#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
diff --git a/libs/apprise/plugins/NotifyTwist.py b/libs/apprise/plugins/NotifyTwist.py
index ea7b19760..36a55313a 100644
--- a/libs/apprise/plugins/NotifyTwist.py
+++ b/libs/apprise/plugins/NotifyTwist.py
@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
-# BSD 3-Clause License
+# BSD 2-Clause License
#
# Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <[email protected]>
@@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
-# 3. Neither the name of the copyright holder nor the names of its
-# contributors may be used to endorse or promote products derived from
-# this software without specific prior written permission.
-#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
@@ -106,10 +102,12 @@ class NotifyTwist(NotifyBase):
'name': _('Password'),
'type': 'string',
'private': True,
+ 'required': True,
},
'email': {
'name': _('Email'),
'type': 'string',
+ 'required': True,
},
'target_channel': {
'name': _('Target Channel'),
diff --git a/libs/apprise/plugins/NotifyTwitter.py b/libs/apprise/plugins/NotifyTwitter.py
index 7862d0042..3647c8b39 100644
--- a/libs/apprise/plugins/NotifyTwitter.py
+++ b/libs/apprise/plugins/NotifyTwitter.py
@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
-# BSD 3-Clause License
+# BSD 2-Clause License
#
# Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <[email protected]>
@@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
-# 3. Neither the name of the copyright holder nor the names of its
-# contributors may be used to endorse or promote products derived from
-# this software without specific prior written permission.
-#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
@@ -36,6 +32,7 @@ import re
import requests
from copy import deepcopy
from datetime import datetime
+from datetime import timezone
from requests_oauthlib import OAuth1
from json import dumps
from json import loads
@@ -82,11 +79,14 @@ class NotifyTwitter(NotifyBase):
service_url = 'https://twitter.com/'
# The default secure protocol is twitter.
- secure_protocol = ('twitter', 'tweet')
+ secure_protocol = ('x', 'twitter', 'tweet')
# A URL that takes you to the setup/help of the specific protocol
setup_url = 'https://github.com/caronc/apprise/wiki/Notify_twitter'
+ # Support attachments
+ attachment_support = True
+
# Do not set body_maxlen as it is set in a property value below
# since the length varies depending if we are doing a direct message
# or a tweet
@@ -124,13 +124,14 @@ class NotifyTwitter(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 tweets a day
# This value only get's adjusted if the server sets it that way
ratelimit_remaining = 1
templates = (
+ '{schema}://{ckey}/{csecret}/{akey}/{asecret}',
'{schema}://{ckey}/{csecret}/{akey}/{asecret}/{targets}',
)
@@ -283,7 +284,7 @@ class NotifyTwitter(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:
@@ -412,7 +413,7 @@ class NotifyTwitter(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))
@@ -512,7 +513,7 @@ class NotifyTwitter(NotifyBase):
'additional_owners':
','.join([str(x) for x in targets.values()])
}
- if no:
+ if no or not body:
# strip text and replace it with the image representation
_data['text'] = \
'{:02d}/{:02d}'.format(no + 1, len(attachments))
@@ -678,7 +679,7 @@ class NotifyTwitter(NotifyBase):
# Twitter 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
@@ -736,8 +737,9 @@ class NotifyTwitter(NotifyBase):
# Capture rate limiting if possible
self.ratelimit_remaining = \
int(r.headers.get('x-rate-limit-remaining'))
- self.ratelimit_reset = datetime.utcfromtimestamp(
- int(r.headers.get('x-rate-limit-reset')))
+ self.ratelimit_reset = datetime.fromtimestamp(
+ int(r.headers.get('x-rate-limit-reset')), timezone.utc
+ ).replace(tzinfo=None)
except (TypeError, ValueError):
# This is returned if we could not retrieve this information
diff --git a/libs/apprise/plugins/NotifyVoipms.py b/libs/apprise/plugins/NotifyVoipms.py
index a4ec5ae1b..c39da4dfe 100644
--- a/libs/apprise/plugins/NotifyVoipms.py
+++ b/libs/apprise/plugins/NotifyVoipms.py
@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
-# BSD 3-Clause License
+# BSD 2-Clause License
#
# Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <[email protected]>
@@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
-# 3. Neither the name of the copyright holder nor the names of its
-# contributors may be used to endorse or promote products derived from
-# this software without specific prior written permission.
-#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
@@ -78,7 +74,6 @@ class NotifyVoipms(NotifyBase):
# Define object templates
templates = (
- '{schema}://{password}:{email}',
'{schema}://{password}:{email}/{from_phone}/{targets}',
)
@@ -111,6 +106,7 @@ class NotifyVoipms(NotifyBase):
'targets': {
'name': _('Targets'),
'type': 'list:string',
+ 'required': True,
},
})
diff --git a/libs/apprise/plugins/NotifyVonage.py b/libs/apprise/plugins/NotifyVonage.py
index bc3ab0647..48d823195 100644
--- a/libs/apprise/plugins/NotifyVonage.py
+++ b/libs/apprise/plugins/NotifyVonage.py
@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
-# BSD 3-Clause License
+# BSD 2-Clause License
#
# Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <[email protected]>
@@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
-# 3. Neither the name of the copyright holder nor the names of its
-# contributors may be used to endorse or promote products derived from
-# this software without specific prior written permission.
-#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
diff --git a/libs/apprise/plugins/NotifyWebexTeams.py b/libs/apprise/plugins/NotifyWebexTeams.py
index 6b953b711..67ed4e4b8 100644
--- a/libs/apprise/plugins/NotifyWebexTeams.py
+++ b/libs/apprise/plugins/NotifyWebexTeams.py
@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
-# BSD 3-Clause License
+# BSD 2-Clause License
#
# Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <[email protected]>
@@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
-# 3. Neither the name of the copyright holder nor the names of its
-# contributors may be used to endorse or promote products derived from
-# this software without specific prior written permission.
-#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
diff --git a/libs/apprise/plugins/NotifyWhatsApp.py b/libs/apprise/plugins/NotifyWhatsApp.py
new file mode 100644
index 000000000..efa90f89b
--- /dev/null
+++ b/libs/apprise/plugins/NotifyWhatsApp.py
@@ -0,0 +1,559 @@
+# -*- coding: utf-8 -*-
+# BSD 2-Clause License
+#
+# Apprise - Push Notification Library.
+# Copyright (c) 2023, Chris Caron <[email protected]>
+#
+# 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 Source:
+# https://developers.facebook.com/docs/whatsapp/cloud-api/reference/messages
+#
+# 1. Register a developer account with Meta:
+# https://developers.facebook.com/docs/whatsapp/cloud-api/get-started
+# 2. Enable 2 Factor Authentication (2FA) with your account (if not done
+# already)
+# 3. Create a App using WhatsApp Product. There are 2 to create an app from
+# Do NOT chose the WhatsApp Webhook one (choose the other)
+#
+# When you click on the API Setup section of your new app you need to record
+# both the access token and the From Phone Number ID. Note that this not the
+# from phone number itself, but it's ID. It's displayed below and contains
+# way more numbers then your typical phone number
+
+import re
+import requests
+from json import loads, dumps
+from .NotifyBase import NotifyBase
+from ..common import NotifyType
+from ..utils import is_phone_no
+from ..utils import parse_phone_no
+from ..utils import validate_regex
+from ..AppriseLocale import gettext_lazy as _
+
+
+class NotifyWhatsApp(NotifyBase):
+ """
+ A wrapper for WhatsApp Notifications
+ """
+
+ # The default descriptive name associated with the Notification
+ service_name = 'WhatsApp'
+
+ # The services URL
+ service_url = \
+ 'https://developers.facebook.com/docs/whatsapp/cloud-api/get-started'
+
+ # All notification requests are secure
+ secure_protocol = 'whatsapp'
+
+ # Allow 300 requests per minute.
+ # 60/300 = 0.2
+ request_rate_per_sec = 0.20
+
+ # Facebook Graph version
+ fb_graph_version = 'v17.0'
+
+ # A URL that takes you to the setup/help of the specific protocol
+ setup_url = 'https://github.com/caronc/apprise/wiki/Notify_whatsapp'
+
+ # WhatsApp Message Notification URL
+ notify_url = 'https://graph.facebook.com/{fb_ver}/{phone_id}/messages'
+
+ # The maximum length of the body
+ body_maxlen = 1024
+
+ # 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}://{token}@{from_phone_id}/{targets}',
+ '{schema}://{template}:{token}@{from_phone_id}/{targets}',
+ )
+
+ # Define our template tokens
+ template_tokens = dict(NotifyBase.template_tokens, **{
+ 'token': {
+ 'name': _('Access Token'),
+ 'type': 'string',
+ 'private': True,
+ 'required': True,
+ 'regex': (r'^[a-z0-9]+$', 'i'),
+ },
+ 'template': {
+ 'name': _('Template Name'),
+ 'type': 'string',
+ 'required': False,
+ 'regex': (r'^[^\s]+$', 'i'),
+ },
+ 'from_phone_id': {
+ 'name': _('From Phone ID'),
+ 'type': 'string',
+ 'private': True,
+ 'required': True,
+ 'regex': (r'^[0-9]+$', 'i'),
+ },
+ 'target_phone': {
+ 'name': _('Target Phone No'),
+ 'type': 'string',
+ 'prefix': '+',
+ 'regex': (r'^[0-9\s)(+-]+$', 'i'),
+ 'map_to': 'targets',
+ },
+ 'targets': {
+ 'name': _('Targets'),
+ 'type': 'list:string',
+ },
+ 'language': {
+ 'name': _('Language'),
+ 'type': 'string',
+ 'default': 'en_US',
+ 'regex': (r'^[^0-9\s]+$', 'i'),
+ },
+ })
+
+ # Define our template arguments
+ template_args = dict(NotifyBase.template_args, **{
+ 'to': {
+ 'alias_of': 'targets',
+ },
+ 'from': {
+ 'alias_of': 'from_phone_id',
+ },
+ 'token': {
+ 'alias_of': 'token',
+ },
+ 'template': {
+ 'alias_of': 'template',
+ },
+ 'lang': {
+ 'alias_of': 'language',
+ },
+ })
+
+ # Our supported mappings and component keys
+ component_key_re = re.compile(
+ r'(?P<key>((?P<id>[1-9][0-9]*)|(?P<map>body|type)))', re.IGNORECASE)
+
+ # Define any kwargs we're using
+ template_kwargs = {
+ 'template_mapping': {
+ 'name': _('Template Mapping'),
+ 'prefix': ':',
+ },
+ }
+
+ def __init__(self, token, from_phone_id, template=None, targets=None,
+ language=None, template_mapping=None, **kwargs):
+ """
+ Initialize WhatsApp Object
+ """
+ super().__init__(**kwargs)
+
+ # The Access Token associated with the account
+ self.token = validate_regex(
+ token, *self.template_tokens['token']['regex'])
+ if not self.token:
+ msg = 'An invalid WhatsApp Access Token ' \
+ '({}) was specified.'.format(token)
+ self.logger.warning(msg)
+ raise TypeError(msg)
+
+ # The From Phone ID associated with the account
+ self.from_phone_id = validate_regex(
+ from_phone_id, *self.template_tokens['from_phone_id']['regex'])
+ if not self.from_phone_id:
+ msg = 'An invalid WhatsApp From Phone ID ' \
+ '({}) was specified.'.format(from_phone_id)
+ self.logger.warning(msg)
+ raise TypeError(msg)
+
+ # The template to associate with the message
+ if template:
+ self.template = validate_regex(
+ template, *self.template_tokens['template']['regex'])
+ if not self.template:
+ msg = 'An invalid WhatsApp Template Name ' \
+ '({}) was specified.'.format(template)
+ self.logger.warning(msg)
+ raise TypeError(msg)
+
+ # The Template language Code to use
+ if language:
+ self.language = validate_regex(
+ language, *self.template_tokens['language']['regex'])
+ if not self.language:
+ msg = 'An invalid WhatsApp Template Language Code ' \
+ '({}) was specified.'.format(language)
+ self.logger.warning(msg)
+ raise TypeError(msg)
+ else:
+ self.language = self.template_tokens['language']['default']
+ else:
+ #
+ # Message Mode
+ #
+ self.template = None
+
+ # 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('+{}'.format(result['full']))
+
+ self.template_mapping = {}
+ if template_mapping:
+ # Store our extra payload entries
+ self.template_mapping.update(template_mapping)
+
+ # Validate Mapping and prepare Components
+ self.components = dict()
+ self.component_keys = list()
+ for key, val in self.template_mapping.items():
+ matched = self.component_key_re.match(key)
+ if not matched:
+ msg = 'An invalid Template Component ID ' \
+ '({}) was specified.'.format(key)
+ self.logger.warning(msg)
+ raise TypeError(msg)
+
+ if matched.group('id'):
+ #
+ # Manual Component Assigment (by id)
+ #
+ index = matched.group('id')
+ map_to = {
+ "type": "text",
+ "text": val,
+ }
+
+ else: # matched.group('map')
+ map_to = matched.group('map').lower()
+ matched = self.component_key_re.match(val)
+ if not (matched and matched.group('id')):
+ msg = 'An invalid Template Component Mapping ' \
+ '(:{}={}) was specified.'.format(key, val)
+ self.logger.warning(msg)
+ raise TypeError(msg)
+ index = matched.group('id')
+
+ if index in self.components:
+ msg = 'The Template Component index ' \
+ '({}) was already assigned.'.format(key)
+ self.logger.warning(msg)
+ raise TypeError(msg)
+
+ self.components[index] = map_to
+ self.component_keys = self.components.keys()
+ # Adjust sorting and assume that the user put the order correctly;
+ # if not Facebook just won't be very happy and will reject the
+ # message
+ sorted(self.component_keys)
+
+ return
+
+ def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs):
+ """
+ Perform WhatsApp Notification
+ """
+
+ if not self.targets:
+ self.logger.warning(
+ 'There are no valid WhatsApp targets to notify.')
+ return False
+
+ # error tracking (used for function return)
+ has_error = False
+
+ # Prepare our URL
+ url = self.notify_url.format(
+ fb_ver=self.fb_graph_version,
+ phone_id=self.from_phone_id,
+ )
+
+ # Prepare our headers
+ headers = {
+ 'User-Agent': self.app_id,
+ 'Accept': 'application/json',
+ 'Content-Type': 'application/json',
+ 'Authorization': f'Bearer {self.token}',
+ }
+
+ payload = {
+ 'messaging_product': 'whatsapp',
+ # The To gets populated in the loop below
+ 'to': None,
+ }
+
+ if not self.template:
+ #
+ # Send Message
+ #
+ payload.update({
+ 'recipient_type': "individual",
+ 'type': 'text',
+ 'text': {"body": body},
+ })
+
+ else:
+ #
+ # Send Template
+ #
+ payload.update({
+ 'type': 'template',
+ "template": {
+ "name": self.template,
+ "language": {"code": self.language},
+ },
+ })
+
+ if self.components:
+ payload['template']['components'] = [
+ {
+ "type": "body",
+ "parameters": [],
+ }
+ ]
+ for key in self.component_keys:
+ if isinstance(self.components[key], dict):
+ # Manual Assignment
+ payload['template']['components'][0]["parameters"]\
+ .append(self.components[key])
+ continue
+
+ # Mapping of body and/or notify type
+ payload['template']['components'][0]["parameters"].append({
+ "type": "text",
+ "text": body if self.components[key] == 'body'
+ else notify_type,
+ })
+
+ # Create a copy of the targets list
+ targets = list(self.targets)
+
+ while len(targets):
+ # Get our target to notify
+ target = targets.pop(0)
+
+ # Prepare our user
+ payload['to'] = target
+
+ # Some Debug Logging
+ self.logger.debug('WhatsApp POST URL: {} (cert_verify={})'.format(
+ url, self.verify_certificate))
+ self.logger.debug('WhatsApp Payload: {}' .format(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 not in (
+ requests.codes.created, requests.codes.ok):
+ # We had a problem
+ status_str = \
+ NotifyBase.http_response_code_lookup(r.status_code)
+
+ # set up our status code to use
+ status_code = r.status_code
+
+ try:
+ # Update our status response if we can
+ json_response = loads(r.content)
+ status_code = \
+ json_response['error'].get('code', status_code)
+ status_str = \
+ json_response['error'].get('message', status_str)
+
+ except (AttributeError, TypeError, ValueError, KeyError):
+ # KeyError = r.content is parseable but does not
+ # contain 'error'
+ # ValueError = r.content is Unparsable
+ # TypeError = r.content is None
+ # AttributeError = r is None
+
+ # We could not parse JSON response.
+ # We will just use the status we already have.
+ pass
+
+ self.logger.warning(
+ 'Failed to send WhatsApp notification to {}: '
+ '{}{}error={}.'.format(
+ target,
+ status_str,
+ ', ' if status_str else '',
+ status_code))
+
+ self.logger.debug(
+ 'Response Details:\r\n{}'.format(r.content))
+
+ # Mark our failure
+ has_error = True
+ continue
+
+ else:
+ self.logger.info(
+ 'Sent WhatsApp notification to {}.'.format(target))
+
+ except requests.RequestException as e:
+ self.logger.warning(
+ 'A Connection error occurred sending WhatsApp:%s ' % (
+ target) + 'notification.'
+ )
+ 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 = {}
+ if self.template:
+ # Add language to our URL
+ params['lang'] = self.language
+
+ # Extend our parameters
+ params.update(self.url_parameters(privacy=privacy, *args, **kwargs))
+
+ # 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}://{template}{token}@{from_id}/{targets}/?{params}'\
+ .format(
+ schema=self.secure_protocol,
+ from_id=self.pprint(
+ self.from_phone_id, privacy, safe=''),
+ token=self.pprint(self.token, privacy, safe=''),
+ template='' if not self.template
+ else '{}:'.format(
+ NotifyWhatsApp.quote(self.template, safe='')),
+ targets='/'.join(
+ [NotifyWhatsApp.quote(x, safe='') for x in self.targets]),
+ params=NotifyWhatsApp.urlencode(params))
+
+ def __len__(self):
+ """
+ Returns the number of targets associated with this notification
+ """
+ targets = len(self.targets)
+ return targets if targets > 0 else 1
+
+ @staticmethod
+ def parse_url(url):
+ """
+ Parses the URL and returns enough arguments that can allow
+ us to re-instantiate this object.
+
+ """
+ results = NotifyBase.parse_url(url, verify_host=False)
+
+ if not results:
+ # We're done early as we couldn't load the results
+ return results
+
+ # Get our entries; split_path() looks after unquoting content for us
+ # by default
+ results['targets'] = NotifyWhatsApp.split_path(results['fullpath'])
+
+ # The hostname is our From Phone ID
+ results['from_phone_id'] = NotifyWhatsApp.unquote(results['host'])
+
+ # Determine if we have a Template, otherwise load our token
+ if results['password']:
+ #
+ # Template Mode
+ #
+ results['template'] = NotifyWhatsApp.unquote(results['user'])
+ results['token'] = NotifyWhatsApp.unquote(results['password'])
+
+ else:
+ #
+ # Message Mode
+ #
+ results['token'] = NotifyWhatsApp.unquote(results['user'])
+
+ # Access token
+ if 'token' in results['qsd'] and len(results['qsd']['token']):
+ # Extract the account sid from an argument
+ results['token'] = \
+ NotifyWhatsApp.unquote(results['qsd']['token'])
+
+ # Template
+ if 'template' in results['qsd'] and len(results['qsd']['template']):
+ results['template'] = results['qsd']['template']
+
+ # Template Language
+ if 'lang' in results['qsd'] and len(results['qsd']['lang']):
+ results['language'] = results['qsd']['lang']
+
+ # 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['from_phone_id'] = \
+ NotifyWhatsApp.unquote(results['qsd']['from'])
+ if 'source' in results['qsd'] and \
+ len(results['qsd']['source']):
+ results['from_phone_id'] = \
+ NotifyWhatsApp.unquote(results['qsd']['source'])
+
+ # 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'] += \
+ NotifyWhatsApp.parse_phone_no(results['qsd']['to'])
+
+ # store any additional payload extra's defined
+ results['template_mapping'] = {
+ NotifyWhatsApp.unquote(x): NotifyWhatsApp.unquote(y)
+ for x, y in results['qsd:'].items()
+ }
+
+ return results
diff --git a/libs/apprise/plugins/NotifyWindows.py b/libs/apprise/plugins/NotifyWindows.py
index 70f438894..226cf92bf 100644
--- a/libs/apprise/plugins/NotifyWindows.py
+++ b/libs/apprise/plugins/NotifyWindows.py
@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
-# BSD 3-Clause License
+# BSD 2-Clause License
#
# Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <[email protected]>
@@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
-# 3. Neither the name of the copyright holder nor the names of its
-# contributors may be used to endorse or promote products derived from
-# this software without specific prior written permission.
-#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
diff --git a/libs/apprise/plugins/NotifyXBMC.py b/libs/apprise/plugins/NotifyXBMC.py
index 963a74d88..a973989ac 100644
--- a/libs/apprise/plugins/NotifyXBMC.py
+++ b/libs/apprise/plugins/NotifyXBMC.py
@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
-# BSD 3-Clause License
+# BSD 2-Clause License
#
# Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <[email protected]>
@@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
-# 3. Neither the name of the copyright holder nor the names of its
-# contributors may be used to endorse or promote products derived from
-# this software without specific prior written permission.
-#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
diff --git a/libs/apprise/plugins/NotifyXML.py b/libs/apprise/plugins/NotifyXML.py
index 04cdac10e..20eeb114c 100644
--- a/libs/apprise/plugins/NotifyXML.py
+++ b/libs/apprise/plugins/NotifyXML.py
@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
-# BSD 3-Clause License
+# BSD 2-Clause License
#
# Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <[email protected]>
@@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
-# 3. Neither the name of the copyright holder nor the names of its
-# contributors may be used to endorse or promote products derived from
-# this software without specific prior written permission.
-#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
@@ -79,6 +75,9 @@ class NotifyXML(NotifyBase):
# A URL that takes you to the setup/help of the specific protocol
setup_url = 'https://github.com/caronc/apprise/wiki/Notify_Custom_XML'
+ # Support attachments
+ attachment_support = True
+
# Allows the user to specify the NotifyImageSize object
image_size = NotifyImageSize.XY_128
@@ -213,9 +212,6 @@ class NotifyXML(NotifyBase):
# Store our extra headers
self.headers.update(headers)
- # Set our xsd url
- self.xsd_url = self.xsd_default_url.format(version=self.xsd_ver)
-
self.payload_overrides = {}
self.payload_extras = {}
if payload:
@@ -237,11 +233,13 @@ class NotifyXML(NotifyBase):
self.payload_map[key] = v
self.payload_overrides[key] = v
- # Over-ride XSD URL as data is no longer known
- self.xsd_url = None
-
else:
self.payload_extras[key] = v
+
+ # Set our xsd url
+ self.xsd_url = None if self.payload_overrides or self.payload_extras \
+ else self.xsd_default_url.format(version=self.xsd_ver)
+
return
def url(self, privacy=False, *args, **kwargs):
@@ -340,7 +338,7 @@ class NotifyXML(NotifyBase):
['<{}>{}</{}>'.format(k, v, k) for k, v in payload_base.items()])
attachments = []
- if attach:
+ if attach and self.attachment_support:
for attachment in attach:
# Perform some simple error checking
if not attachment:
diff --git a/libs/apprise/plugins/NotifyZulip.py b/libs/apprise/plugins/NotifyZulip.py
index f9521ae19..f0d0cd8d5 100644
--- a/libs/apprise/plugins/NotifyZulip.py
+++ b/libs/apprise/plugins/NotifyZulip.py
@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
-# BSD 3-Clause License
+# BSD 2-Clause License
#
# Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <[email protected]>
@@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
-# 3. Neither the name of the copyright holder nor the names of its
-# contributors may be used to endorse or promote products derived from
-# this software without specific prior written permission.
-#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
@@ -131,6 +127,7 @@ class NotifyZulip(NotifyBase):
'name': _('Bot Name'),
'type': 'string',
'regex': (r'^[A-Z0-9_-]{1,32}$', 'i'),
+ 'required': True,
},
'organization': {
'name': _('Organization'),
diff --git a/libs/apprise/plugins/__init__.py b/libs/apprise/plugins/__init__.py
index 5560568b7..27afef05c 100644
--- a/libs/apprise/plugins/__init__.py
+++ b/libs/apprise/plugins/__init__.py
@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
-# BSD 3-Clause License
+# BSD 2-Clause License
#
# Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <[email protected]>
@@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
-# 3. Neither the name of the copyright holder nor the names of its
-# contributors may be used to endorse or promote products derived from
-# this software without specific prior written permission.
-#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
@@ -165,6 +161,9 @@ def _sanitize_token(tokens, default_delimiter):
"""
+ # Used for tracking groups
+ group_map = {}
+
# Iterate over our tokens
for key in tokens.keys():
@@ -181,14 +180,27 @@ def _sanitize_token(tokens, default_delimiter):
# Default type to key
tokens[key]['map_to'] = key
+ # Track our map_to objects
+ if tokens[key]['map_to'] not in group_map:
+ group_map[tokens[key]['map_to']] = set()
+ group_map[tokens[key]['map_to']].add(key)
+
if 'type' not in tokens[key]:
# Default type to string
tokens[key]['type'] = 'string'
- elif tokens[key]['type'].startswith('list') \
- and 'delim' not in tokens[key]:
- # Default list delimiter (if not otherwise specified)
- tokens[key]['delim'] = default_delimiter
+ elif tokens[key]['type'].startswith('list'):
+ if 'delim' not in tokens[key]:
+ # Default list delimiter (if not otherwise specified)
+ tokens[key]['delim'] = default_delimiter
+
+ if key in group_map[tokens[key]['map_to']]: # pragma: no branch
+ # Remove ourselves from the list
+ group_map[tokens[key]['map_to']].remove(key)
+
+ # Pointing to the set directly so we can dynamically update
+ # ourselves
+ tokens[key]['group'] = group_map[tokens[key]['map_to']]
elif tokens[key]['type'].startswith('choice') \
and 'default' not in tokens[key] \
@@ -266,6 +278,13 @@ def details(plugin):
# # Identifies if the entry specified is required or not
# 'required': True,
#
+ # # Identifies all tokens detected to be associated with the
+ # # list:string
+ # # This is ony present in list:string objects and is only set
+ # # if this element acts as an alias for several other
+ # # kwargs/fields.
+ # 'group': [],
+ #
# # Identify a default value
# 'default': 'http',
#
diff --git a/libs/apprise/utils.py b/libs/apprise/utils.py
index 561a5a232..8d644ce90 100644
--- a/libs/apprise/utils.py
+++ b/libs/apprise/utils.py
@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
-# BSD 3-Clause License
+# BSD 2-Clause License
#
# Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <[email protected]>
@@ -14,10 +14,6 @@
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
-# 3. Neither the name of the copyright holder nor the names of its
-# contributors may be used to endorse or promote products derived from
-# this software without specific prior written permission.
-#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
@@ -36,6 +32,7 @@ import json
import contextlib
import os
import hashlib
+import locale
from itertools import chain
from os.path import expanduser
from functools import reduce
@@ -142,14 +139,14 @@ NOTIFY_CUSTOM_DEL_TOKENS = re.compile(r'^-(?P<key>.*)\s*')
NOTIFY_CUSTOM_COLON_TOKENS = re.compile(r'^:(?P<key>.*)\s*')
# Used for attempting to acquire the schema if the URL can't be parsed.
-GET_SCHEMA_RE = re.compile(r'\s*(?P<schema>[a-z0-9]{2,9})://.*$', re.I)
+GET_SCHEMA_RE = re.compile(r'\s*(?P<schema>[a-z0-9]{1,12})://.*$', re.I)
# Used for validating that a provided entry is indeed a schema
# this is slightly different then the GET_SCHEMA_RE above which
# insists the schema is only valid with a :// entry. this one
# extrapolates the individual entries
URL_DETAILS_RE = re.compile(
- r'\s*(?P<schema>[a-z0-9]{2,9})(://(?P<base>.*))?$', re.I)
+ r'\s*(?P<schema>[a-z0-9]{1,12})(://(?P<base>.*))?$', re.I)
# Regular expression based and expanded from:
# http://www.regular-expressions.info/email.html
@@ -193,7 +190,7 @@ CALL_SIGN_DETECTION_RE = re.compile(
# Regular expression used to destinguish between multiple URLs
URL_DETECTION_RE = re.compile(
- r'([a-z0-9]+?:\/\/.*?)(?=$|[\s,]+[a-z0-9]{2,9}?:\/\/)', re.I)
+ r'([a-z0-9]+?:\/\/.*?)(?=$|[\s,]+[a-z0-9]{1,12}?:\/\/)', re.I)
EMAIL_DETECTION_RE = re.compile(
r'[\s,]*([^@]+@.*?)(?=$|[\s,]+'
@@ -1119,7 +1116,7 @@ def urlencode(query, doseq=False, safe='', encoding=None, errors=None):
errors=errors)
-def parse_list(*args):
+def parse_list(*args, cast=None):
"""
Take a string list and break it into a delimited
list of arguments. This funciton also supports
@@ -1142,6 +1139,9 @@ def parse_list(*args):
result = []
for arg in args:
+ if not isinstance(arg, (str, set, list, bool, tuple)) and arg and cast:
+ arg = cast(arg)
+
if isinstance(arg, str):
result += re.split(STRING_DELIMITERS, arg)
@@ -1154,7 +1154,6 @@ def parse_list(*args):
# Since Python v3 returns a filter (iterator) whereas Python v2 returned
# a list, we need to change it into a list object to remain compatible with
# both distribution types.
- # TODO: Review after dropping support for Python 2.
return sorted([x for x in filter(bool, list(set(result)))])
@@ -1488,7 +1487,7 @@ def environ(*remove, **update):
# Create a backup of our environment for restoration purposes
env_orig = os.environ.copy()
-
+ loc_orig = locale.getlocale()
try:
os.environ.update(update)
[os.environ.pop(k, None) for k in remove]
@@ -1497,6 +1496,13 @@ def environ(*remove, **update):
finally:
# Restore our snapshot
os.environ = env_orig.copy()
+ try:
+ # Restore locale
+ locale.setlocale(locale.LC_ALL, loc_orig)
+
+ except locale.Error:
+ # Thrown in py3.6
+ pass
def apply_template(template, app_mode=TemplateType.RAW, **kwargs):