diff --git a/libs/apprise-1.7.3.dist-info/INSTALLER b/libs/apprise-1.7.4.dist-info/INSTALLER similarity index 100% rename from libs/apprise-1.7.3.dist-info/INSTALLER rename to libs/apprise-1.7.4.dist-info/INSTALLER diff --git a/libs/apprise-1.7.3.dist-info/LICENSE b/libs/apprise-1.7.4.dist-info/LICENSE similarity index 100% rename from libs/apprise-1.7.3.dist-info/LICENSE rename to libs/apprise-1.7.4.dist-info/LICENSE diff --git a/libs/apprise-1.7.3.dist-info/METADATA b/libs/apprise-1.7.4.dist-info/METADATA similarity index 96% rename from libs/apprise-1.7.3.dist-info/METADATA rename to libs/apprise-1.7.4.dist-info/METADATA index ca24a401d..1fbaf2a08 100644 --- a/libs/apprise-1.7.3.dist-info/METADATA +++ b/libs/apprise-1.7.4.dist-info/METADATA @@ -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: lead2gold@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 @@ -115,6 +115,7 @@ The table below identifies the services this tool supports and some example serv | [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
lametric://apikey@hostname:port
lametric://client_id@client_secret | [Line](https://github.com/caronc/apprise/wiki/Notify_line) | line:// | (TCP) 443 | line://Token@User
line://Token/User1/User2/UserN +| [LunaSea](https://github.com/caronc/apprise/wiki/Notify_lunasea) | lunasea:// | (TCP) 80 or 443 | lunasea://user:pass@+FireBaseDevice/
lunasea://user:pass@FireBaseUser/
lunasea://user:pass@hostname/+FireBaseDevice/
lunasea://user:pass@hostname/@FireBaseUser/ | [Mailgun](https://github.com/caronc/apprise/wiki/Notify_mailgun) | mailgun:// | (TCP) 443 | mailgun://user@hostname/apikey
mailgun://user@hostname/apikey/email
mailgun://user@hostname/apikey/email1/email2/emailN
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
mastodon://access_key@hostname/@user
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
matrix://user@hostname
matrixs://user:pass@hostname:port/#room_alias
matrixs://user:pass@hostname:port/!room_id
matrixs://user:pass@hostname:port/#room_alias/!room_id/#room2
matrixs://token@hostname:port/?webhook=matrix
matrix://user:token@hostname/?webhook=slack&format=markdown @@ -270,43 +271,35 @@ No one wants to put their credentials out for everyone to see on the command lin # 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 --git a/libs/apprise-1.7.3.dist-info/RECORD b/libs/apprise-1.7.4.dist-info/RECORD similarity index 94% rename from libs/apprise-1.7.3.dist-info/RECORD rename to libs/apprise-1.7.4.dist-info/RECORD index de47ab4a7..ca5dcd003 100644 --- a/libs/apprise-1.7.3.dist-info/RECORD +++ b/libs/apprise-1.7.4.dist-info/RECORD @@ -1,12 +1,12 @@ ../../bin/apprise,sha256=ZJ-e4qqxNLtdW_DAvpuPPX5iROIiQd8I6nvg7vtAv-g,233 -apprise-1.7.3.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 -apprise-1.7.3.dist-info/LICENSE,sha256=gt7qKBxRhVcdmXCYVtrWP6DtYjD0DzONet600dkU994,1343 -apprise-1.7.3.dist-info/METADATA,sha256=1IS6O2IzRJcduJO9wK9tJhz1jDhZXcTTXfudj3-yy-Q,44360 -apprise-1.7.3.dist-info/RECORD,, -apprise-1.7.3.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 -apprise-1.7.3.dist-info/WHEEL,sha256=Xo9-1PvkuimrydujYJAjF7pCkriuXBpUPEjma1nZyJ0,92 -apprise-1.7.3.dist-info/entry_points.txt,sha256=71YypBuNdjAKiaLsiMG40HEfLHxkU4Mi7o_S0s0d8wI,45 -apprise-1.7.3.dist-info/top_level.txt,sha256=JrCRn-_rXw5LMKXkIgMSE4E0t1Ks9TYrBH54Pflwjkk,8 +apprise-1.7.4.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 +apprise-1.7.4.dist-info/LICENSE,sha256=gt7qKBxRhVcdmXCYVtrWP6DtYjD0DzONet600dkU994,1343 +apprise-1.7.4.dist-info/METADATA,sha256=Lc66iPsSCFv0zmoQX8NFuc_V5CqFYN5Yrx_gqeN8OF8,44502 +apprise-1.7.4.dist-info/RECORD,, +apprise-1.7.4.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +apprise-1.7.4.dist-info/WHEEL,sha256=Xo9-1PvkuimrydujYJAjF7pCkriuXBpUPEjma1nZyJ0,92 +apprise-1.7.4.dist-info/entry_points.txt,sha256=71YypBuNdjAKiaLsiMG40HEfLHxkU4Mi7o_S0s0d8wI,45 +apprise-1.7.4.dist-info/top_level.txt,sha256=JrCRn-_rXw5LMKXkIgMSE4E0t1Ks9TYrBH54Pflwjkk,8 apprise/Apprise.py,sha256=Stm2NhJprWRaMwQfTiIQG_nR1bLpHi_zcdwEcsCpa-A,32865 apprise/Apprise.pyi,sha256=_4TBKvT-QVj3s6PuTh3YX-BbQMeJTdBGdVpubLMY4_k,2203 apprise/AppriseAsset.py,sha256=jRW8Y1EcAvjVA9h_mINmsjO4DM3S0aDl6INIFVMcUCs,11647 @@ -21,7 +21,7 @@ apprise/ConfigurationManager.py,sha256=MUmGajxjgnr6FGN7xb3q0nD0VVgdTdvapBBR7CsI- apprise/NotificationManager.py,sha256=ZJgkiCgcJ7Bz_6bwQ47flrcxvLMbA4Vbw0HG_yTsGdE,2041 apprise/URLBase.py,sha256=ZWjHz69790EfVNDIBzWzRZzjw-gwC3db_t3_3an6cWI,28388 apprise/URLBase.pyi,sha256=WLaRREH7FzZ5x3-qkDkupojWGFC4uFwJ1EDt02lVs8c,520 -apprise/__init__.py,sha256=hqhBy0IX4xGRicwbKBMX_OVy1tgOo7hBrH_hG0n0XP4,3368 +apprise/__init__.py,sha256=oBHq9Zbcwz9DTkurqnEhbu9Q79a0TdVAZrWFIhlk__8,3368 apprise/assets/NotifyXML-1.0.xsd,sha256=292qQ_IUl5EWDhPyzm9UTT0C2rVvJkyGar8jiODkJs8,986 apprise/assets/NotifyXML-1.1.xsd,sha256=bjR3CGG4AEXoJjYkGCbDttKHSkPP1FlIWO02E7G59g4,1758 apprise/assets/themes/default/apprise-failure-128x128.ico,sha256=Mt0ptfHJaN3Wsv5UCNDn9_3lyEDHxVDv1JdaDEI_xCA,67646 @@ -50,7 +50,7 @@ apprise/attachment/AttachBase.pyi,sha256=w0XG_QKauiMLJ7eQ4S57IiLIURZHm_Snw7l6-ih apprise/attachment/AttachFile.py,sha256=MbHY_av0GeM_AIBKV02Hq7SHiZ9eCr1yTfvDMUgi2I4,4765 apprise/attachment/AttachHTTP.py,sha256=dyDy3U47cI28ENhaw1r5nQlGh8FWHZlHI8n9__k8wcY,11995 apprise/attachment/__init__.py,sha256=xabgXpvV05X-YRuqIt3uGYMXwYNXjHyF6Dwd8HfZCFE,1658 -apprise/cli.py,sha256=Xl69ZR6dd9SkKqYErAiq2sSK89mXPwWr-QzHaJmK0Ic,20228 +apprise/cli.py,sha256=h-pWSQPqQficH6J-OEp3MTGydWyt6vMYnDZvHCeAt4Y,20697 apprise/common.py,sha256=I6wfrndggCL7l7KAl7Cm4uwAX9n0l3SN4-BVvTE0L0M,5593 apprise/common.pyi,sha256=luF3QRiClDCk8Z23rI6FCGYsVmodOt_JYfYyzGogdNM,447 apprise/config/ConfigBase.py,sha256=A4p_N9vSxOK37x9kuYeZFzHhAeEt-TCe2oweNi2KGg4,53062 @@ -67,7 +67,7 @@ apprise/emojis.py,sha256=ONF0t8dY9f2XlEkLUG79-ybKVAj2GqbPj2-Be97vAoI,87738 apprise/i18n/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 apprise/i18n/en/LC_MESSAGES/apprise.mo,sha256=oUTuHREmLEYN07oqYqRMJ_kU71-o5o37NsF4RXlC5AU,3959 apprise/logger.py,sha256=131hqhed8cUj9x_mfXDEvwA2YbcYDFAYiWVK1HgxRVY,6921 -apprise/manager.py,sha256=1KQVMAzq-wyZlzDBObKawQySah5F_Cq7LFdkmDctqDU,27086 +apprise/manager.py,sha256=R9w8jxQRNy6Z_XDcobkt4JYbrC4jtj2OwRw9Zrib3CA,26857 apprise/plugins/NotifyAppriseAPI.py,sha256=ISBE0brD3eQdyw3XrGXd4Uc4kSYvIuI3SSUVCt-bkdo,16654 apprise/plugins/NotifyAprs.py,sha256=IS1uxIl391L3i2LOK6x8xmlOG1W58k4o793Oq2W5Wao,24220 apprise/plugins/NotifyBark.py,sha256=bsDvKooRy4k1Gg7tvBjv3DIx7-WZiV_mbTrkTwMtd9Q,15698 @@ -108,6 +108,7 @@ apprise/plugins/NotifyKavenegar.py,sha256=F5xTUdebM1lK6yGFbZJQB9Zgw2LTI0angeA-3N apprise/plugins/NotifyKumulos.py,sha256=eCEW2ZverZqETOLHVWMC4E8Ll6rEhhEWOSD73RD80SM,8214 apprise/plugins/NotifyLametric.py,sha256=h8vZoX-Ll5NBZRprBlxTO2H9w0lOiMxglGvUgJtK4_8,37534 apprise/plugins/NotifyLine.py,sha256=OVI0ozMJcq_-dI8dodVX52dzUzgENlAbOik-Kw4l-rI,10676 +apprise/plugins/NotifyLunaSea.py,sha256=woN8XdkwAjhgxAXp7Zj4XsWLybNL80l4W3Dx5BvobZg,14459 apprise/plugins/NotifyMQTT.py,sha256=PFLwESgR8dMZvVFHxmOZ8xfy-YqyX5b2kl_e8Z1lo-0,19537 apprise/plugins/NotifyMSG91.py,sha256=P7JPyT1xmucnaEeCZPf_6aJfe1gS_STYYwEM7hJ7QBw,12677 apprise/plugins/NotifyMSTeams.py,sha256=dFH575hoLL3zRddbBKfozlYjxvPJGbj3BKvfJSIkvD0,22976 diff --git a/libs/apprise-1.7.3.dist-info/REQUESTED b/libs/apprise-1.7.4.dist-info/REQUESTED similarity index 100% rename from libs/apprise-1.7.3.dist-info/REQUESTED rename to libs/apprise-1.7.4.dist-info/REQUESTED diff --git a/libs/apprise-1.7.3.dist-info/WHEEL b/libs/apprise-1.7.4.dist-info/WHEEL similarity index 100% rename from libs/apprise-1.7.3.dist-info/WHEEL rename to libs/apprise-1.7.4.dist-info/WHEEL diff --git a/libs/apprise-1.7.3.dist-info/entry_points.txt b/libs/apprise-1.7.4.dist-info/entry_points.txt similarity index 100% rename from libs/apprise-1.7.3.dist-info/entry_points.txt rename to libs/apprise-1.7.4.dist-info/entry_points.txt diff --git a/libs/apprise-1.7.3.dist-info/top_level.txt b/libs/apprise-1.7.4.dist-info/top_level.txt similarity index 100% rename from libs/apprise-1.7.3.dist-info/top_level.txt rename to libs/apprise-1.7.4.dist-info/top_level.txt diff --git a/libs/apprise/__init__.py b/libs/apprise/__init__.py index c07d769ae..bb18eaec8 100644 --- a/libs/apprise/__init__.py +++ b/libs/apprise/__init__.py @@ -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 ' diff --git a/libs/apprise/cli.py b/libs/apprise/cli.py index 59a644272..11a6cbc2b 100644 --- a/libs/apprise/cli.py +++ b/libs/apprise/cli.py @@ -67,25 +67,30 @@ CONTEXT_SETTINGS = dict(help_option_names=['-h', '--help']) 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 @@ if platform.system() == 'Windows': # 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 @@ if platform.system() == 'Windows': # 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 --git a/libs/apprise/manager.py b/libs/apprise/manager.py index d649afab7..c2b715d4f 100644 --- a/libs/apprise/manager.py +++ b/libs/apprise/manager.py @@ -365,67 +365,66 @@ class PluginManager(metaclass=Singleton): # 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 + 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) + # 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')): + 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) + 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: + 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 - self._paths_previously_scanned.add(path) + if not cache or \ + (cache and new_path not in + self._paths_previously_scanned): + # Load our module + _import_module(new_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) + # 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 - # Load our module - _import_module(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 + + # Load our module + _import_module(path) return None diff --git a/libs/apprise/plugins/NotifyLunaSea.py b/libs/apprise/plugins/NotifyLunaSea.py new file mode 100644 index 000000000..51d820915 --- /dev/null +++ b/libs/apprise/plugins/NotifyLunaSea.py @@ -0,0 +1,440 @@ +# -*- coding: utf-8 -*- +# BSD 2-Clause License +# +# Apprise - Push Notification Library. +# Copyright (c) 2024, Chris Caron +# +# 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 --git a/libs/version.txt b/libs/version.txt index 0e1dd8c78..bfd019444 100644 --- a/libs/version.txt +++ b/libs/version.txt @@ -2,7 +2,7 @@ alembic==1.13.1 aniso8601==9.0.1 argparse==1.4.0 -apprise==1.7.3 +apprise==1.7.4 apscheduler<=3.10.4 attrs==23.2.0 blinker==1.7.0