Script 'mail_helper' called by obssrc Hello community, here is the log from the commit of package python-apprise for openSUSE:Factory checked in at 2024-03-11 15:32:26 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/python-apprise (Old) and /work/SRC/openSUSE:Factory/.python-apprise.new.1770 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "python-apprise" Mon Mar 11 15:32:26 2024 rev:3 rq:1156760 version:1.7.4 Changes: -------- --- /work/SRC/openSUSE:Factory/python-apprise/python-apprise.changes 2024-03-04 21:25:04.130249288 +0100 +++ /work/SRC/openSUSE:Factory/.python-apprise.new.1770/python-apprise.changes 2024-03-11 15:38:44.645047888 +0100 @@ -1,0 +2,10 @@ +Sun Mar 10 07:01:27 UTC 2024 - Joshua Smith <jsmith...@gmail.com> + +- Update to version 1.7.4: + Features: + * LunaSea support added + * .conf configuration file support added to CLI. + Fixes: + * Fix: Custom module deadlock which fixes bug from 1.7.3 + +------------------------------------------------------------------- Old: ---- apprise-1.7.3.tar.gz New: ---- apprise-1.7.4.tar.gz ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ python-apprise.spec ++++++ --- /var/tmp/diff_new_pack.mpXnZA/_old 2024-03-11 15:38:45.113065123 +0100 +++ /var/tmp/diff_new_pack.mpXnZA/_new 2024-03-11 15:38:45.113065123 +0100 @@ -26,8 +26,9 @@ %endif Name: python-apprise -Version: 1.7.3 +Version: 1.7.4 Release: 0 +Group: Development/Libraries/Python Summary: A simple wrapper to many popular notification services used today License: BSD-2-Clause URL: https://github.com/caronc/apprise @@ -46,7 +47,6 @@ BuildRequires: %{python_module PyYAML} BuildRequires: %{python_module certifi} BuildRequires: %{python_module click >= 5.0} -BuildRequires: %{python_module cryptography} BuildRequires: %{python_module dbus-python} BuildRequires: %{python_module paho-mqtt} BuildRequires: %{python_module pip} @@ -63,7 +63,6 @@ Requires: python-PyYAML Requires: python-certifi Requires: python-click >= 5.0 -Requires: python-cryptography Requires: python-requests Requires: python-requests-oauthlib Recommends: python-paho-mqtt ++++++ apprise-1.7.3.tar.gz -> apprise-1.7.4.tar.gz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/apprise-1.7.3/KEYWORDS new/apprise-1.7.4/KEYWORDS --- old/apprise-1.7.3/KEYWORDS 2024-03-03 23:42:49.000000000 +0100 +++ new/apprise-1.7.4/KEYWORDS 2024-03-09 22:39:52.000000000 +0100 @@ -36,6 +36,7 @@ Kumulos LaMetric Line +LunaSea MacOSX Mailgun Mastodon diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/apprise-1.7.3/PKG-INFO new/apprise-1.7.4/PKG-INFO --- old/apprise-1.7.3/PKG-INFO 2024-03-04 00:04:18.300392400 +0100 +++ new/apprise-1.7.4/PKG-INFO 2024-03-09 22:41:33.454005200 +0100 @@ -1,12 +1,12 @@ Metadata-Version: 2.1 Name: apprise -Version: 1.7.3 +Version: 1.7.4 Summary: Push Notifications that work with just about every platform! Home-page: https://github.com/caronc/apprise Author: Chris Caron Author-email: lead2g...@gmail.com License: BSD -Keywords: Alerts Apprise API Automated Packet Reporting System AWS Boxcar BulkSMS BulkVS Burst SMS Chat CLI ClickSend D7Networks Dapnet DBus DingTalk Discord Email Emby Enigma2 Faast FCM Flock Form Gnome Google Chat Gotify Growl Guilded Home Assistant httpSMS IFTTT Join JSON Kavenegar KODI Kumulos LaMetric Line MacOSX Mailgun Mastodon Matrix Mattermost MessageBird Microsoft Misskey MQTT MSG91 MSTeams Nextcloud NextcloudTalk Notica Notifiarr Notifico Ntfy Office365 OneSignal Opsgenie PagerDuty PagerTree ParsePlatform PopcornNotify Prowl PushBullet Pushed Pushjet PushMe Push Notifications Pushover PushSafer Pushy PushDeer Reddit Revolt Rocket.Chat RSyslog Ryver SendGrid ServerChan SES Signal SimplePush Sinch Slack SMSEagle SMS Manager SMTP2Go SNS SparkPost Streamlabs Stride Synology Chat Syslog Techulus Telegram Threema Gateway Twilio Twist Twitter Voipms Vonage Webex WeCom Bot WhatsApp Windows XBMC XML Zulip +Keywords: Alerts Apprise API Automated Packet Reporting System AWS Boxcar BulkSMS BulkVS Burst SMS Chat CLI ClickSend D7Networks Dapnet DBus DingTalk Discord Email Emby Enigma2 Faast FCM Flock Form Gnome Google Chat Gotify Growl Guilded Home Assistant httpSMS IFTTT Join JSON Kavenegar KODI Kumulos LaMetric Line LunaSea MacOSX Mailgun Mastodon Matrix Mattermost MessageBird Microsoft Misskey MQTT MSG91 MSTeams Nextcloud NextcloudTalk Notica Notifiarr Notifico Ntfy Office365 OneSignal Opsgenie PagerDuty PagerTree ParsePlatform PopcornNotify Prowl PushBullet Pushed Pushjet PushMe Push Notifications Pushover PushSafer Pushy PushDeer Reddit Revolt Rocket.Chat RSyslog Ryver SendGrid ServerChan SES Signal SimplePush Sinch Slack SMSEagle SMS Manager SMTP2Go SNS SparkPost Streamlabs Stride Synology Chat Syslog Techulus Telegram Threema Gateway Twilio Twist Twitter Voipms Vonage Webex WeCom Bot WhatsApp Windows XBMC XML Zulip Classifier: Development Status :: 5 - Production/Stable Classifier: Intended Audience :: Developers Classifier: Intended Audience :: System Administrators @@ -108,6 +108,7 @@ | [Kumulos](https://github.com/caronc/apprise/wiki/Notify_kumulos) | kumulos:// | (TCP) 443 | kumulos://apikey/serverkey | [LaMetric Time](https://github.com/caronc/apprise/wiki/Notify_lametric) | lametric:// | (TCP) 443 | lametric://apikey@device_ipaddr<br/>lametric://apikey@hostname:port<br/>lametric://client_id@client_secret | [Line](https://github.com/caronc/apprise/wiki/Notify_line) | line:// | (TCP) 443 | line://Token@User<br/>line://Token/User1/User2/UserN +| [LunaSea](https://github.com/caronc/apprise/wiki/Notify_lunasea) | lunasea:// | (TCP) 80 or 443 | lunasea://user:pass@+FireBaseDevice/<br/>lunasea://user:pass@FireBaseUser/<br/>lunasea://user:pass@hostname/+FireBaseDevice/<br/>lunasea://user:pass@hostname/@FireBaseUser/ | [Mailgun](https://github.com/caronc/apprise/wiki/Notify_mailgun) | mailgun:// | (TCP) 443 | mailgun://user@hostname/apikey<br />mailgun://user@hostname/apikey/email<br />mailgun://user@hostname/apikey/email1/email2/emailN<br />mailgun://user@hostname/apikey/?name="From%20User" | [Mastodon](https://github.com/caronc/apprise/wiki/Notify_mastodon) | mastodon:// or mastodons://| (TCP) 80 or 443 | mastodon://access_key@hostname<br />mastodon://access_key@hostname/@user<br />mastodon://access_key@hostname/@user1/@user2/@userN | [Matrix](https://github.com/caronc/apprise/wiki/Notify_matrix) | matrix:// or matrixs:// | (TCP) 80 or 443 | matrix://hostname<br />matrix://user@hostname<br />matrixs://user:pass@hostname:port/#room_alias<br />matrixs://user:pass@hostname:port/!room_id<br />matrixs://user:pass@hostname:port/#room_alias/!room_id/#room2<br />matrixs://token@hostname:port/?webhook=matrix<br />matrix://user:token@hostname/?webhook=slack&format=markdown @@ -263,43 +264,35 @@ # By default if no url or configuration is specified apprise will attempt to load # configuration files (if present) from: # ~/.apprise -# ~/.apprise.yml # ~/.apprise.yaml -# ~/.config/apprise -# ~/.config/apprise.yml +# ~/.config/apprise.conf # ~/.config/apprise.yaml -# /etc/apprise -# /etc/apprise.yml +# /etc/apprise.conf # /etc/apprise.yaml # Also a subdirectory handling allows you to leverage plugins # ~/.apprise/apprise -# ~/.apprise/apprise.yml # ~/.apprise/apprise.yaml -# ~/.config/apprise/apprise -# ~/.config/apprise/apprise.yml +# ~/.config/apprise/apprise.conf # ~/.config/apprise/apprise.yaml -# /etc/apprise/apprise -# /etc/apprise/apprise.yml # /etc/apprise/apprise.yaml +# /etc/apprise/apprise.conf # Windows users can store their default configuration files here: -# %APPDATA%/Apprise/apprise -# %APPDATA%/Apprise/apprise.yml +# %APPDATA%/Apprise/apprise.conf # %APPDATA%/Apprise/apprise.yaml -# %LOCALAPPDATA%/Apprise/apprise -# %LOCALAPPDATA%/Apprise/apprise.yml +# %LOCALAPPDATA%/Apprise/apprise.conf # %LOCALAPPDATA%/Apprise/apprise.yaml -# %ALLUSERSPROFILE%\Apprise\apprise -# %ALLUSERSPROFILE%\Apprise\apprise.yml +# %ALLUSERSPROFILE%\Apprise\apprise.conf # %ALLUSERSPROFILE%\Apprise\apprise.yaml -# %PROGRAMFILES%\Apprise\apprise -# %PROGRAMFILES%\Apprise\apprise.yml +# %PROGRAMFILES%\Apprise\apprise.conf # %PROGRAMFILES%\Apprise\apprise.yaml -# %COMMONPROGRAMFILES%\Apprise\apprise -# %COMMONPROGRAMFILES%\Apprise\apprise.yml +# %COMMONPROGRAMFILES%\Apprise\apprise.conf # %COMMONPROGRAMFILES%\Apprise\apprise.yaml +# The configuration files specified above can also be identified with a `.yml` +# extension or even just entirely removing the `.conf` extension altogether. + # If you loaded one of those files, your command line gets really easy: apprise -vv -t 'my title' -b 'my notification body' diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/apprise-1.7.3/README.md new/apprise-1.7.4/README.md --- old/apprise-1.7.3/README.md 2024-03-04 00:01:28.000000000 +0100 +++ new/apprise-1.7.4/README.md 2024-03-09 22:39:52.000000000 +0100 @@ -77,6 +77,7 @@ | [Kumulos](https://github.com/caronc/apprise/wiki/Notify_kumulos) | kumulos:// | (TCP) 443 | kumulos://apikey/serverkey | [LaMetric Time](https://github.com/caronc/apprise/wiki/Notify_lametric) | lametric:// | (TCP) 443 | lametric://apikey@device_ipaddr<br/>lametric://apikey@hostname:port<br/>lametric://client_id@client_secret | [Line](https://github.com/caronc/apprise/wiki/Notify_line) | line:// | (TCP) 443 | line://Token@User<br/>line://Token/User1/User2/UserN +| [LunaSea](https://github.com/caronc/apprise/wiki/Notify_lunasea) | lunasea:// | (TCP) 80 or 443 | lunasea://user:pass@+FireBaseDevice/<br/>lunasea://user:pass@FireBaseUser/<br/>lunasea://user:pass@hostname/+FireBaseDevice/<br/>lunasea://user:pass@hostname/@FireBaseUser/ | [Mailgun](https://github.com/caronc/apprise/wiki/Notify_mailgun) | mailgun:// | (TCP) 443 | mailgun://user@hostname/apikey<br />mailgun://user@hostname/apikey/email<br />mailgun://user@hostname/apikey/email1/email2/emailN<br />mailgun://user@hostname/apikey/?name="From%20User" | [Mastodon](https://github.com/caronc/apprise/wiki/Notify_mastodon) | mastodon:// or mastodons://| (TCP) 80 or 443 | mastodon://access_key@hostname<br />mastodon://access_key@hostname/@user<br />mastodon://access_key@hostname/@user1/@user2/@userN | [Matrix](https://github.com/caronc/apprise/wiki/Notify_matrix) | matrix:// or matrixs:// | (TCP) 80 or 443 | matrix://hostname<br />matrix://user@hostname<br />matrixs://user:pass@hostname:port/#room_alias<br />matrixs://user:pass@hostname:port/!room_id<br />matrixs://user:pass@hostname:port/#room_alias/!room_id/#room2<br />matrixs://token@hostname:port/?webhook=matrix<br />matrix://user:token@hostname/?webhook=slack&format=markdown @@ -232,43 +233,35 @@ # By default if no url or configuration is specified apprise will attempt to load # configuration files (if present) from: # ~/.apprise -# ~/.apprise.yml # ~/.apprise.yaml -# ~/.config/apprise -# ~/.config/apprise.yml +# ~/.config/apprise.conf # ~/.config/apprise.yaml -# /etc/apprise -# /etc/apprise.yml +# /etc/apprise.conf # /etc/apprise.yaml # Also a subdirectory handling allows you to leverage plugins # ~/.apprise/apprise -# ~/.apprise/apprise.yml # ~/.apprise/apprise.yaml -# ~/.config/apprise/apprise -# ~/.config/apprise/apprise.yml +# ~/.config/apprise/apprise.conf # ~/.config/apprise/apprise.yaml -# /etc/apprise/apprise -# /etc/apprise/apprise.yml # /etc/apprise/apprise.yaml +# /etc/apprise/apprise.conf # Windows users can store their default configuration files here: -# %APPDATA%/Apprise/apprise -# %APPDATA%/Apprise/apprise.yml +# %APPDATA%/Apprise/apprise.conf # %APPDATA%/Apprise/apprise.yaml -# %LOCALAPPDATA%/Apprise/apprise -# %LOCALAPPDATA%/Apprise/apprise.yml +# %LOCALAPPDATA%/Apprise/apprise.conf # %LOCALAPPDATA%/Apprise/apprise.yaml -# %ALLUSERSPROFILE%\Apprise\apprise -# %ALLUSERSPROFILE%\Apprise\apprise.yml +# %ALLUSERSPROFILE%\Apprise\apprise.conf # %ALLUSERSPROFILE%\Apprise\apprise.yaml -# %PROGRAMFILES%\Apprise\apprise -# %PROGRAMFILES%\Apprise\apprise.yml +# %PROGRAMFILES%\Apprise\apprise.conf # %PROGRAMFILES%\Apprise\apprise.yaml -# %COMMONPROGRAMFILES%\Apprise\apprise -# %COMMONPROGRAMFILES%\Apprise\apprise.yml +# %COMMONPROGRAMFILES%\Apprise\apprise.conf # %COMMONPROGRAMFILES%\Apprise\apprise.yaml +# The configuration files specified above can also be identified with a `.yml` +# extension or even just entirely removing the `.conf` extension altogether. + # If you loaded one of those files, your command line gets really easy: apprise -vv -t 'my title' -b 'my notification body' diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/apprise-1.7.3/apprise/__init__.py new/apprise-1.7.4/apprise/__init__.py --- old/apprise-1.7.3/apprise/__init__.py 2024-03-04 00:01:58.000000000 +0100 +++ new/apprise-1.7.4/apprise/__init__.py 2024-03-09 22:40:14.000000000 +0100 @@ -27,7 +27,7 @@ # POSSIBILITY OF SUCH DAMAGE. __title__ = 'Apprise' -__version__ = '1.7.3' +__version__ = '1.7.4' __author__ = 'Chris Caron' __license__ = 'BSD' __copywrite__ = 'Copyright (C) 2024 Chris Caron <lead2g...@gmail.com>' diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/apprise-1.7.3/apprise/cli.py new/apprise-1.7.4/apprise/cli.py --- old/apprise-1.7.3/apprise/cli.py 2024-03-04 00:01:28.000000000 +0100 +++ new/apprise-1.7.4/apprise/cli.py 2024-03-09 22:39:52.000000000 +0100 @@ -67,25 +67,30 @@ DEFAULT_CONFIG_PATHS = ( # Legacy Path Support '~/.apprise', + '~/.apprise.conf', '~/.apprise.yml', '~/.apprise.yaml', '~/.config/apprise', + '~/.config/apprise.conf', '~/.config/apprise.yml', '~/.config/apprise.yaml', # Plugin Support Extended Directory Search Paths '~/.apprise/apprise', + '~/.apprise/apprise.conf', '~/.apprise/apprise.yml', '~/.apprise/apprise.yaml', '~/.config/apprise/apprise', + '~/.config/apprise/apprise.conf', '~/.config/apprise/apprise.yml', '~/.config/apprise/apprise.yaml', - # Global Configuration Support + # Global Configuration File Support '/etc/apprise', '/etc/apprise.yml', '/etc/apprise.yaml', '/etc/apprise/apprise', + '/etc/apprise/apprise.conf', '/etc/apprise/apprise.yml', '/etc/apprise/apprise.yaml', ) @@ -104,9 +109,11 @@ # Default Config Search Path for Windows Users DEFAULT_CONFIG_PATHS = ( expandvars('%APPDATA%\\Apprise\\apprise'), + expandvars('%APPDATA%\\Apprise\\apprise.conf'), expandvars('%APPDATA%\\Apprise\\apprise.yml'), expandvars('%APPDATA%\\Apprise\\apprise.yaml'), expandvars('%LOCALAPPDATA%\\Apprise\\apprise'), + expandvars('%LOCALAPPDATA%\\Apprise\\apprise.conf'), expandvars('%LOCALAPPDATA%\\Apprise\\apprise.yml'), expandvars('%LOCALAPPDATA%\\Apprise\\apprise.yaml'), @@ -116,16 +123,19 @@ # C:\ProgramData\Apprise\ expandvars('%ALLUSERSPROFILE%\\Apprise\\apprise'), + expandvars('%ALLUSERSPROFILE%\\Apprise\\apprise.conf'), expandvars('%ALLUSERSPROFILE%\\Apprise\\apprise.yml'), expandvars('%ALLUSERSPROFILE%\\Apprise\\apprise.yaml'), # C:\Program Files\Apprise expandvars('%PROGRAMFILES%\\Apprise\\apprise'), + expandvars('%PROGRAMFILES%\\Apprise\\apprise.conf'), expandvars('%PROGRAMFILES%\\Apprise\\apprise.yml'), expandvars('%PROGRAMFILES%\\Apprise\\apprise.yaml'), # C:\Program Files\Common Files expandvars('%COMMONPROGRAMFILES%\\Apprise\\apprise'), + expandvars('%COMMONPROGRAMFILES%\\Apprise\\apprise.conf'), expandvars('%COMMONPROGRAMFILES%\\Apprise\\apprise.yml'), expandvars('%COMMONPROGRAMFILES%\\Apprise\\apprise.yaml'), ) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/apprise-1.7.3/apprise/manager.py new/apprise-1.7.4/apprise/manager.py --- old/apprise-1.7.3/apprise/manager.py 2024-03-03 20:41:00.000000000 +0100 +++ new/apprise-1.7.4/apprise/manager.py 2024-03-09 22:39:49.000000000 +0100 @@ -365,67 +365,66 @@ # end of _import_module() return - with self._lock: - for _path in paths: - path = os.path.abspath(os.path.expanduser(_path)) - if (cache and path in self._paths_previously_scanned) \ - or not os.path.exists(path): - # We're done as we've already scanned this - continue - - # Store our path as a way of hashing it has been handled - self._paths_previously_scanned.add(path) - - if os.path.isdir(path) and not \ - os.path.isfile(os.path.join(path, '__init__.py')): + for _path in paths: + path = os.path.abspath(os.path.expanduser(_path)) + if (cache and path in self._paths_previously_scanned) \ + or not os.path.exists(path): + # We're done as we've already scanned this + continue + + # Store our path as a way of hashing it has been handled + self._paths_previously_scanned.add(path) + + if os.path.isdir(path) and not \ + os.path.isfile(os.path.join(path, '__init__.py')): + + logger.debug('Scanning for custom plugins in: %s', path) + for entry in os.listdir(path): + re_match = module_re.match(entry) + if not re_match: + # keep going + logger.trace('Plugin Scan: Ignoring %s', entry) + continue - logger.debug('Scanning for custom plugins in: %s', path) - for entry in os.listdir(path): - re_match = module_re.match(entry) - if not re_match: - # keep going - logger.trace('Plugin Scan: Ignoring %s', entry) + new_path = os.path.join(path, entry) + if os.path.isdir(new_path): + # Update our path + new_path = os.path.join(path, entry, '__init__.py') + if not os.path.isfile(new_path): + logger.trace( + 'Plugin Scan: Ignoring %s', + os.path.join(path, entry)) continue - new_path = os.path.join(path, entry) - if os.path.isdir(new_path): - # Update our path - new_path = os.path.join(path, entry, '__init__.py') - if not os.path.isfile(new_path): - logger.trace( - 'Plugin Scan: Ignoring %s', - os.path.join(path, entry)) - continue - - if not cache or \ - (cache and new_path not in - self._paths_previously_scanned): - # Load our module - _import_module(new_path) - - # Add our subdir path - self._paths_previously_scanned.add(new_path) - else: - if os.path.isdir(path): - # This logic is safe to apply because we already - # validated the directories state above; update our - # path - path = os.path.join(path, '__init__.py') - if cache and path in self._paths_previously_scanned: - continue + if not cache or \ + (cache and new_path not in + self._paths_previously_scanned): + # Load our module + _import_module(new_path) + + # Add our subdir path + self._paths_previously_scanned.add(new_path) + else: + if os.path.isdir(path): + # This logic is safe to apply because we already + # validated the directories state above; update our + # path + path = os.path.join(path, '__init__.py') + if cache and path in self._paths_previously_scanned: + continue - self._paths_previously_scanned.add(path) + self._paths_previously_scanned.add(path) - # directly load as is - re_match = module_re.match(os.path.basename(path)) - # must be a match and must have a .py extension - if not re_match or not re_match.group(1): - # keep going - logger.trace('Plugin Scan: Ignoring %s', path) - continue + # directly load as is + re_match = module_re.match(os.path.basename(path)) + # must be a match and must have a .py extension + if not re_match or not re_match.group(1): + # keep going + logger.trace('Plugin Scan: Ignoring %s', path) + continue - # Load our module - _import_module(path) + # Load our module + _import_module(path) return None diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/apprise-1.7.3/apprise/plugins/NotifyLunaSea.py new/apprise-1.7.4/apprise/plugins/NotifyLunaSea.py --- old/apprise-1.7.3/apprise/plugins/NotifyLunaSea.py 1970-01-01 01:00:00.000000000 +0100 +++ new/apprise-1.7.4/apprise/plugins/NotifyLunaSea.py 2024-03-09 22:39:52.000000000 +0100 @@ -0,0 +1,440 @@ +# -*- coding: utf-8 -*- +# BSD 2-Clause License +# +# Apprise - Push Notification Library. +# Copyright (c) 2024, Chris Caron <lead2g...@gmail.com> +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# +# 2. Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. +# +# API: +# https://docs.lunasea.app/lunasea/notifications/custom-notifications +# +import re +import requests +from json import dumps + +from .NotifyBase import NotifyBase +from ..common import NotifyType +from ..common import NotifyImageSize +from ..utils import parse_list +from ..utils import is_hostname +from ..utils import is_ipaddr +from ..utils import parse_bool +from ..AppriseLocale import gettext_lazy as _ +from ..URLBase import PrivacyMode + + +class LunaSeaMode: + """ + Define LunaSea Notification Modes + """ + # App posts upstream to the developer API on LunaSea's website + CLOUD = "cloud" + + # Running a dedicated private ntfy Server + PRIVATE = "private" + + +LUNASEA_MODES = ( + LunaSeaMode.CLOUD, + LunaSeaMode.PRIVATE, +) + + +class NotifyLunaSea(NotifyBase): + """ + A wrapper for LunaSea Notifications + """ + + # The default descriptive name associated with the Notification + service_name = 'LunaSea' + + # The services URL + service_url = 'https://luasea.app' + + # The default insecure protocol + protocol = ('lunasea', 'lsea') + + # The default secure protocol + secure_protocol = ('lunaseas', 'lseas') + + # A URL that takes you to the setup/help of the specific protocol + setup_url = 'https://github.com/caronc/apprise/wiki/Notify_lunasea' + + # Allows the user to specify the NotifyImageSize object + image_size = NotifyImageSize.XY_256 + + # LunaSea Notification Details + cloud_notify_url = 'https://notify.lunasea.app' + notify_user_path = '/v1/custom/user/{}' + notify_device_path = '/v1/custom/device/{}' + + # if our hostname matches the following we automatically enforce + # cloud mode + __auto_cloud_host = re.compile(r'(notify\.)?lunasea\.app', re.IGNORECASE) + + # Define object templates + templates = ( + '{schema}://{targets}', + '{schema}://{host}/{targets}', + '{schema}://{host}:{port}/{targets}', + '{schema}://{user}@{host}/{targets}', + '{schema}://{user}@{host}:{port}/{targets}', + '{schema}://{user}:{password}@{host}/{targets}', + '{schema}://{user}:{password}@{host}:{port}/{targets}', + ) + + # Define our template tokens + template_tokens = dict(NotifyBase.template_tokens, **{ + 'host': { + 'name': _('Hostname'), + 'type': 'string', + }, + 'port': { + 'name': _('Port'), + 'type': 'int', + 'min': 1, + 'max': 65535, + }, + 'user': { + 'name': _('Username'), + 'type': 'string', + }, + 'password': { + 'name': _('Password'), + 'type': 'string', + 'private': True, + }, + 'token': { + 'name': _('Token'), + 'type': 'string', + 'private': True, + }, + 'target_user': { + 'name': _('Target User'), + 'type': 'string', + 'prefix': '@', + 'map_to': 'targets', + }, + 'target_device': { + 'name': _('Target Device'), + 'type': 'string', + 'prefix': '+', + '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', + }, + 'image': { + 'name': _('Include Image'), + 'type': 'bool', + 'default': False, + 'map_to': 'include_image', + }, + 'mode': { + 'name': _('Mode'), + 'type': 'choice:string', + 'values': LUNASEA_MODES, + 'default': LunaSeaMode.PRIVATE, + }, + }) + + def __init__(self, targets=None, mode=None, token=None, + include_image=False, **kwargs): + """ + Initialize LunaSea Object + """ + super().__init__(**kwargs) + + # Show image associated with notification + self.include_image = \ + self.template_args['image']['default'] \ + if include_image is None else include_image + + # Prepare our mode + self.mode = mode.strip().lower() \ + if isinstance(mode, str) \ + else self.template_args['mode']['default'] + + if self.mode not in LUNASEA_MODES: + msg = 'An invalid LunaSea mode ({}) was specified.'.format(mode) + self.logger.warning(msg) + raise TypeError(msg) + + self.targets = [] + for target in parse_list(targets): + if len(target) < 4: + self.logger.warning( + 'A specified target ({}) is invalid and will be ' + 'ignored'.format(target)) + continue + + if target[0] == '+': + # Device + self.targets.append(('+', target[1:])) + + elif target[0] == '@': + # User + self.targets.append(('@', target[1:])) + + else: + # User + self.targets.append(('@', target)) + + return + + def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs): + """ + Perform LunaSea Notification + """ + + # error tracking (used for function return) + has_error = False + + if not len(self.targets): + # We have nothing to notify; we're done + self.logger.warning('There are no LunaSea targets to notify') + return False + + # Prepare our headers + headers = { + 'User-Agent': self.app_id, + 'Content-Type': 'application/json', + 'Accept': 'application/json', + } + + # prepare payload + payload = { + 'title': title if title else self.app_desc, + 'body': body, + } + + # Acquire image_url + image_url = None if not self.include_image \ + else self.image_url(notify_type) + + if image_url: + payload['image'] = image_url + + # Prepare our Authentication (if defined) + if self.user and self.password: + auth = (self.user, self.password) + + else: + # No Auth + auth = None + + if self.mode == LunaSeaMode.CLOUD: + # Cloud Service + notify_url = self.cloud_notify_url + + else: + # Local Hosting + schema = 'https' if self.secure else 'http' + + notify_url = '%s://%s' % (schema, self.host) + if isinstance(self.port, int): + notify_url += ':%d' % self.port + + # Create a copy of the targets list + targets = list(self.targets) + while len(targets): + target = targets.pop(0) + + if target[0] == '+': + url = notify_url + self.notify_device_path.format(target[1]) + + else: + url = notify_url + self.notify_user_path.format(target[1]) + + self.logger.debug('LunaSea POST URL: %s (cert_verify=%r)' % ( + url, self.verify_certificate, + )) + self.logger.debug('LunaSea 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, + auth=auth, + verify=self.verify_certificate, + timeout=self.request_timeout, + ) + + if r.status_code not in ( + requests.codes.ok, requests.codes.no_content): + # We had a problem + status_str = \ + NotifyLunaSea.http_response_code_lookup(r.status_code) + + self.logger.warning( + 'Failed to deliver payload to LunaSea:' + '{}{}error={}.'.format( + status_str, + ', ' if status_str else '', + r.status_code)) + + self.logger.debug( + 'Response Details:\r\n{}'.format(r.content)) + + has_error = True + + # otherwise we were successful + continue + + except requests.RequestException as e: + self.logger.warning( + 'A Connection error occurred communicating with LunaSea.') + self.logger.debug('Socket Exception: %s' % str(e)) + + has_error = True + + return not has_error + + def url(self, privacy=False, *args, **kwargs): + """ + Returns the URL built dynamically based on specified arguments. + """ + + params = { + 'mode': self.mode, + 'image': 'yes' if self.include_image else 'no', + } + + # Our URL parameters + params.update(self.url_parameters(privacy=privacy, *args, **kwargs)) + + auth = '' + if self.user and self.password: + auth = '{user}:{password}@'.format( + user=NotifyLunaSea.quote(self.user, safe=''), + password=self.pprint( + self.password, privacy, mode=PrivacyMode.Secret, + safe=''), + ) + elif self.user: + auth = '{user}@'.format( + user=NotifyLunaSea.quote(self.user, safe=''), + ) + + if self.mode == LunaSeaMode.PRIVATE: + default_port = 443 if self.secure else 80 + return '{schema}://{auth}{host}{port}/{targets}?{params}'.format( + schema=self.secure_protocol[0] + if self.secure else self.protocol[0], + auth=auth, + host=self.host, + port='' if self.port is None or self.port == default_port + else ':{}'.format(self.port), + targets='/'.join( + [NotifyLunaSea.quote(x[0] + x[1], safe='@+') + for x in self.targets]), + params=NotifyLunaSea.urlencode(params) + ) + + else: # Cloud mode + return '{schema}://{auth}{targets}?{params}'.format( + schema=self.protocol[0], + auth=auth, + targets='/'.join( + [NotifyLunaSea.quote(x[0] + x[1], safe='@+') + for x in self.targets]), + params=NotifyLunaSea.urlencode(params) + ) + + def __len__(self): + """ + Returns the number of targets associated with this notification + """ + # always return 1 + return 1 if not self.targets else 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 + + # Fetch our targets + results['targets'] = NotifyLunaSea.split_path(results['fullpath']) + + # Boolean to include an image or not + results['include_image'] = parse_bool(results['qsd'].get( + 'image', NotifyLunaSea.template_args['image']['default'])) + + # The 'to' makes it easier to use yaml configuration + if 'to' in results['qsd'] and len(results['qsd']['to']): + results['targets'] += \ + NotifyLunaSea.parse_list(results['qsd']['to']) + + # Mode override + if 'mode' in results['qsd'] and results['qsd']['mode']: + results['mode'] = NotifyLunaSea.unquote( + results['qsd']['mode'].strip().lower()) + + else: + # We can try to detect the mode based on the validity of the + # hostname. + # + # This isn't a surfire way to do things though; it's best to + # specify the mode= flag + results['mode'] = LunaSeaMode.PRIVATE \ + if ((is_hostname(results['host']) + or is_ipaddr(results['host'])) and results['targets']) \ + else LunaSeaMode.CLOUD + + if results['mode'] == LunaSeaMode.CLOUD: + # Store first entry as it can be a topic too in this case + # But only if we also rule it out not being the words + # lunasea.app itself, something that starts wiht an non-alpha + # numeric character: + if not NotifyLunaSea.__auto_cloud_host.search(results['host']): + # Add it to the front of the list for consistency + results['targets'].insert(0, results['host']) + + elif results['mode'] == LunaSeaMode.PRIVATE and \ + not (is_hostname(results['host'] or + is_ipaddr(results['host']))): + # Invalid Host for LunaSeaMode.PRIVATE + return None + + return results diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/apprise-1.7.3/apprise.egg-info/PKG-INFO new/apprise-1.7.4/apprise.egg-info/PKG-INFO --- old/apprise-1.7.3/apprise.egg-info/PKG-INFO 2024-03-04 00:04:18.000000000 +0100 +++ new/apprise-1.7.4/apprise.egg-info/PKG-INFO 2024-03-09 22:41:33.000000000 +0100 @@ -1,12 +1,12 @@ Metadata-Version: 2.1 Name: apprise -Version: 1.7.3 +Version: 1.7.4 Summary: Push Notifications that work with just about every platform! Home-page: https://github.com/caronc/apprise Author: Chris Caron Author-email: lead2g...@gmail.com License: BSD -Keywords: Alerts Apprise API Automated Packet Reporting System AWS Boxcar BulkSMS BulkVS Burst SMS Chat CLI ClickSend D7Networks Dapnet DBus DingTalk Discord Email Emby Enigma2 Faast FCM Flock Form Gnome Google Chat Gotify Growl Guilded Home Assistant httpSMS IFTTT Join JSON Kavenegar KODI Kumulos LaMetric Line MacOSX Mailgun Mastodon Matrix Mattermost MessageBird Microsoft Misskey MQTT MSG91 MSTeams Nextcloud NextcloudTalk Notica Notifiarr Notifico Ntfy Office365 OneSignal Opsgenie PagerDuty PagerTree ParsePlatform PopcornNotify Prowl PushBullet Pushed Pushjet PushMe Push Notifications Pushover PushSafer Pushy PushDeer Reddit Revolt Rocket.Chat RSyslog Ryver SendGrid ServerChan SES Signal SimplePush Sinch Slack SMSEagle SMS Manager SMTP2Go SNS SparkPost Streamlabs Stride Synology Chat Syslog Techulus Telegram Threema Gateway Twilio Twist Twitter Voipms Vonage Webex WeCom Bot WhatsApp Windows XBMC XML Zulip +Keywords: Alerts Apprise API Automated Packet Reporting System AWS Boxcar BulkSMS BulkVS Burst SMS Chat CLI ClickSend D7Networks Dapnet DBus DingTalk Discord Email Emby Enigma2 Faast FCM Flock Form Gnome Google Chat Gotify Growl Guilded Home Assistant httpSMS IFTTT Join JSON Kavenegar KODI Kumulos LaMetric Line LunaSea MacOSX Mailgun Mastodon Matrix Mattermost MessageBird Microsoft Misskey MQTT MSG91 MSTeams Nextcloud NextcloudTalk Notica Notifiarr Notifico Ntfy Office365 OneSignal Opsgenie PagerDuty PagerTree ParsePlatform PopcornNotify Prowl PushBullet Pushed Pushjet PushMe Push Notifications Pushover PushSafer Pushy PushDeer Reddit Revolt Rocket.Chat RSyslog Ryver SendGrid ServerChan SES Signal SimplePush Sinch Slack SMSEagle SMS Manager SMTP2Go SNS SparkPost Streamlabs Stride Synology Chat Syslog Techulus Telegram Threema Gateway Twilio Twist Twitter Voipms Vonage Webex WeCom Bot WhatsApp Windows XBMC XML Zulip Classifier: Development Status :: 5 - Production/Stable Classifier: Intended Audience :: Developers Classifier: Intended Audience :: System Administrators @@ -108,6 +108,7 @@ | [Kumulos](https://github.com/caronc/apprise/wiki/Notify_kumulos) | kumulos:// | (TCP) 443 | kumulos://apikey/serverkey | [LaMetric Time](https://github.com/caronc/apprise/wiki/Notify_lametric) | lametric:// | (TCP) 443 | lametric://apikey@device_ipaddr<br/>lametric://apikey@hostname:port<br/>lametric://client_id@client_secret | [Line](https://github.com/caronc/apprise/wiki/Notify_line) | line:// | (TCP) 443 | line://Token@User<br/>line://Token/User1/User2/UserN +| [LunaSea](https://github.com/caronc/apprise/wiki/Notify_lunasea) | lunasea:// | (TCP) 80 or 443 | lunasea://user:pass@+FireBaseDevice/<br/>lunasea://user:pass@FireBaseUser/<br/>lunasea://user:pass@hostname/+FireBaseDevice/<br/>lunasea://user:pass@hostname/@FireBaseUser/ | [Mailgun](https://github.com/caronc/apprise/wiki/Notify_mailgun) | mailgun:// | (TCP) 443 | mailgun://user@hostname/apikey<br />mailgun://user@hostname/apikey/email<br />mailgun://user@hostname/apikey/email1/email2/emailN<br />mailgun://user@hostname/apikey/?name="From%20User" | [Mastodon](https://github.com/caronc/apprise/wiki/Notify_mastodon) | mastodon:// or mastodons://| (TCP) 80 or 443 | mastodon://access_key@hostname<br />mastodon://access_key@hostname/@user<br />mastodon://access_key@hostname/@user1/@user2/@userN | [Matrix](https://github.com/caronc/apprise/wiki/Notify_matrix) | matrix:// or matrixs:// | (TCP) 80 or 443 | matrix://hostname<br />matrix://user@hostname<br />matrixs://user:pass@hostname:port/#room_alias<br />matrixs://user:pass@hostname:port/!room_id<br />matrixs://user:pass@hostname:port/#room_alias/!room_id/#room2<br />matrixs://token@hostname:port/?webhook=matrix<br />matrix://user:token@hostname/?webhook=slack&format=markdown @@ -263,43 +264,35 @@ # By default if no url or configuration is specified apprise will attempt to load # configuration files (if present) from: # ~/.apprise -# ~/.apprise.yml # ~/.apprise.yaml -# ~/.config/apprise -# ~/.config/apprise.yml +# ~/.config/apprise.conf # ~/.config/apprise.yaml -# /etc/apprise -# /etc/apprise.yml +# /etc/apprise.conf # /etc/apprise.yaml # Also a subdirectory handling allows you to leverage plugins # ~/.apprise/apprise -# ~/.apprise/apprise.yml # ~/.apprise/apprise.yaml -# ~/.config/apprise/apprise -# ~/.config/apprise/apprise.yml +# ~/.config/apprise/apprise.conf # ~/.config/apprise/apprise.yaml -# /etc/apprise/apprise -# /etc/apprise/apprise.yml # /etc/apprise/apprise.yaml +# /etc/apprise/apprise.conf # Windows users can store their default configuration files here: -# %APPDATA%/Apprise/apprise -# %APPDATA%/Apprise/apprise.yml +# %APPDATA%/Apprise/apprise.conf # %APPDATA%/Apprise/apprise.yaml -# %LOCALAPPDATA%/Apprise/apprise -# %LOCALAPPDATA%/Apprise/apprise.yml +# %LOCALAPPDATA%/Apprise/apprise.conf # %LOCALAPPDATA%/Apprise/apprise.yaml -# %ALLUSERSPROFILE%\Apprise\apprise -# %ALLUSERSPROFILE%\Apprise\apprise.yml +# %ALLUSERSPROFILE%\Apprise\apprise.conf # %ALLUSERSPROFILE%\Apprise\apprise.yaml -# %PROGRAMFILES%\Apprise\apprise -# %PROGRAMFILES%\Apprise\apprise.yml +# %PROGRAMFILES%\Apprise\apprise.conf # %PROGRAMFILES%\Apprise\apprise.yaml -# %COMMONPROGRAMFILES%\Apprise\apprise -# %COMMONPROGRAMFILES%\Apprise\apprise.yml +# %COMMONPROGRAMFILES%\Apprise\apprise.conf # %COMMONPROGRAMFILES%\Apprise\apprise.yaml +# The configuration files specified above can also be identified with a `.yml` +# extension or even just entirely removing the `.conf` extension altogether. + # If you loaded one of those files, your command line gets really easy: apprise -vv -t 'my title' -b 'my notification body' diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/apprise-1.7.3/apprise.egg-info/SOURCES.txt new/apprise-1.7.4/apprise.egg-info/SOURCES.txt --- old/apprise-1.7.3/apprise.egg-info/SOURCES.txt 2024-03-04 00:04:18.000000000 +0100 +++ new/apprise-1.7.4/apprise.egg-info/SOURCES.txt 2024-03-09 22:41:33.000000000 +0100 @@ -111,6 +111,7 @@ apprise/plugins/NotifyKumulos.py apprise/plugins/NotifyLametric.py apprise/plugins/NotifyLine.py +apprise/plugins/NotifyLunaSea.py apprise/plugins/NotifyMQTT.py apprise/plugins/NotifyMSG91.py apprise/plugins/NotifyMSTeams.py @@ -250,6 +251,7 @@ test/test_plugin_kumulos.py test/test_plugin_lametric.py test/test_plugin_line.py +test/test_plugin_lunasea.py test/test_plugin_macosx.py test/test_plugin_mailgun.py test/test_plugin_mastodon.py diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/apprise-1.7.3/packaging/man/apprise.1 new/apprise-1.7.4/packaging/man/apprise.1 --- old/apprise-1.7.3/packaging/man/apprise.1 2024-03-04 00:03:50.000000000 +0100 +++ new/apprise-1.7.4/packaging/man/apprise.1 2024-03-09 22:41:08.000000000 +0100 @@ -233,25 +233,19 @@ . .nf -~/\.apprise -~/\.apprise\.yml +~/\.apprise\.conf ~/\.apprise\.yaml -~/\.config/apprise -~/\.config/apprise\.yml +~/\.config/apprise\.conf ~/\.config/apprise\.yaml -~/\.apprise/apprise -~/\.apprise/apprise\.yml +~/\.apprise/apprise\.conf ~/\.apprise/apprise\.yaml -~/\.config/apprise/apprise -~/\.config/apprise/apprise\.yml +~/\.config/apprise/apprise\.conf ~/\.config/apprise/apprise\.yaml -/etc/apprise -/etc/apprise\.yml +/etc/apprise\.conf /etc/apprise\.yaml -/etc/apprise/apprise -/etc/apprise/apprise\.yml +/etc/apprise/apprise\.conf /etc/apprise/apprise\.yaml . .fi @@ -259,6 +253,9 @@ .IP "" 0 . .P +The \fBconfiguration files\fR specified above can also be identified with a \fB\.yml\fR extension or even just entirely removing the \fB\.conf\fR extension altogether\. +. +.P If a default configuration file is referenced in any way by the \fBapprise\fR tool, you no longer need to provide it a Service URL\. Usage of the \fBapprise\fR tool simplifies to: . .IP "" 4 diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/apprise-1.7.3/packaging/man/apprise.1.html new/apprise-1.7.4/packaging/man/apprise.1.html --- old/apprise-1.7.3/packaging/man/apprise.1.html 2024-03-04 00:03:50.000000000 +0100 +++ new/apprise-1.7.4/packaging/man/apprise.1.html 2024-03-09 22:41:09.000000000 +0100 @@ -276,28 +276,25 @@ a cloud location (by referencing <code>http://</code> or <code>https://</code>. By default <strong>apprise</strong> looks in the following local locations for configuration files and loads them:</p> -<pre><code>~/.apprise -~/.apprise.yml +<pre><code>~/.apprise.conf ~/.apprise.yaml -~/.config/apprise -~/.config/apprise.yml +~/.config/apprise.conf ~/.config/apprise.yaml -~/.apprise/apprise -~/.apprise/apprise.yml +~/.apprise/apprise.conf ~/.apprise/apprise.yaml -~/.config/apprise/apprise -~/.config/apprise/apprise.yml +~/.config/apprise/apprise.conf ~/.config/apprise/apprise.yaml -/etc/apprise -/etc/apprise.yml +/etc/apprise.conf /etc/apprise.yaml -/etc/apprise/apprise -/etc/apprise/apprise.yml +/etc/apprise/apprise.conf /etc/apprise/apprise.yaml </code></pre> +<p>The <strong>configuration files</strong> specified above can also be identified with a <code>.yml</code> +extension or even just entirely removing the <code>.conf</code> extension altogether.</p> + <p>If a default configuration file is referenced in any way by the <strong>apprise</strong> tool, you no longer need to provide it a Service URL. Usage of the <strong>apprise</strong> tool simplifies to:</p> @@ -319,7 +316,7 @@ <h2 id="COPYRIGHT">COPYRIGHT</h2> -<p>Apprise is Copyright (C) 2024 Chris Caron <a href="mailto:lead2gold@gmail.com" data-bare-link="true">lead2gold@gmail.com</a></p> +<p>Apprise is Copyright (C) 2024 Chris Caron <a href="mailto:lead2gold@gmail.com" data-bare-link="true">lead2gold@gmail.com</a></p> <ol class='man-decor man-foot man foot'> diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/apprise-1.7.3/packaging/man/apprise.md new/apprise-1.7.4/packaging/man/apprise.md --- old/apprise-1.7.3/packaging/man/apprise.md 2024-03-04 00:01:28.000000000 +0100 +++ new/apprise-1.7.4/packaging/man/apprise.md 2024-03-09 22:39:52.000000000 +0100 @@ -188,27 +188,24 @@ a cloud location (by referencing `http://` or `https://`. By default **apprise** looks in the following local locations for configuration files and loads them: - ~/.apprise - ~/.apprise.yml + ~/.apprise.conf ~/.apprise.yaml - ~/.config/apprise - ~/.config/apprise.yml + ~/.config/apprise.conf ~/.config/apprise.yaml - ~/.apprise/apprise - ~/.apprise/apprise.yml + ~/.apprise/apprise.conf ~/.apprise/apprise.yaml - ~/.config/apprise/apprise - ~/.config/apprise/apprise.yml + ~/.config/apprise/apprise.conf ~/.config/apprise/apprise.yaml - /etc/apprise - /etc/apprise.yml + /etc/apprise.conf /etc/apprise.yaml - /etc/apprise/apprise - /etc/apprise/apprise.yml + /etc/apprise/apprise.conf /etc/apprise/apprise.yaml +The **configuration files** specified above can also be identified with a `.yml` +extension or even just entirely removing the `.conf` extension altogether. + If a default configuration file is referenced in any way by the **apprise** tool, you no longer need to provide it a Service URL. Usage of the **apprise** tool simplifies to: diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/apprise-1.7.3/packaging/redhat/python-apprise.spec new/apprise-1.7.4/packaging/redhat/python-apprise.spec --- old/apprise-1.7.3/packaging/redhat/python-apprise.spec 2024-03-04 00:03:25.000000000 +0100 +++ new/apprise-1.7.4/packaging/redhat/python-apprise.spec 2024-03-09 22:40:47.000000000 +0100 @@ -42,7 +42,7 @@ Apprise API, APRS, AWS SES, AWS SNS, Bark, Boxcar, Burst SMS, BulkSMS, BulkVS, ClickSend, DAPNET, DingTalk, Discord, E-Mail, Emby, Faast, FCM, Flock, Google Chat, Gotify, Growl, Guilded, Home Assistant, httpSMS, IFTTT, Join, -Kavenegar, KODI, Kumulos, LaMetric, Line, MacOSX, Mailgun, Mastodon, +Kavenegar, KODI, Kumulos, LaMetric, Line, LunaSea, MacOSX, Mailgun, Mastodon, Mattermost,Matrix, MessageBird, Microsoft Windows, Microsoft Teams, Misskey, MQTT, MSG91, MyAndroid, Nexmo, Nextcloud, NextcloudTalk, Notica, Notifiarr, Notifico, ntfy, Office365, OneSignal, Opsgenie, PagerDuty, PagerTree, @@ -54,7 +54,7 @@ Voipms, Vonage, WeCom Bot, WhatsApp, Webex Teams} Name: python-%{pypi_name} -Version: 1.7.3 +Version: 1.7.4 Release: 1%{?dist} Summary: A simple wrapper to many popular notification services used today License: BSD @@ -195,6 +195,9 @@ %{python3_sitelib}/%{pypi_name}/cli.* %changelog +* Sat Mar 9 2024 Chris Caron <lead2g...@gmail.com> - 1.7.4 +- Updated to v1.7.4 + * Sun Mar 3 2024 Chris Caron <lead2g...@gmail.com> - 1.7.3 - Updated to v1.7.3 diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/apprise-1.7.3/setup.py new/apprise-1.7.4/setup.py --- old/apprise-1.7.3/setup.py 2024-03-04 00:01:47.000000000 +0100 +++ new/apprise-1.7.4/setup.py 2024-03-09 22:40:06.000000000 +0100 @@ -62,7 +62,7 @@ setup( name='apprise', - version='1.7.3', + version='1.7.4', description='Push Notifications that work with just about every platform!', license='BSD', long_description=open('README.md', encoding="utf-8").read(), diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/apprise-1.7.3/test/test_apprise_cli.py new/apprise-1.7.4/test/test_apprise_cli.py --- old/apprise-1.7.3/test/test_apprise_cli.py 2024-01-27 21:33:15.000000000 +0100 +++ new/apprise-1.7.4/test/test_apprise_cli.py 2024-03-09 22:39:49.000000000 +0100 @@ -615,6 +615,68 @@ assert result.exit_code == 0 +def test_apprise_cli_modules(tmpdir): + """ + CLI: --plugin (-P) + + """ + + runner = CliRunner() + + # + # Loading of modules works correctly + # + notify_cmod_base = tmpdir.mkdir('cli_modules') + notify_cmod = notify_cmod_base.join('hook.py') + notify_cmod.write(cleandoc(""" + from apprise.decorators import notify + + @notify(on="climod") + def mywrapper(body, title, notify_type, *args, **kwargs): + pass + """)) + + result = runner.invoke(cli.main, [ + '--plugin-path', str(notify_cmod), + '-t', 'title', + '-b', 'body', + 'climod://', + ]) + + assert result.exit_code == 0 + + # Test -P + result = runner.invoke(cli.main, [ + '-P', str(notify_cmod), + '-t', 'title', + '-b', 'body', + 'climod://', + ]) + + assert result.exit_code == 0 + + # Test double hooks + notify_cmod2 = notify_cmod_base.join('hook2.py') + notify_cmod2.write(cleandoc(""" + from apprise.decorators import notify + + @notify(on="climod2") + def mywrapper(body, title, notify_type, *args, **kwargs): + pass + """)) + + result = runner.invoke(cli.main, [ + '--plugin-path', str(notify_cmod), + '--plugin-path', str(notify_cmod2), + '-t', 'title', + '-b', 'body', + 'climod://', + 'climod2://', + ]) + + assert result.exit_code == 0 + + def test_apprise_cli_details(tmpdir): """ CLI: --details (-l) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/apprise-1.7.3/test/test_plugin_lunasea.py new/apprise-1.7.4/test/test_plugin_lunasea.py --- old/apprise-1.7.3/test/test_plugin_lunasea.py 1970-01-01 01:00:00.000000000 +0100 +++ new/apprise-1.7.4/test/test_plugin_lunasea.py 2024-03-09 22:39:52.000000000 +0100 @@ -0,0 +1,280 @@ +# -*- coding: utf-8 -*- +# BSD 2-Clause License +# +# Apprise - Push Notification Library. +# Copyright (c) 2024, Chris Caron <lead2g...@gmail.com> +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# +# 2. Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. + +import os +from unittest import mock +from json import loads + +import requests +from helpers import AppriseURLTester + +from apprise.plugins.NotifyLunaSea import NotifyLunaSea + +# Disable logging for a cleaner testing output +import logging +logging.disable(logging.CRITICAL) + +# Attachment Directory +TEST_VAR_DIR = os.path.join(os.path.dirname(__file__), 'var') + +# Our Testing URLs +apprise_url_tests = ( + ('lunasea://', { + # Initializes okay (as cloud mode) but has no targets to notify + 'instance': NotifyLunaSea, + # invalid targets specified (nothing to notify) + # as a result the response type will be false + 'response': False, + }), + ('lunaseas://44$$$$%3012/?mode=private', { + # Private mode initialization with a horrible hostname + 'instance': None + }), + ('lunasea://:@/', { + # Initializes okay (as cloud mode) but has no targets to notify + 'instance': NotifyLunaSea, + # invalid targets specified (nothing to notify) + # as a result the response type will be false + 'response': False, + }), + # No targets + ('lunasea://user:pass@localhost?mode=private', { + 'instance': NotifyLunaSea, + # invalid targets specified (nothing to notify) + # as a result the response type will be false + 'response': False, + }), + # No valid targets + ('lunasea://user:pass@localhost/#/!/@', { + 'instance': NotifyLunaSea, + # invalid targets specified (nothing to notify) + # as a result the response type will be false + 'response': False, + }), + # user/pass combos + ('lunasea://user@localhost/@user/', { + 'instance': NotifyLunaSea, + # Our expected url(privacy=True) startswith() response: + 'privacy_url': 'lunasea://user@localhost/@user', + }), + # LunaSea cloud mode (enforced) + ('lunasea://lunasea.app/@user/+device/', { + 'instance': NotifyLunaSea, + }), + # No user/pass combo + ('lunasea://localhost/@user/@user2/?image=True', { + 'instance': NotifyLunaSea, + }), + # Enforce image but not otherwise find one + ('lunasea://localhost/+device/?image=True', { + 'instance': NotifyLunaSea, + 'include_image': False, + }), + # No images + ('lunasea://localhost/+device/?image=False', { + 'instance': NotifyLunaSea, + }), + ('lunaseas://user:pass@localhost?to=+device', { + 'instance': NotifyLunaSea, + # The response text is expected to be the following on a success + }), + ('https://just/a/random/host/that/means/nothing', { + # Nothing transpires from this + 'instance': None + }), + # Several targets + ('lunasea://user:pass@+device/user/@user2/?mode=cloud', { + 'instance': NotifyLunaSea, + # The response text is expected to be the following on a success + }), + # Several targets (but do not add lunasea.app) + ('lunasea://user:p...@lunasea.app/user1/user2/?mode=cloud', { + 'instance': NotifyLunaSea, + # The response text is expected to be the following on a success + }), + ('lunaseas://user:web/token@localhost/user/?mode=invalid', { + # Invalid mode + 'instance': TypeError, + }), + ('lunasea://user:pass@localhost:8089/+device/user1', { + 'instance': NotifyLunaSea, + # force a failure using basic mode + 'response': False, + 'requests_response_code': requests.codes.internal_server_error, + }), + ('lunasea://user:pass@localhost:8082/+device', { + 'instance': NotifyLunaSea, + # throw a bizzare code forcing us to fail to look it up + 'response': False, + 'requests_response_code': 999, + }), + ('lunasea://user:pass@localhost:8083/user1/user2/', { + 'instance': NotifyLunaSea, + # Throws a series of connection and transfer exceptions when this flag + # is set and tests that we gracfully handle them + 'test_requests_exceptions': True, + }), +) + + +def test_plugin_lunasea_urls(): + """ + NotifyLunaSea() Apprise URLs + + """ + + # Run our general tests + AppriseURLTester(tests=apprise_url_tests).run_all() + + +@mock.patch('requests.post') +def test_plugin_custom_lunasea_edge_cases(mock_post): + """ + NotifyLunaSea() Edge Cases + + """ + + # Prepare our response + response = requests.Request() + response.status_code = requests.codes.ok + response.content = '' + + # Prepare Mock + mock_post.return_value = response + + # Prepare a URL with some garbage in it that gets parsed out anyway + # key take away is we provided userA and device1 + results = NotifyLunaSea.parse_url('lsea://user:pass@@userA,+device1,~~,,') + + assert isinstance(results, dict) + assert results['user'] == 'user' + assert results['password'] == 'pass' + assert results['port'] is None + assert results['host'] == 'userA,+device1,~~,,' + assert results['fullpath'] is None + assert results['path'] is None + assert results['query'] is None + assert results['schema'] == 'lsea' + assert results['url'] == 'lsea://user:pass@userA,+device1,~~,,' + assert isinstance(results['qsd:'], dict) is True + + instance = NotifyLunaSea(**results) + assert isinstance(instance, NotifyLunaSea) + assert len(instance.targets) == 2 + assert ('@', 'userA') in instance.targets + assert ('+', 'device1') in instance.targets + + assert instance.notify("test") is True + + # 1 call to user, and second to device + assert mock_post.call_count == 2 + + url = mock_post.call_args_list[0][0][0] + assert url == 'https://notify.lunasea.app/v1/custom/device/device1' + payload = loads(mock_post.call_args_list[0][1]['data']) + assert 'title' in payload + assert 'body' in payload + assert 'image' not in payload + assert payload['body'] == 'test' + assert payload['title'] == 'Apprise Notifications' + + url = mock_post.call_args_list[1][0][0] + assert url == 'https://notify.lunasea.app/v1/custom/user/userA' + payload = loads(mock_post.call_args_list[1][1]['data']) + assert 'title' in payload + assert 'body' in payload + assert 'image' not in payload + assert payload['body'] == 'test' + assert payload['title'] == 'Apprise Notifications' + + assert '@userA' in instance.url() + assert '+device1' in instance.url() + + # Test using a locally hosted instance now: + mock_post.reset_mock() + + results = NotifyLunaSea.parse_url( + 'lseas://user:pass@myhost:3222/@userA,+device1,~~,,') + + assert isinstance(results, dict) + assert results['user'] == 'user' + assert results['password'] == 'pass' + assert results['port'] == 3222 + assert results['host'] == 'myhost' + assert ( + results['fullpath'] == '/%40userA%2C%2Bdevice1%2C~~%2C%2C' or + # Compatible with RHEL8 (Python v3.6.8) + results['fullpath'] == '/%40userA%2C%2Bdevice1%2C%7E%7E%2C%2C' + ) + assert results['path'] == '/' + assert ( + results['query'] == '%40userA%2C%2Bdevice1%2C~~%2C%2C' or + # Compatible with RHEL8 (Python v3.6.8) + results['query'] == '%40userA%2C%2Bdevice1%2C%7E%7E%2C%2C' + ) + assert results['schema'] == 'lseas' + assert ( + results['url'] == + 'lseas://user:pass@myhost:3222/%40userA%2C%2Bdevice1%2C~~%2C%2C' or + # Compatible with RHEL8 (Python v3.6.8) + results['url'] == + 'lseas://user:pass@myhost:3222/%40userA%2C%2Bdevice1%2C%7E%7E%2C%2C' + ) + assert isinstance(results['qsd:'], dict) is True + + instance = NotifyLunaSea(**results) + assert isinstance(instance, NotifyLunaSea) + assert len(instance.targets) == 2 + assert ('@', 'userA') in instance.targets + assert ('+', 'device1') in instance.targets + + assert instance.notify("test") is True + + # 1 call to user, and second to device + assert mock_post.call_count == 2 + + url = mock_post.call_args_list[0][0][0] + assert url == 'https://myhost:3222/v1/custom/device/device1' + payload = loads(mock_post.call_args_list[0][1]['data']) + assert 'title' in payload + assert 'body' in payload + assert 'image' not in payload + assert payload['body'] == 'test' + assert payload['title'] == 'Apprise Notifications' + + url = mock_post.call_args_list[1][0][0] + assert url == 'https://myhost:3222/v1/custom/user/userA' + payload = loads(mock_post.call_args_list[1][1]['data']) + assert 'title' in payload + assert 'body' in payload + assert 'image' not in payload + assert payload['body'] == 'test' + assert payload['title'] == 'Apprise Notifications' + + assert '@userA' in instance.url() + assert '+device1' in instance.url()