Updated apprise to latest version to prevent deadlocks issue in 1.7.3.

pull/2427/head v1.4.3-beta.10
morpheus65535 3 months ago
parent 35eabb6a83
commit aedf2d4d89

@ -1,12 +1,12 @@
Metadata-Version: 2.1 Metadata-Version: 2.1
Name: apprise Name: apprise
Version: 1.7.3 Version: 1.7.4
Summary: Push Notifications that work with just about every platform! Summary: Push Notifications that work with just about every platform!
Home-page: https://github.com/caronc/apprise Home-page: https://github.com/caronc/apprise
Author: Chris Caron Author: Chris Caron
Author-email: lead2gold@gmail.com Author-email: lead2gold@gmail.com
License: BSD 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: Development Status :: 5 - Production/Stable
Classifier: Intended Audience :: Developers Classifier: Intended Audience :: Developers
Classifier: Intended Audience :: System Administrators 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 | [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 | [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 | [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" | [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 | [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 | [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
@ -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 # By default if no url or configuration is specified apprise will attempt to load
# configuration files (if present) from: # configuration files (if present) from:
# ~/.apprise # ~/.apprise
# ~/.apprise.yml
# ~/.apprise.yaml # ~/.apprise.yaml
# ~/.config/apprise # ~/.config/apprise.conf
# ~/.config/apprise.yml
# ~/.config/apprise.yaml # ~/.config/apprise.yaml
# /etc/apprise # /etc/apprise.conf
# /etc/apprise.yml
# /etc/apprise.yaml # /etc/apprise.yaml
# Also a subdirectory handling allows you to leverage plugins # Also a subdirectory handling allows you to leverage plugins
# ~/.apprise/apprise # ~/.apprise/apprise
# ~/.apprise/apprise.yml
# ~/.apprise/apprise.yaml # ~/.apprise/apprise.yaml
# ~/.config/apprise/apprise # ~/.config/apprise/apprise.conf
# ~/.config/apprise/apprise.yml
# ~/.config/apprise/apprise.yaml # ~/.config/apprise/apprise.yaml
# /etc/apprise/apprise
# /etc/apprise/apprise.yml
# /etc/apprise/apprise.yaml # /etc/apprise/apprise.yaml
# /etc/apprise/apprise.conf
# Windows users can store their default configuration files here: # Windows users can store their default configuration files here:
# %APPDATA%/Apprise/apprise # %APPDATA%/Apprise/apprise.conf
# %APPDATA%/Apprise/apprise.yml
# %APPDATA%/Apprise/apprise.yaml # %APPDATA%/Apprise/apprise.yaml
# %LOCALAPPDATA%/Apprise/apprise # %LOCALAPPDATA%/Apprise/apprise.conf
# %LOCALAPPDATA%/Apprise/apprise.yml
# %LOCALAPPDATA%/Apprise/apprise.yaml # %LOCALAPPDATA%/Apprise/apprise.yaml
# %ALLUSERSPROFILE%\Apprise\apprise # %ALLUSERSPROFILE%\Apprise\apprise.conf
# %ALLUSERSPROFILE%\Apprise\apprise.yml
# %ALLUSERSPROFILE%\Apprise\apprise.yaml # %ALLUSERSPROFILE%\Apprise\apprise.yaml
# %PROGRAMFILES%\Apprise\apprise # %PROGRAMFILES%\Apprise\apprise.conf
# %PROGRAMFILES%\Apprise\apprise.yml
# %PROGRAMFILES%\Apprise\apprise.yaml # %PROGRAMFILES%\Apprise\apprise.yaml
# %COMMONPROGRAMFILES%\Apprise\apprise # %COMMONPROGRAMFILES%\Apprise\apprise.conf
# %COMMONPROGRAMFILES%\Apprise\apprise.yml
# %COMMONPROGRAMFILES%\Apprise\apprise.yaml # %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: # If you loaded one of those files, your command line gets really easy:
apprise -vv -t 'my title' -b 'my notification body' apprise -vv -t 'my title' -b 'my notification body'

@ -1,12 +1,12 @@
../../bin/apprise,sha256=ZJ-e4qqxNLtdW_DAvpuPPX5iROIiQd8I6nvg7vtAv-g,233 ../../bin/apprise,sha256=ZJ-e4qqxNLtdW_DAvpuPPX5iROIiQd8I6nvg7vtAv-g,233
apprise-1.7.3.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 apprise-1.7.4.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
apprise-1.7.3.dist-info/LICENSE,sha256=gt7qKBxRhVcdmXCYVtrWP6DtYjD0DzONet600dkU994,1343 apprise-1.7.4.dist-info/LICENSE,sha256=gt7qKBxRhVcdmXCYVtrWP6DtYjD0DzONet600dkU994,1343
apprise-1.7.3.dist-info/METADATA,sha256=1IS6O2IzRJcduJO9wK9tJhz1jDhZXcTTXfudj3-yy-Q,44360 apprise-1.7.4.dist-info/METADATA,sha256=Lc66iPsSCFv0zmoQX8NFuc_V5CqFYN5Yrx_gqeN8OF8,44502
apprise-1.7.3.dist-info/RECORD,, apprise-1.7.4.dist-info/RECORD,,
apprise-1.7.3.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 apprise-1.7.4.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
apprise-1.7.3.dist-info/WHEEL,sha256=Xo9-1PvkuimrydujYJAjF7pCkriuXBpUPEjma1nZyJ0,92 apprise-1.7.4.dist-info/WHEEL,sha256=Xo9-1PvkuimrydujYJAjF7pCkriuXBpUPEjma1nZyJ0,92
apprise-1.7.3.dist-info/entry_points.txt,sha256=71YypBuNdjAKiaLsiMG40HEfLHxkU4Mi7o_S0s0d8wI,45 apprise-1.7.4.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/top_level.txt,sha256=JrCRn-_rXw5LMKXkIgMSE4E0t1Ks9TYrBH54Pflwjkk,8
apprise/Apprise.py,sha256=Stm2NhJprWRaMwQfTiIQG_nR1bLpHi_zcdwEcsCpa-A,32865 apprise/Apprise.py,sha256=Stm2NhJprWRaMwQfTiIQG_nR1bLpHi_zcdwEcsCpa-A,32865
apprise/Apprise.pyi,sha256=_4TBKvT-QVj3s6PuTh3YX-BbQMeJTdBGdVpubLMY4_k,2203 apprise/Apprise.pyi,sha256=_4TBKvT-QVj3s6PuTh3YX-BbQMeJTdBGdVpubLMY4_k,2203
apprise/AppriseAsset.py,sha256=jRW8Y1EcAvjVA9h_mINmsjO4DM3S0aDl6INIFVMcUCs,11647 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/NotificationManager.py,sha256=ZJgkiCgcJ7Bz_6bwQ47flrcxvLMbA4Vbw0HG_yTsGdE,2041
apprise/URLBase.py,sha256=ZWjHz69790EfVNDIBzWzRZzjw-gwC3db_t3_3an6cWI,28388 apprise/URLBase.py,sha256=ZWjHz69790EfVNDIBzWzRZzjw-gwC3db_t3_3an6cWI,28388
apprise/URLBase.pyi,sha256=WLaRREH7FzZ5x3-qkDkupojWGFC4uFwJ1EDt02lVs8c,520 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.0.xsd,sha256=292qQ_IUl5EWDhPyzm9UTT0C2rVvJkyGar8jiODkJs8,986
apprise/assets/NotifyXML-1.1.xsd,sha256=bjR3CGG4AEXoJjYkGCbDttKHSkPP1FlIWO02E7G59g4,1758 apprise/assets/NotifyXML-1.1.xsd,sha256=bjR3CGG4AEXoJjYkGCbDttKHSkPP1FlIWO02E7G59g4,1758
apprise/assets/themes/default/apprise-failure-128x128.ico,sha256=Mt0ptfHJaN3Wsv5UCNDn9_3lyEDHxVDv1JdaDEI_xCA,67646 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/AttachFile.py,sha256=MbHY_av0GeM_AIBKV02Hq7SHiZ9eCr1yTfvDMUgi2I4,4765
apprise/attachment/AttachHTTP.py,sha256=dyDy3U47cI28ENhaw1r5nQlGh8FWHZlHI8n9__k8wcY,11995 apprise/attachment/AttachHTTP.py,sha256=dyDy3U47cI28ENhaw1r5nQlGh8FWHZlHI8n9__k8wcY,11995
apprise/attachment/__init__.py,sha256=xabgXpvV05X-YRuqIt3uGYMXwYNXjHyF6Dwd8HfZCFE,1658 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.py,sha256=I6wfrndggCL7l7KAl7Cm4uwAX9n0l3SN4-BVvTE0L0M,5593
apprise/common.pyi,sha256=luF3QRiClDCk8Z23rI6FCGYsVmodOt_JYfYyzGogdNM,447 apprise/common.pyi,sha256=luF3QRiClDCk8Z23rI6FCGYsVmodOt_JYfYyzGogdNM,447
apprise/config/ConfigBase.py,sha256=A4p_N9vSxOK37x9kuYeZFzHhAeEt-TCe2oweNi2KGg4,53062 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/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
apprise/i18n/en/LC_MESSAGES/apprise.mo,sha256=oUTuHREmLEYN07oqYqRMJ_kU71-o5o37NsF4RXlC5AU,3959 apprise/i18n/en/LC_MESSAGES/apprise.mo,sha256=oUTuHREmLEYN07oqYqRMJ_kU71-o5o37NsF4RXlC5AU,3959
apprise/logger.py,sha256=131hqhed8cUj9x_mfXDEvwA2YbcYDFAYiWVK1HgxRVY,6921 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/NotifyAppriseAPI.py,sha256=ISBE0brD3eQdyw3XrGXd4Uc4kSYvIuI3SSUVCt-bkdo,16654
apprise/plugins/NotifyAprs.py,sha256=IS1uxIl391L3i2LOK6x8xmlOG1W58k4o793Oq2W5Wao,24220 apprise/plugins/NotifyAprs.py,sha256=IS1uxIl391L3i2LOK6x8xmlOG1W58k4o793Oq2W5Wao,24220
apprise/plugins/NotifyBark.py,sha256=bsDvKooRy4k1Gg7tvBjv3DIx7-WZiV_mbTrkTwMtd9Q,15698 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/NotifyKumulos.py,sha256=eCEW2ZverZqETOLHVWMC4E8Ll6rEhhEWOSD73RD80SM,8214
apprise/plugins/NotifyLametric.py,sha256=h8vZoX-Ll5NBZRprBlxTO2H9w0lOiMxglGvUgJtK4_8,37534 apprise/plugins/NotifyLametric.py,sha256=h8vZoX-Ll5NBZRprBlxTO2H9w0lOiMxglGvUgJtK4_8,37534
apprise/plugins/NotifyLine.py,sha256=OVI0ozMJcq_-dI8dodVX52dzUzgENlAbOik-Kw4l-rI,10676 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/NotifyMQTT.py,sha256=PFLwESgR8dMZvVFHxmOZ8xfy-YqyX5b2kl_e8Z1lo-0,19537
apprise/plugins/NotifyMSG91.py,sha256=P7JPyT1xmucnaEeCZPf_6aJfe1gS_STYYwEM7hJ7QBw,12677 apprise/plugins/NotifyMSG91.py,sha256=P7JPyT1xmucnaEeCZPf_6aJfe1gS_STYYwEM7hJ7QBw,12677
apprise/plugins/NotifyMSTeams.py,sha256=dFH575hoLL3zRddbBKfozlYjxvPJGbj3BKvfJSIkvD0,22976 apprise/plugins/NotifyMSTeams.py,sha256=dFH575hoLL3zRddbBKfozlYjxvPJGbj3BKvfJSIkvD0,22976

@ -27,7 +27,7 @@
# POSSIBILITY OF SUCH DAMAGE. # POSSIBILITY OF SUCH DAMAGE.
__title__ = 'Apprise' __title__ = 'Apprise'
__version__ = '1.7.3' __version__ = '1.7.4'
__author__ = 'Chris Caron' __author__ = 'Chris Caron'
__license__ = 'BSD' __license__ = 'BSD'
__copywrite__ = 'Copyright (C) 2024 Chris Caron <lead2gold@gmail.com>' __copywrite__ = 'Copyright (C) 2024 Chris Caron <lead2gold@gmail.com>'

@ -67,25 +67,30 @@ CONTEXT_SETTINGS = dict(help_option_names=['-h', '--help'])
DEFAULT_CONFIG_PATHS = ( DEFAULT_CONFIG_PATHS = (
# Legacy Path Support # Legacy Path Support
'~/.apprise', '~/.apprise',
'~/.apprise.conf',
'~/.apprise.yml', '~/.apprise.yml',
'~/.apprise.yaml', '~/.apprise.yaml',
'~/.config/apprise', '~/.config/apprise',
'~/.config/apprise.conf',
'~/.config/apprise.yml', '~/.config/apprise.yml',
'~/.config/apprise.yaml', '~/.config/apprise.yaml',
# Plugin Support Extended Directory Search Paths # Plugin Support Extended Directory Search Paths
'~/.apprise/apprise', '~/.apprise/apprise',
'~/.apprise/apprise.conf',
'~/.apprise/apprise.yml', '~/.apprise/apprise.yml',
'~/.apprise/apprise.yaml', '~/.apprise/apprise.yaml',
'~/.config/apprise/apprise', '~/.config/apprise/apprise',
'~/.config/apprise/apprise.conf',
'~/.config/apprise/apprise.yml', '~/.config/apprise/apprise.yml',
'~/.config/apprise/apprise.yaml', '~/.config/apprise/apprise.yaml',
# Global Configuration Support # Global Configuration File Support
'/etc/apprise', '/etc/apprise',
'/etc/apprise.yml', '/etc/apprise.yml',
'/etc/apprise.yaml', '/etc/apprise.yaml',
'/etc/apprise/apprise', '/etc/apprise/apprise',
'/etc/apprise/apprise.conf',
'/etc/apprise/apprise.yml', '/etc/apprise/apprise.yml',
'/etc/apprise/apprise.yaml', '/etc/apprise/apprise.yaml',
) )
@ -104,9 +109,11 @@ if platform.system() == 'Windows':
# Default Config Search Path for Windows Users # Default Config Search Path for Windows Users
DEFAULT_CONFIG_PATHS = ( DEFAULT_CONFIG_PATHS = (
expandvars('%APPDATA%\\Apprise\\apprise'), expandvars('%APPDATA%\\Apprise\\apprise'),
expandvars('%APPDATA%\\Apprise\\apprise.conf'),
expandvars('%APPDATA%\\Apprise\\apprise.yml'), expandvars('%APPDATA%\\Apprise\\apprise.yml'),
expandvars('%APPDATA%\\Apprise\\apprise.yaml'), expandvars('%APPDATA%\\Apprise\\apprise.yaml'),
expandvars('%LOCALAPPDATA%\\Apprise\\apprise'), expandvars('%LOCALAPPDATA%\\Apprise\\apprise'),
expandvars('%LOCALAPPDATA%\\Apprise\\apprise.conf'),
expandvars('%LOCALAPPDATA%\\Apprise\\apprise.yml'), expandvars('%LOCALAPPDATA%\\Apprise\\apprise.yml'),
expandvars('%LOCALAPPDATA%\\Apprise\\apprise.yaml'), expandvars('%LOCALAPPDATA%\\Apprise\\apprise.yaml'),
@ -116,16 +123,19 @@ if platform.system() == 'Windows':
# C:\ProgramData\Apprise\ # C:\ProgramData\Apprise\
expandvars('%ALLUSERSPROFILE%\\Apprise\\apprise'), expandvars('%ALLUSERSPROFILE%\\Apprise\\apprise'),
expandvars('%ALLUSERSPROFILE%\\Apprise\\apprise.conf'),
expandvars('%ALLUSERSPROFILE%\\Apprise\\apprise.yml'), expandvars('%ALLUSERSPROFILE%\\Apprise\\apprise.yml'),
expandvars('%ALLUSERSPROFILE%\\Apprise\\apprise.yaml'), expandvars('%ALLUSERSPROFILE%\\Apprise\\apprise.yaml'),
# C:\Program Files\Apprise # C:\Program Files\Apprise
expandvars('%PROGRAMFILES%\\Apprise\\apprise'), expandvars('%PROGRAMFILES%\\Apprise\\apprise'),
expandvars('%PROGRAMFILES%\\Apprise\\apprise.conf'),
expandvars('%PROGRAMFILES%\\Apprise\\apprise.yml'), expandvars('%PROGRAMFILES%\\Apprise\\apprise.yml'),
expandvars('%PROGRAMFILES%\\Apprise\\apprise.yaml'), expandvars('%PROGRAMFILES%\\Apprise\\apprise.yaml'),
# C:\Program Files\Common Files # C:\Program Files\Common Files
expandvars('%COMMONPROGRAMFILES%\\Apprise\\apprise'), expandvars('%COMMONPROGRAMFILES%\\Apprise\\apprise'),
expandvars('%COMMONPROGRAMFILES%\\Apprise\\apprise.conf'),
expandvars('%COMMONPROGRAMFILES%\\Apprise\\apprise.yml'), expandvars('%COMMONPROGRAMFILES%\\Apprise\\apprise.yml'),
expandvars('%COMMONPROGRAMFILES%\\Apprise\\apprise.yaml'), expandvars('%COMMONPROGRAMFILES%\\Apprise\\apprise.yaml'),
) )

@ -365,67 +365,66 @@ class PluginManager(metaclass=Singleton):
# end of _import_module() # end of _import_module()
return return
with self._lock: for _path in paths:
for _path in paths: path = os.path.abspath(os.path.expanduser(_path))
path = os.path.abspath(os.path.expanduser(_path)) if (cache and path in self._paths_previously_scanned) \
if (cache and path in self._paths_previously_scanned) \ or not os.path.exists(path):
or not os.path.exists(path): # We're done as we've already scanned this
# We're done as we've already scanned this continue
continue
# Store our path as a way of hashing it has been handled # Store our path as a way of hashing it has been handled
self._paths_previously_scanned.add(path) self._paths_previously_scanned.add(path)
if os.path.isdir(path) and not \ if os.path.isdir(path) and not \
os.path.isfile(os.path.join(path, '__init__.py')): os.path.isfile(os.path.join(path, '__init__.py')):
logger.debug('Scanning for custom plugins in: %s', path) logger.debug('Scanning for custom plugins in: %s', path)
for entry in os.listdir(path): for entry in os.listdir(path):
re_match = module_re.match(entry) re_match = module_re.match(entry)
if not re_match: if not re_match:
# keep going # keep going
logger.trace('Plugin Scan: Ignoring %s', entry) logger.trace('Plugin Scan: Ignoring %s', entry)
continue continue
new_path = os.path.join(path, entry) new_path = os.path.join(path, entry)
if os.path.isdir(new_path): if os.path.isdir(new_path):
# Update our path # Update our path
new_path = os.path.join(path, entry, '__init__.py') new_path = os.path.join(path, entry, '__init__.py')
if not os.path.isfile(new_path): if not os.path.isfile(new_path):
logger.trace( logger.trace(
'Plugin Scan: Ignoring %s', 'Plugin Scan: Ignoring %s',
os.path.join(path, entry)) 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 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 # Add our subdir path
re_match = module_re.match(os.path.basename(path)) self._paths_previously_scanned.add(new_path)
# must be a match and must have a .py extension else:
if not re_match or not re_match.group(1): if os.path.isdir(path):
# keep going # This logic is safe to apply because we already
logger.trace('Plugin Scan: Ignoring %s', path) # 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 continue
# Load our module self._paths_previously_scanned.add(path)
_import_module(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 return None

@ -0,0 +1,440 @@
# -*- coding: utf-8 -*-
# BSD 2-Clause License
#
# Apprise - Push Notification Library.
# Copyright (c) 2024, Chris Caron <lead2gold@gmail.com>
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# 1. Redistributions of source code must retain the above copyright notice,
# this list of conditions and the following disclaimer.
#
# 2. Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
#
# API:
# 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

@ -2,7 +2,7 @@
alembic==1.13.1 alembic==1.13.1
aniso8601==9.0.1 aniso8601==9.0.1
argparse==1.4.0 argparse==1.4.0
apprise==1.7.3 apprise==1.7.4
apscheduler<=3.10.4 apscheduler<=3.10.4
attrs==23.2.0 attrs==23.2.0
blinker==1.7.0 blinker==1.7.0

Loading…
Cancel
Save