diff options
author | morpheus65535 <[email protected]> | 2024-03-03 22:47:09 -0500 |
---|---|---|
committer | morpheus65535 <[email protected]> | 2024-03-03 22:47:09 -0500 |
commit | e5db62eb95ac86e9a4e2b42abcfb74cd9a8611d1 (patch) | |
tree | 562141e0f8f7e196b59825b74cb1886217e391e2 /libs | |
parent | 20d235e1b511d5c31e4fdeaf381f749130e1dc7b (diff) | |
download | bazarr-e5db62eb95ac86e9a4e2b42abcfb74cd9a8611d1.tar.gz bazarr-e5db62eb95ac86e9a4e2b42abcfb74cd9a8611d1.zip |
no log: latest apprise upgradev1.4.3-beta.6
Diffstat (limited to 'libs')
-rw-r--r-- | libs/apprise-1.7.3.dist-info/INSTALLER (renamed from libs/apprise-1.7.2.dist-info/INSTALLER) | 0 | ||||
-rw-r--r-- | libs/apprise-1.7.3.dist-info/LICENSE (renamed from libs/apprise-1.7.2.dist-info/LICENSE) | 0 | ||||
-rw-r--r-- | libs/apprise-1.7.3.dist-info/METADATA (renamed from libs/apprise-1.7.2.dist-info/METADATA) | 16 | ||||
-rw-r--r-- | libs/apprise-1.7.3.dist-info/RECORD (renamed from libs/apprise-1.7.2.dist-info/RECORD) | 33 | ||||
-rw-r--r-- | libs/apprise-1.7.3.dist-info/REQUESTED (renamed from libs/apprise-1.7.2.dist-info/REQUESTED) | 0 | ||||
-rw-r--r-- | libs/apprise-1.7.3.dist-info/WHEEL (renamed from libs/apprise-1.7.2.dist-info/WHEEL) | 0 | ||||
-rw-r--r-- | libs/apprise-1.7.3.dist-info/entry_points.txt (renamed from libs/apprise-1.7.2.dist-info/entry_points.txt) | 0 | ||||
-rw-r--r-- | libs/apprise-1.7.3.dist-info/top_level.txt (renamed from libs/apprise-1.7.2.dist-info/top_level.txt) | 0 | ||||
-rw-r--r-- | libs/apprise/URLBase.py | 6 | ||||
-rw-r--r-- | libs/apprise/__init__.py | 2 | ||||
-rw-r--r-- | libs/apprise/cli.py | 11 | ||||
-rw-r--r-- | libs/apprise/manager.py | 347 | ||||
-rw-r--r-- | libs/apprise/plugins/NotifyEmail.py | 118 | ||||
-rw-r--r-- | libs/apprise/plugins/NotifyMacOSX.py | 1 | ||||
-rw-r--r-- | libs/apprise/plugins/NotifyMatterMost.py | 372 | ||||
-rw-r--r-- | libs/apprise/plugins/NotifyMattermost.py | 2 | ||||
-rw-r--r-- | libs/apprise/plugins/NotifyNtfy.py | 5 | ||||
-rw-r--r-- | libs/apprise/plugins/NotifyRevolt.py | 437 | ||||
-rw-r--r-- | libs/apprise/plugins/NotifyTelegram.py | 3 | ||||
-rw-r--r-- | libs/version.txt | 2 |
20 files changed, 750 insertions, 605 deletions
diff --git a/libs/apprise-1.7.2.dist-info/INSTALLER b/libs/apprise-1.7.3.dist-info/INSTALLER index a1b589e38..a1b589e38 100644 --- a/libs/apprise-1.7.2.dist-info/INSTALLER +++ b/libs/apprise-1.7.3.dist-info/INSTALLER diff --git a/libs/apprise-1.7.2.dist-info/LICENSE b/libs/apprise-1.7.3.dist-info/LICENSE index f9154fefe..f9154fefe 100644 --- a/libs/apprise-1.7.2.dist-info/LICENSE +++ b/libs/apprise-1.7.3.dist-info/LICENSE diff --git a/libs/apprise-1.7.2.dist-info/METADATA b/libs/apprise-1.7.3.dist-info/METADATA index 9d4e2f391..ca24a401d 100644 --- a/libs/apprise-1.7.2.dist-info/METADATA +++ b/libs/apprise-1.7.3.dist-info/METADATA @@ -1,12 +1,12 @@ Metadata-Version: 2.1 Name: apprise -Version: 1.7.2 +Version: 1.7.3 Summary: Push Notifications that work with just about every platform! Home-page: https://github.com/caronc/apprise Author: Chris Caron Author-email: [email protected] 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 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 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 @@ -146,6 +146,7 @@ The table below identifies the services this tool supports and some example serv | [Pushy](https://github.com/caronc/apprise/wiki/Notify_pushy) | pushy:// | (TCP) 443 | pushy://apikey/DEVICE<br />pushy://apikey/DEVICE1/DEVICE2/DEVICEN<br />pushy://apikey/TOPIC<br />pushy://apikey/TOPIC1/TOPIC2/TOPICN | [PushDeer](https://github.com/caronc/apprise/wiki/Notify_pushdeer) | pushdeer:// or pushdeers:// | (TCP) 80 or 443 | pushdeer://pushKey<br />pushdeer://hostname/pushKey<br />pushdeer://hostname:port/pushKey | [Reddit](https://github.com/caronc/apprise/wiki/Notify_reddit) | reddit:// | (TCP) 443 | reddit://user:password@app_id/app_secret/subreddit<br />reddit://user:password@app_id/app_secret/sub1/sub2/subN +| [Revolt](https://github.com/caronc/apprise/wiki/Notify_Revolt) | revolt:// | (TCP) 443 | revolt://bottoken/ChannelID<br />revolt://bottoken/ChannelID1/ChannelID2/ChannelIDN | | [Rocket.Chat](https://github.com/caronc/apprise/wiki/Notify_rocketchat) | rocket:// or rockets:// | (TCP) 80 or 443 | rocket://user:password@hostname/RoomID/Channel<br />rockets://user:password@hostname:443/#Channel1/#Channel1/RoomID<br />rocket://user:password@hostname/#Channel<br />rocket://webhook@hostname<br />rockets://webhook@hostname/@User/#Channel | [RSyslog](https://github.com/caronc/apprise/wiki/Notify_rsyslog) | rsyslog:// | (UDP) 514 | rsyslog://hostname<br />rsyslog://hostname/Facility | [Ryver](https://github.com/caronc/apprise/wiki/Notify_ryver) | ryver:// | (TCP) 443 | ryver://Organization/Token<br />ryver://botname@Organization/Token @@ -270,30 +271,41 @@ No one wants to put their credentials out for everyone to see on the command lin # configuration files (if present) from: # ~/.apprise # ~/.apprise.yml +# ~/.apprise.yaml # ~/.config/apprise # ~/.config/apprise.yml +# ~/.config/apprise.yaml # /etc/apprise # /etc/apprise.yml +# /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.yaml # /etc/apprise/apprise # /etc/apprise/apprise.yml +# /etc/apprise/apprise.yaml # Windows users can store their default configuration files here: # %APPDATA%/Apprise/apprise # %APPDATA%/Apprise/apprise.yml +# %APPDATA%/Apprise/apprise.yaml # %LOCALAPPDATA%/Apprise/apprise # %LOCALAPPDATA%/Apprise/apprise.yml +# %LOCALAPPDATA%/Apprise/apprise.yaml # %ALLUSERSPROFILE%\Apprise\apprise # %ALLUSERSPROFILE%\Apprise\apprise.yml +# %ALLUSERSPROFILE%\Apprise\apprise.yaml # %PROGRAMFILES%\Apprise\apprise # %PROGRAMFILES%\Apprise\apprise.yml +# %PROGRAMFILES%\Apprise\apprise.yaml # %COMMONPROGRAMFILES%\Apprise\apprise # %COMMONPROGRAMFILES%\Apprise\apprise.yml +# %COMMONPROGRAMFILES%\Apprise\apprise.yaml # 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.2.dist-info/RECORD b/libs/apprise-1.7.3.dist-info/RECORD index 335494660..de47ab4a7 100644 --- a/libs/apprise-1.7.2.dist-info/RECORD +++ b/libs/apprise-1.7.3.dist-info/RECORD @@ -1,12 +1,12 @@ ../../bin/apprise,sha256=ZJ-e4qqxNLtdW_DAvpuPPX5iROIiQd8I6nvg7vtAv-g,233 -apprise-1.7.2.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 -apprise-1.7.2.dist-info/LICENSE,sha256=gt7qKBxRhVcdmXCYVtrWP6DtYjD0DzONet600dkU994,1343 -apprise-1.7.2.dist-info/METADATA,sha256=lNkOI_XF6axOtqkZLFfmVDiDGew_HtM2pfFDZyG62ME,43818 -apprise-1.7.2.dist-info/RECORD,, -apprise-1.7.2.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 -apprise-1.7.2.dist-info/WHEEL,sha256=Xo9-1PvkuimrydujYJAjF7pCkriuXBpUPEjma1nZyJ0,92 -apprise-1.7.2.dist-info/entry_points.txt,sha256=71YypBuNdjAKiaLsiMG40HEfLHxkU4Mi7o_S0s0d8wI,45 -apprise-1.7.2.dist-info/top_level.txt,sha256=JrCRn-_rXw5LMKXkIgMSE4E0t1Ks9TYrBH54Pflwjkk,8 +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/Apprise.py,sha256=Stm2NhJprWRaMwQfTiIQG_nR1bLpHi_zcdwEcsCpa-A,32865 apprise/Apprise.pyi,sha256=_4TBKvT-QVj3s6PuTh3YX-BbQMeJTdBGdVpubLMY4_k,2203 apprise/AppriseAsset.py,sha256=jRW8Y1EcAvjVA9h_mINmsjO4DM3S0aDl6INIFVMcUCs,11647 @@ -19,9 +19,9 @@ apprise/AppriseLocale.py,sha256=ISth7xC7M1WhsSNXdGZFouaA4bi07KP35m9RX-ExG48,8852 apprise/AttachmentManager.py,sha256=EwlnjuKn3fv_pioWcmMCkyDTsO178t6vkEOD8AjAPsw,2053 apprise/ConfigurationManager.py,sha256=MUmGajxjgnr6FGN7xb3q0nD0VVgdTdvapBBR7CsI-rc,2058 apprise/NotificationManager.py,sha256=ZJgkiCgcJ7Bz_6bwQ47flrcxvLMbA4Vbw0HG_yTsGdE,2041 -apprise/URLBase.py,sha256=HgRiGXOCb4ZhTXmRved9VxfcX-eec3pII3Eb0zRh8Aw,28389 +apprise/URLBase.py,sha256=ZWjHz69790EfVNDIBzWzRZzjw-gwC3db_t3_3an6cWI,28388 apprise/URLBase.pyi,sha256=WLaRREH7FzZ5x3-qkDkupojWGFC4uFwJ1EDt02lVs8c,520 -apprise/__init__.py,sha256=cQvk-yABi1MGIYCxa9di1DYMMAl6IuI5BhbzfOt6NSY,3368 +apprise/__init__.py,sha256=hqhBy0IX4xGRicwbKBMX_OVy1tgOo7hBrH_hG0n0XP4,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=fa-3beNKx3ZC3KkNwgJMMfs1LDI2Hjyol_bXp6WhK4s,19739 +apprise/cli.py,sha256=Xl69ZR6dd9SkKqYErAiq2sSK89mXPwWr-QzHaJmK0Ic,20228 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=sJUNy6IttMVVS3D8Nqzab96dogmKJpNVOBVx93HrX7c,25526 +apprise/manager.py,sha256=1KQVMAzq-wyZlzDBObKawQySah5F_Cq7LFdkmDctqDU,27086 apprise/plugins/NotifyAppriseAPI.py,sha256=ISBE0brD3eQdyw3XrGXd4Uc4kSYvIuI3SSUVCt-bkdo,16654 apprise/plugins/NotifyAprs.py,sha256=IS1uxIl391L3i2LOK6x8xmlOG1W58k4o793Oq2W5Wao,24220 apprise/plugins/NotifyBark.py,sha256=bsDvKooRy4k1Gg7tvBjv3DIx7-WZiV_mbTrkTwMtd9Q,15698 @@ -83,7 +83,7 @@ apprise/plugins/NotifyDBus.py,sha256=1eVJHIL3XkFjDePMqfcll35Ie1vxggJ1iBsVFAIaF00 apprise/plugins/NotifyDapnet.py,sha256=KuXjBU0ZrIYtoDei85NeLZ-IP810T4w5oFXH9sWiSh0,13624 apprise/plugins/NotifyDingTalk.py,sha256=NJyETgN6QjtRqtxQjfBLFVuFpURyWykRftm6WpQJVbY,12009 apprise/plugins/NotifyDiscord.py,sha256=M_qmTzB7NNL5_agjYDX38KBN1jRzDBp2EMSNwEF_9Tw,26072 -apprise/plugins/NotifyEmail.py,sha256=q75KtPsvLIaa_0gH4-0ASV4KbE9VKDo3ssj_j7Z-fdk,38284 +apprise/plugins/NotifyEmail.py,sha256=DhAzLFX4pzzuS07QQFcv0VUOYu2PzQE7TTjlPokJcPY,38883 apprise/plugins/NotifyEmby.py,sha256=OMVO8XsVl_XCBYNNNQi8ni2lS4voLfU8Puk1xJOAvHs,24039 apprise/plugins/NotifyEnigma2.py,sha256=Hj0Q9YOeljSwbfiuMKLqXTVX_1g_mjNUGEts7wfrwno,11498 apprise/plugins/NotifyFCM/__init__.py,sha256=mBFtIgIJuLIFnMB5ndx5Makjs9orVMc2oLoD7LaVT48,21669 @@ -111,7 +111,7 @@ apprise/plugins/NotifyLine.py,sha256=OVI0ozMJcq_-dI8dodVX52dzUzgENlAbOik-Kw4l-rI 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 -apprise/plugins/NotifyMacOSX.py,sha256=1LlSjTxkm27btdXCE-rDn2FMGiyZmqlR5-HoXLxK7jM,8227 +apprise/plugins/NotifyMacOSX.py,sha256=y2fGpSZXomFiNwKbWImrXQUMVM4JR4uPCnsWpnxQrFA,8271 apprise/plugins/NotifyMailgun.py,sha256=FNS_QLOQWMo62yVO-mMZkpiXudUtSdbHOjfSrLC4oIo,25409 apprise/plugins/NotifyMastodon.py,sha256=2ovjQIOOITHH8lOinC8QCFCJN2QA8foIM2pjdknbblc,35277 apprise/plugins/NotifyMatrix.py,sha256=I8kdaZUZS-drew0JExBbChQVe7Ib4EwAjQd0xE30XT0,50049 @@ -123,7 +123,7 @@ apprise/plugins/NotifyNextcloudTalk.py,sha256=dLl_g7Knq5PVcadbzDuQsxbGHTZlC4r-pQ apprise/plugins/NotifyNotica.py,sha256=yHmk8HiNFjzoI4Gewo_nBRrx9liEmhT95k1d10wqhYg,12990 apprise/plugins/NotifyNotifiarr.py,sha256=ADwLJO9eenfLkNa09tXMGSBTM4c3zTY0SEePvyB8WYA,15857 apprise/plugins/NotifyNotifico.py,sha256=Qe9jMN_M3GL4XlYIWkAf-w_Hf65g9Hde4bVuytGhUW4,12035 -apprise/plugins/NotifyNtfy.py,sha256=EiG7-z84XibAcWd0iANsU7nZofWEa9xQ6X1z8oc1ZGE,27789 +apprise/plugins/NotifyNtfy.py,sha256=TkDs6jOc30XQn2O2BJ14-nE_cohPdJiSS8DpYXc9hoE,27953 apprise/plugins/NotifyOffice365.py,sha256=8TxsVsdbUghmNj0kceMlmoZzTOKQTgn3priI8JuRuHE,25190 apprise/plugins/NotifyOneSignal.py,sha256=gsw7ckW7xLiJDRUb7eJHNe_4bvdBXmt6_YsB1u_ghjw,18153 apprise/plugins/NotifyOpsgenie.py,sha256=zJWpknjoHq35Iv9w88ucR62odaeIN3nrGFPtYnhDdjA,20515 @@ -142,6 +142,7 @@ apprise/plugins/NotifyPushover.py,sha256=MJDquV4zl1cNrGZOC55hLlt6lOb6625WeUcgS5c apprise/plugins/NotifyPushy.py,sha256=mmWcnu905Fvc8ihYXvZ7lVYErGZH5Q-GbBNS20v5r48,12496 apprise/plugins/NotifyRSyslog.py,sha256=W42LT90X65-pNoU7KdhdX1PBcmsz9RyV376CDa_H3CI,11982 apprise/plugins/NotifyReddit.py,sha256=E78OSyDQfUalBEcg71sdMsNBOwdj7cVBnELrhrZEAXY,25785 +apprise/plugins/NotifyRevolt.py,sha256=DRA9Xylwl6leVjVFuJcP4L1cG49CIBtnQdxh4BKnAZ4,14500 apprise/plugins/NotifyRocketChat.py,sha256=GTEfT-upQ56tJgE0kuc59l4uQGySj_d15wjdcARR9Ko,24624 apprise/plugins/NotifyRyver.py,sha256=yhHPMLGeJtcHwBKSPPk0OBfp59DgTvXio1R59JhrJu4,11823 apprise/plugins/NotifySES.py,sha256=wtRmpAZkS5mQma6sdiaPT6U1xcgoj77CB9mNFvSEAw8,33545 @@ -160,7 +161,7 @@ apprise/plugins/NotifyStreamlabs.py,sha256=lx3N8T2ufUWFYIZ-kU_rOv50YyGWBqLSCKk7x apprise/plugins/NotifySynology.py,sha256=_jTqfgWeOuSi_I8geMOraHBVFtDkvm9mempzymrmeAo,11105 apprise/plugins/NotifySyslog.py,sha256=J9Kain2bb-PDNiG5Ydb0q678cYjNE_NjZFqMG9oEXM0,10617 apprise/plugins/NotifyTechulusPush.py,sha256=m43_Qj1scPcgCRX5Dr2Ul7nxMbaiVxNzm_HRuNmfgoA,7253 -apprise/plugins/NotifyTelegram.py,sha256=km4Izpx0SIP4f__R9_rVjdgUpJCXmM8KX8Tvl3FMqms,35630 +apprise/plugins/NotifyTelegram.py,sha256=Bim4mmPcefHNpvbNSy3pmLuCXRw5IVVWUNUB1SkIhDM,35624 apprise/plugins/NotifyThreema.py,sha256=C_C3j0fJWgeF2uB7ceJFXOdC6Lt0TFBInFMs5Xlg04M,11885 apprise/plugins/NotifyTwilio.py,sha256=WCo8eTI9OF1rtg3ueHHRDXt4Lp45eZ6h3IdTZVf5HM8,15976 apprise/plugins/NotifyTwist.py,sha256=nZA73CYVe-p0tkVMy5q3vFRyflLM4yjUo9LECvkUwgc,28841 diff --git a/libs/apprise-1.7.2.dist-info/REQUESTED b/libs/apprise-1.7.3.dist-info/REQUESTED index e69de29bb..e69de29bb 100644 --- a/libs/apprise-1.7.2.dist-info/REQUESTED +++ b/libs/apprise-1.7.3.dist-info/REQUESTED diff --git a/libs/apprise-1.7.2.dist-info/WHEEL b/libs/apprise-1.7.3.dist-info/WHEEL index ba48cbcf9..ba48cbcf9 100644 --- a/libs/apprise-1.7.2.dist-info/WHEEL +++ b/libs/apprise-1.7.3.dist-info/WHEEL diff --git a/libs/apprise-1.7.2.dist-info/entry_points.txt b/libs/apprise-1.7.3.dist-info/entry_points.txt index 7f20ac9a3..7f20ac9a3 100644 --- a/libs/apprise-1.7.2.dist-info/entry_points.txt +++ b/libs/apprise-1.7.3.dist-info/entry_points.txt diff --git a/libs/apprise-1.7.2.dist-info/top_level.txt b/libs/apprise-1.7.3.dist-info/top_level.txt index 9f8c12a76..9f8c12a76 100644 --- a/libs/apprise-1.7.2.dist-info/top_level.txt +++ b/libs/apprise-1.7.3.dist-info/top_level.txt diff --git a/libs/apprise/URLBase.py b/libs/apprise/URLBase.py index c9f5877f6..2467a4c1e 100644 --- a/libs/apprise/URLBase.py +++ b/libs/apprise/URLBase.py @@ -28,7 +28,7 @@ import re from .logger import logger -from time import sleep +import time from datetime import datetime from xml.sax.saxutils import escape as sax_escape @@ -298,12 +298,12 @@ class URLBase: if wait is not None: self.logger.debug('Throttling forced for {}s...'.format(wait)) - sleep(wait) + time.sleep(wait) elif elapsed < self.request_rate_per_sec: self.logger.debug('Throttling for {}s...'.format( self.request_rate_per_sec - elapsed)) - sleep(self.request_rate_per_sec - elapsed) + time.sleep(self.request_rate_per_sec - elapsed) # Update our timestamp before we leave self._last_io_datetime = datetime.now() diff --git a/libs/apprise/__init__.py b/libs/apprise/__init__.py index 6b9f0394b..c07d769ae 100644 --- a/libs/apprise/__init__.py +++ b/libs/apprise/__init__.py @@ -27,7 +27,7 @@ # POSSIBILITY OF SUCH DAMAGE. __title__ = 'Apprise' -__version__ = '1.7.2' +__version__ = '1.7.3' __author__ = 'Chris Caron' __license__ = 'BSD' __copywrite__ = 'Copyright (C) 2024 Chris Caron <[email protected]>' diff --git a/libs/apprise/cli.py b/libs/apprise/cli.py index 0e16b99f4..59a644272 100644 --- a/libs/apprise/cli.py +++ b/libs/apprise/cli.py @@ -68,20 +68,26 @@ DEFAULT_CONFIG_PATHS = ( # Legacy Path Support '~/.apprise', '~/.apprise.yml', + '~/.apprise.yaml', '~/.config/apprise', '~/.config/apprise.yml', + '~/.config/apprise.yaml', # Plugin Support Extended Directory Search Paths '~/.apprise/apprise', '~/.apprise/apprise.yml', + '~/.apprise/apprise.yaml', '~/.config/apprise/apprise', '~/.config/apprise/apprise.yml', + '~/.config/apprise/apprise.yaml', # Global Configuration Support '/etc/apprise', '/etc/apprise.yml', + '/etc/apprise.yaml', '/etc/apprise/apprise', '/etc/apprise/apprise.yml', + '/etc/apprise/apprise.yaml', ) # Define our paths to search for plugins @@ -99,8 +105,10 @@ if platform.system() == 'Windows': DEFAULT_CONFIG_PATHS = ( expandvars('%APPDATA%\\Apprise\\apprise'), expandvars('%APPDATA%\\Apprise\\apprise.yml'), + expandvars('%APPDATA%\\Apprise\\apprise.yaml'), expandvars('%LOCALAPPDATA%\\Apprise\\apprise'), expandvars('%LOCALAPPDATA%\\Apprise\\apprise.yml'), + expandvars('%LOCALAPPDATA%\\Apprise\\apprise.yaml'), # # Global Support @@ -109,14 +117,17 @@ if platform.system() == 'Windows': # C:\ProgramData\Apprise\ expandvars('%ALLUSERSPROFILE%\\Apprise\\apprise'), expandvars('%ALLUSERSPROFILE%\\Apprise\\apprise.yml'), + expandvars('%ALLUSERSPROFILE%\\Apprise\\apprise.yaml'), # C:\Program Files\Apprise expandvars('%PROGRAMFILES%\\Apprise\\apprise'), expandvars('%PROGRAMFILES%\\Apprise\\apprise.yml'), + expandvars('%PROGRAMFILES%\\Apprise\\apprise.yaml'), # C:\Program Files\Common Files expandvars('%COMMONPROGRAMFILES%\\Apprise\\apprise'), expandvars('%COMMONPROGRAMFILES%\\Apprise\\apprise.yml'), + expandvars('%COMMONPROGRAMFILES%\\Apprise\\apprise.yaml'), ) # Default Plugin Search Path for Windows Users diff --git a/libs/apprise/manager.py b/libs/apprise/manager.py index 3d964af28..d649afab7 100644 --- a/libs/apprise/manager.py +++ b/libs/apprise/manager.py @@ -32,6 +32,7 @@ import sys import time import hashlib import inspect +import threading from .utils import import_module from .utils import Singleton from .utils import parse_list @@ -60,6 +61,9 @@ class PluginManager(metaclass=Singleton): # The module path to scan module_path = join(abspath(dirname(__file__)), _id) + # thread safe loading + _lock = threading.Lock() + def __init__(self, *args, **kwargs): """ Over-ride our class instantiation to provide a singleton @@ -103,40 +107,49 @@ class PluginManager(metaclass=Singleton): # effort/overhead doing it again self._paths_previously_scanned = set() + # Track loaded module paths to prevent from loading them again + self._loaded = set() + def unload_modules(self, disable_native=False): """ Reset our object and unload all modules """ - if self._custom_module_map: - # Handle Custom Module Assignments - for meta in self._custom_module_map.values(): - if meta['name'] not in self._module_map: - # Nothing to remove - continue + with self._lock: + if self._custom_module_map: + # Handle Custom Module Assignments + for meta in self._custom_module_map.values(): + if meta['name'] not in self._module_map: + # Nothing to remove + continue - # For the purpose of tidying up un-used modules in memory - loaded = [m for m in sys.modules.keys() - if m.startswith( - self._module_map[meta['name']]['path'])] + # For the purpose of tidying up un-used modules in memory + loaded = [m for m in sys.modules.keys() + if m.startswith( + self._module_map[meta['name']]['path'])] - for module_path in loaded: - del sys.modules[module_path] + for module_path in loaded: + del sys.modules[module_path] - # Reset disabled plugins (if any) - for schema in self._disabled: - self._schema_map[schema].enabled = True - self._disabled.clear() + # Reset disabled plugins (if any) + for schema in self._disabled: + self._schema_map[schema].enabled = True + self._disabled.clear() - # Reset our variables - self._module_map = None if not disable_native else {} - self._schema_map = {} - self._custom_module_map = {} + # Reset our variables + self._schema_map = {} + self._custom_module_map = {} + if disable_native: + self._module_map = {} - # Reset our path cache - self._paths_previously_scanned = set() + else: + self._module_map = None + self._loaded = set() + + # Reset our path cache + self._paths_previously_scanned = set() - def load_modules(self, path=None, name=None): + def load_modules(self, path=None, name=None, force=False): """ Load our modules into memory """ @@ -145,102 +158,120 @@ class PluginManager(metaclass=Singleton): module_name_prefix = self.module_name_prefix if name is None else name module_path = self.module_path if path is None else path - if not self: - # Initialize our maps - self._module_map = {} - self._schema_map = {} - self._custom_module_map = {} + with self._lock: + if not force and module_path in self._loaded: + # We're done + return - # Used for the detection of additional Notify Services objects - # The .py extension is optional as we support loading directories too - module_re = re.compile( - r'^(?P<name>' + self.fname_prefix + r'[a-z0-9]+)(\.py)?$', re.I) - - t_start = time.time() - for f in os.listdir(module_path): - tl_start = time.time() - match = module_re.match(f) - if not match: - # keep going - continue + # Our base reference + module_count = len(self._module_map) if self._module_map else 0 + schema_count = len(self._schema_map) if self._schema_map else 0 - elif match.group('name') == f'{self.fname_prefix}Base': - # keep going - continue + if not self: + # Initialize our maps + self._module_map = {} + self._schema_map = {} + self._custom_module_map = {} - # Store our notification/plugin name: - module_name = match.group('name') - module_pyname = '{}.{}'.format(module_name_prefix, module_name) + # Used for the detection of additional Notify Services objects + # The .py extension is optional as we support loading directories + # too + module_re = re.compile( + r'^(?P<name>' + self.fname_prefix + r'[a-z0-9]+)(\.py)?$', + re.I) - if module_name in self._module_map: - logger.warning( - "%s(s) (%s) already loaded; ignoring %s", - self.name, module_name, os.path.join(module_path, f)) - continue + t_start = time.time() + for f in os.listdir(module_path): + tl_start = time.time() + match = module_re.match(f) + if not match: + # keep going + continue - try: - module = __import__( - module_pyname, - globals(), locals(), - fromlist=[module_name]) - - except ImportError: - # No problem, we can try again another way... - module = import_module( - os.path.join(module_path, f), module_pyname) - if not module: - # logging found in import_module and not needed here + elif match.group('name') == f'{self.fname_prefix}Base': + # keep going continue - if not hasattr(module, module_name): - # Not a library we can load as it doesn't follow the simple - # rule that the class must bear the same name as the - # notification file itself. - logger.trace( - "%s (%s) import failed; no filename/Class " - "match found in %s", - self.name, module_name, os.path.join(module_path, f)) - continue + # Store our notification/plugin name: + module_name = match.group('name') + module_pyname = '{}.{}'.format(module_name_prefix, module_name) - # Get our plugin - plugin = getattr(module, module_name) - if not hasattr(plugin, 'app_id'): - # Filter out non-notification modules - logger.trace( - "(%s) import failed; no app_id defined in %s", - self.name, module_name, os.path.join(module_path, f)) - continue + if module_name in self._module_map: + logger.warning( + "%s(s) (%s) already loaded; ignoring %s", + self.name, module_name, os.path.join(module_path, f)) + continue - # Add our plugin name to our module map - self._module_map[module_name] = { - 'plugin': set([plugin]), - 'module': module, - 'path': '{}.{}'.format(module_name_prefix, module_name), - 'native': True, - } + try: + module = __import__( + module_pyname, + globals(), locals(), + fromlist=[module_name]) + + except ImportError: + # No problem, we can try again another way... + module = import_module( + os.path.join(module_path, f), module_pyname) + if not module: + # logging found in import_module and not needed here + continue - fn = getattr(plugin, 'schemas', None) - schemas = set([]) if not callable(fn) else fn(plugin) + if not hasattr(module, module_name): + # Not a library we can load as it doesn't follow the simple + # rule that the class must bear the same name as the + # notification file itself. + logger.trace( + "%s (%s) import failed; no filename/Class " + "match found in %s", + self.name, module_name, os.path.join(module_path, f)) + continue - # map our schema to our plugin - for schema in schemas: - if schema in self._schema_map: - logger.error( - "{} schema ({}) mismatch detected - {} to {}" - .format(self.name, schema, self._schema_map, plugin)) + # Get our plugin + plugin = getattr(module, module_name) + if not hasattr(plugin, 'app_id'): + # Filter out non-notification modules + logger.trace( + "(%s) import failed; no app_id defined in %s", + self.name, module_name, os.path.join(module_path, f)) continue - # Assign plugin - self._schema_map[schema] = plugin + # Add our plugin name to our module map + self._module_map[module_name] = { + 'plugin': set([plugin]), + 'module': module, + 'path': '{}.{}'.format(module_name_prefix, module_name), + 'native': True, + } + + fn = getattr(plugin, 'schemas', None) + schemas = set([]) if not callable(fn) else fn(plugin) + + # map our schema to our plugin + for schema in schemas: + if schema in self._schema_map: + logger.error( + "{} schema ({}) mismatch detected - {} to {}" + .format(self.name, schema, self._schema_map, + plugin)) + continue + + # Assign plugin + self._schema_map[schema] = plugin - logger.trace( - '{} {} loaded in {:.6f}s'.format( - self.name, module_name, (time.time() - tl_start))) - logger.debug( - '{} {}(s) and {} Schema(s) loaded in {:.4f}s' - .format( - self.name, len(self._module_map), len(self._schema_map), - (time.time() - t_start))) + logger.trace( + '{} {} loaded in {:.6f}s'.format( + self.name, module_name, (time.time() - tl_start))) + + # Track the directory loaded so we never load it again + self._loaded.add(module_path) + + logger.debug( + '{} {}(s) and {} Schema(s) loaded in {:.4f}s' + .format( + self.name, + len(self._module_map) - module_count, + len(self._schema_map) - schema_count, + (time.time() - t_start))) def module_detection(self, paths, cache=True): """ @@ -334,67 +365,69 @@ class PluginManager(metaclass=Singleton): # end of _import_module() return - 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 + 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) + # 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)) + 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) + self._paths_previously_scanned.add(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: + # 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 - 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) + # Load our module + _import_module(path) - return None + return None def add(self, plugin, schemas=None, url=None, send_func=None): """ @@ -714,4 +747,4 @@ class PluginManager(metaclass=Singleton): """ Determines if object has loaded or not """ - return True if self._module_map is not None else False + return True if self._loaded and self._module_map is not None else False diff --git a/libs/apprise/plugins/NotifyEmail.py b/libs/apprise/plugins/NotifyEmail.py index 5ca422009..e3ecde3f6 100644 --- a/libs/apprise/plugins/NotifyEmail.py +++ b/libs/apprise/plugins/NotifyEmail.py @@ -295,6 +295,21 @@ EMAIL_TEMPLATES = ( }, ), + # Comcast.net + ( + 'Comcast.net', + re.compile( + r'^((?P<label>[^+]+)\+)?(?P<id>[^@]+)@' + r'(?P<domain>(comcast)\.net)$', re.I), + { + 'port': 465, + 'smtp_host': 'smtp.comcast.net', + 'secure': True, + 'secure_mode': SecureMailMode.SSL, + 'login_type': (WebBaseLogin.EMAIL, ) + }, + ), + # Catch All ( 'Custom', @@ -481,34 +496,6 @@ class NotifyEmail(NotifyBase): # addresses from the URL provided self.from_addr = [False, ''] - if self.user and self.host: - # Prepare the bases of our email - self.from_addr = [self.app_id, '{}@{}'.format( - re.split(r'[\s@]+', self.user)[0], - self.host, - )] - - if from_addr: - result = is_email(from_addr) - if result: - self.from_addr = ( - result['name'] if result['name'] else False, - result['full_email']) - else: - self.from_addr[0] = from_addr - - result = is_email(self.from_addr[1]) - if not result: - # Parse Source domain based on from_addr - msg = 'Invalid ~From~ email specified: {}'.format( - '{} <{}>'.format(self.from_addr[0], self.from_addr[1]) - if self.from_addr[0] else '{}'.format(self.from_addr[1])) - self.logger.warning(msg) - raise TypeError(msg) - - # Store our lookup - self.names[self.from_addr[1]] = self.from_addr[0] - # Now detect the SMTP Server self.smtp_host = \ smtp_host if isinstance(smtp_host, str) else '' @@ -528,25 +515,6 @@ class NotifyEmail(NotifyBase): self.logger.warning(msg) raise TypeError(msg) - if targets: - # Validate recipients (to:) and drop bad ones: - for recipient in parse_emails(targets): - result = is_email(recipient) - if result: - self.targets.append( - (result['name'] if result['name'] else False, - result['full_email'])) - continue - - self.logger.warning( - 'Dropped invalid To email ' - '({}) specified.'.format(recipient), - ) - - else: - # If our target email list is empty we want to add ourselves to it - self.targets.append((False, self.from_addr[1])) - # Validate recipients (cc:) and drop bad ones: for recipient in parse_emails(cc): email = is_email(recipient) @@ -598,6 +566,54 @@ class NotifyEmail(NotifyBase): # Apply any defaults based on certain known configurations self.NotifyEmailDefaults(secure_mode=secure_mode, **kwargs) + if self.user and self.host: + # Prepare the bases of our email + self.from_addr = [self.app_id, '{}@{}'.format( + re.split(r'[\s@]+', self.user)[0], + self.host, + )] + + if from_addr: + result = is_email(from_addr) + if result: + self.from_addr = ( + result['name'] if result['name'] else False, + result['full_email']) + else: + # Only update the string but use the already detected info + self.from_addr[0] = from_addr + + result = is_email(self.from_addr[1]) + if not result: + # Parse Source domain based on from_addr + msg = 'Invalid ~From~ email specified: {}'.format( + '{} <{}>'.format(self.from_addr[0], self.from_addr[1]) + if self.from_addr[0] else '{}'.format(self.from_addr[1])) + self.logger.warning(msg) + raise TypeError(msg) + + # Store our lookup + self.names[self.from_addr[1]] = self.from_addr[0] + + if targets: + # Validate recipients (to:) and drop bad ones: + for recipient in parse_emails(targets): + result = is_email(recipient) + if result: + self.targets.append( + (result['name'] if result['name'] else False, + result['full_email'])) + continue + + self.logger.warning( + 'Dropped invalid To email ' + '({}) specified.'.format(recipient), + ) + + else: + # If our target email list is empty we want to add ourselves to it + self.targets.append((False, self.from_addr[1])) + if not self.secure and self.secure_mode != SecureMailMode.INSECURE: # Enable Secure mode if not otherwise set self.secure = True @@ -664,9 +680,7 @@ class NotifyEmail(NotifyBase): # was specified, then we default to having them all set (which # basically implies that there are no restrictions and use use # whatever was specified) - login_type = EMAIL_TEMPLATES[i][2]\ - .get('login_type', []) - + login_type = EMAIL_TEMPLATES[i][2].get('login_type', []) if login_type: # only apply additional logic to our user if a login_type # was specified. @@ -676,6 +690,10 @@ class NotifyEmail(NotifyBase): # not supported; switch it to user id self.user = match.group('id') + else: + # Enforce our host information + self.host = self.user.split('@')[1] + elif WebBaseLogin.USERID not in login_type: # user specified but login type # not supported; switch it to email diff --git a/libs/apprise/plugins/NotifyMacOSX.py b/libs/apprise/plugins/NotifyMacOSX.py index 971951259..dd53369fe 100644 --- a/libs/apprise/plugins/NotifyMacOSX.py +++ b/libs/apprise/plugins/NotifyMacOSX.py @@ -98,6 +98,7 @@ class NotifyMacOSX(NotifyBase): '/usr/local/bin/terminal-notifier', '/usr/bin/terminal-notifier', '/bin/terminal-notifier', + '/opt/local/bin/terminal-notifier', ) # Define object templates diff --git a/libs/apprise/plugins/NotifyMatterMost.py b/libs/apprise/plugins/NotifyMatterMost.py deleted file mode 100644 index 859fed311..000000000 --- a/libs/apprise/plugins/NotifyMatterMost.py +++ /dev/null @@ -1,372 +0,0 @@ -# -*- coding: utf-8 -*- -# BSD 2-Clause License -# -# Apprise - Push Notification Library. -# Copyright (c) 2023, Chris Caron <[email protected]> -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions are met: -# -# 1. Redistributions of source code must retain the above copyright notice, -# this list of conditions and the following disclaimer. -# -# 2. Redistributions in binary form must reproduce the above copyright notice, -# this list of conditions and the following disclaimer in the documentation -# and/or other materials provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE -# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR -# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF -# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS -# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN -# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) -# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. - -# Create an incoming webhook; the website will provide you with something like: -# http://localhost:8065/hooks/yobjmukpaw3r3urc5h6i369yima -# ^^^^^^^^^^^^^^^^^^^^^^^^^^^ -# |-- this is the webhook --| -# -# You can effectively turn the url above to read this: -# mmost://localhost:8065/yobjmukpaw3r3urc5h6i369yima -# - swap http with mmost -# - drop /hooks/ reference - -import requests -from json import dumps - -from .NotifyBase import NotifyBase -from ..common import NotifyImageSize -from ..common import NotifyType -from ..utils import parse_bool -from ..utils import parse_list -from ..utils import validate_regex -from ..AppriseLocale import gettext_lazy as _ - -# Some Reference Locations: -# - https://docs.mattermost.com/developer/webhooks-incoming.html -# - https://docs.mattermost.com/administration/config-settings.html - - -class NotifyMattermost(NotifyBase): - """ - A wrapper for Mattermost Notifications - """ - - # The default descriptive name associated with the Notification - service_name = 'Mattermost' - - # The services URL - service_url = 'https://mattermost.com/' - - # The default protocol - protocol = 'mmost' - - # The default secure protocol - secure_protocol = 'mmosts' - - # A URL that takes you to the setup/help of the specific protocol - setup_url = 'https://github.com/caronc/apprise/wiki/Notify_mattermost' - - # The default Mattermost port - default_port = 8065 - - # Allows the user to specify the NotifyImageSize object - image_size = NotifyImageSize.XY_72 - - # The maximum allowable characters allowed in the body per message - body_maxlen = 4000 - - # Mattermost does not have a title - title_maxlen = 0 - - # Define object templates - templates = ( - '{schema}://{host}/{token}', - '{schema}://{host}:{port}/{token}', - '{schema}://{host}/{fullpath}/{token}', - '{schema}://{host}:{port}/{fullpath}/{token}', - '{schema}://{botname}@{host}/{token}', - '{schema}://{botname}@{host}:{port}/{token}', - '{schema}://{botname}@{host}/{fullpath}/{token}', - '{schema}://{botname}@{host}:{port}/{fullpath}/{token}', - ) - - # Define our template tokens - template_tokens = dict(NotifyBase.template_tokens, **{ - 'host': { - 'name': _('Hostname'), - 'type': 'string', - 'required': True, - }, - 'token': { - 'name': _('Webhook Token'), - 'type': 'string', - 'private': True, - 'required': True, - }, - 'fullpath': { - 'name': _('Path'), - 'type': 'string', - }, - 'botname': { - 'name': _('Bot Name'), - 'type': 'string', - 'map_to': 'user', - }, - 'port': { - 'name': _('Port'), - 'type': 'int', - 'min': 1, - 'max': 65535, - }, - }) - - # Define our template arguments - template_args = dict(NotifyBase.template_args, **{ - 'channels': { - 'name': _('Channels'), - 'type': 'list:string', - }, - 'image': { - 'name': _('Include Image'), - 'type': 'bool', - 'default': True, - 'map_to': 'include_image', - }, - 'to': { - 'alias_of': 'channels', - }, - }) - - def __init__(self, token, fullpath=None, channels=None, - include_image=False, **kwargs): - """ - Initialize Mattermost Object - """ - super().__init__(**kwargs) - - if self.secure: - self.schema = 'https' - - else: - self.schema = 'http' - - # our full path - self.fullpath = '' if not isinstance( - fullpath, str) else fullpath.strip() - - # Authorization Token (associated with project) - self.token = validate_regex(token) - if not self.token: - msg = 'An invalid Mattermost Authorization Token ' \ - '({}) was specified.'.format(token) - self.logger.warning(msg) - raise TypeError(msg) - - # Optional Channels (strip off any channel prefix entries if present) - self.channels = [x.lstrip('#') for x in parse_list(channels)] - - if not self.port: - self.port = self.default_port - - # Place a thumbnail image inline with the message body - self.include_image = include_image - - return - - def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs): - """ - Perform Mattermost Notification - """ - - # Create a copy of our channels, otherwise place a dummy entry - channels = list(self.channels) if self.channels else [None, ] - - headers = { - 'User-Agent': self.app_id, - 'Content-Type': 'application/json' - } - - # prepare JSON Object - payload = { - 'text': body, - 'icon_url': None, - } - - # Acquire our image url if configured to do so - image_url = None if not self.include_image \ - else self.image_url(notify_type) - - if image_url: - # Set our image configuration if told to do so - payload['icon_url'] = image_url - - # Set our user - payload['username'] = self.user if self.user else self.app_id - - # For error tracking - has_error = False - - while len(channels): - # Pop a channel off of the list - channel = channels.pop(0) - - if channel: - payload['channel'] = channel - - url = '{}://{}:{}{}/hooks/{}'.format( - self.schema, self.host, self.port, self.fullpath, - self.token) - - self.logger.debug('Mattermost POST URL: %s (cert_verify=%r)' % ( - url, self.verify_certificate, - )) - self.logger.debug('Mattermost Payload: %s' % str(payload)) - - # Always call throttle before any remote server i/o is made - self.throttle() - - try: - r = requests.post( - url, - data=dumps(payload), - headers=headers, - verify=self.verify_certificate, - timeout=self.request_timeout, - ) - - if r.status_code != requests.codes.ok: - # We had a problem - status_str = \ - NotifyMattermost.http_response_code_lookup( - r.status_code) - - self.logger.warning( - 'Failed to send Mattermost notification{}: ' - '{}{}error={}.'.format( - '' if not channel - else ' to channel {}'.format(channel), - status_str, - ', ' if status_str else '', - r.status_code)) - - self.logger.debug( - 'Response Details:\r\n{}'.format(r.content)) - - # Flag our error - has_error = True - continue - - else: - self.logger.info( - 'Sent Mattermost notification{}.'.format( - '' if not channel - else ' to channel {}'.format(channel))) - - except requests.RequestException as e: - self.logger.warning( - 'A Connection error occurred sending Mattermost ' - 'notification{}.'.format( - '' if not channel - else ' to channel {}'.format(channel))) - self.logger.debug('Socket Exception: %s' % str(e)) - - # Flag our error - has_error = True - continue - - # Return our overall status - return not has_error - - def url(self, privacy=False, *args, **kwargs): - """ - Returns the URL built dynamically based on specified arguments. - """ - - # Define any URL parameters - params = { - 'image': 'yes' if self.include_image else 'no', - } - - # Extend our parameters - params.update(self.url_parameters(privacy=privacy, *args, **kwargs)) - - if self.channels: - # historically the value only accepted one channel and is - # therefore identified as 'channel'. Channels have always been - # optional, so that is why this setting is nested in an if block - params['channel'] = ','.join( - [NotifyMattermost.quote(x, safe='') for x in self.channels]) - - default_port = 443 if self.secure else self.default_port - default_schema = self.secure_protocol if self.secure else self.protocol - - # Determine if there is a botname present - botname = '' - if self.user: - botname = '{botname}@'.format( - botname=NotifyMattermost.quote(self.user, safe=''), - ) - - return \ - '{schema}://{botname}{hostname}{port}{fullpath}{token}' \ - '/?{params}'.format( - schema=default_schema, - botname=botname, - # never encode hostname since we're expecting it to be a valid - # one - hostname=self.host, - port='' if not self.port or self.port == default_port - else ':{}'.format(self.port), - fullpath='/' if not self.fullpath else '{}/'.format( - NotifyMattermost.quote(self.fullpath, safe='/')), - token=self.pprint(self.token, privacy, safe=''), - params=NotifyMattermost.urlencode(params), - ) - - @staticmethod - def parse_url(url): - """ - Parses the URL and returns enough arguments that can allow - us to re-instantiate this object. - - """ - results = NotifyBase.parse_url(url) - if not results: - # We're done early as we couldn't load the results - return results - - # Acquire our tokens; the last one will always be our token - # all entries before it will be our path - tokens = NotifyMattermost.split_path(results['fullpath']) - - results['token'] = None if not tokens else tokens.pop() - - # Store our path - results['fullpath'] = '' if not tokens \ - else '/{}'.format('/'.join(tokens)) - - # Define our optional list of channels to notify - results['channels'] = list() - - # Support both 'to' (for yaml configuration) and channel= - if 'to' in results['qsd'] and len(results['qsd']['to']): - # Allow the user to specify the channel to post to - results['channels'].append( - NotifyMattermost.parse_list(results['qsd']['to'])) - - if 'channel' in results['qsd'] and len(results['qsd']['channel']): - # Allow the user to specify the channel to post to - results['channels'].append( - NotifyMattermost.parse_list(results['qsd']['channel'])) - - # Image manipulation - results['include_image'] = \ - parse_bool(results['qsd'].get('image', False)) - - return results diff --git a/libs/apprise/plugins/NotifyMattermost.py b/libs/apprise/plugins/NotifyMattermost.py index 859fed311..dbb5f0dd3 100644 --- a/libs/apprise/plugins/NotifyMattermost.py +++ b/libs/apprise/plugins/NotifyMattermost.py @@ -2,7 +2,7 @@ # BSD 2-Clause License # # Apprise - Push Notification Library. -# Copyright (c) 2023, Chris Caron <[email protected]> +# Copyright (c) 2024, Chris Caron <[email protected]> # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: diff --git a/libs/apprise/plugins/NotifyNtfy.py b/libs/apprise/plugins/NotifyNtfy.py index cc705c6cc..138c3fca7 100644 --- a/libs/apprise/plugins/NotifyNtfy.py +++ b/libs/apprise/plugins/NotifyNtfy.py @@ -42,6 +42,7 @@ from json import dumps from os.path import basename from .NotifyBase import NotifyBase +from ..common import NotifyFormat from ..common import NotifyType from ..common import NotifyImageSize from ..AppriseLocale import gettext_lazy as _ @@ -515,6 +516,10 @@ class NotifyNtfy(NotifyBase): if body: virt_payload['message'] = body + if self.notify_format == NotifyFormat.MARKDOWN: + # Support Markdown + headers['X-Markdown'] = 'yes' + if self.priority != NtfyPriority.NORMAL: headers['X-Priority'] = self.priority diff --git a/libs/apprise/plugins/NotifyRevolt.py b/libs/apprise/plugins/NotifyRevolt.py new file mode 100644 index 000000000..ae0a43b10 --- /dev/null +++ b/libs/apprise/plugins/NotifyRevolt.py @@ -0,0 +1,437 @@ +# -*- coding: utf-8 -*- +# BSD 2-Clause License +# +# Apprise - Push Notification Library. +# Copyright (c) 2024, Chris Caron <[email protected]> +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# +# 2. Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. + +# Youll need your own Revolt Bot and a Channel Id for the notifications to +# be sent in since Revolt does not support webhooks yet. +# +# This plugin will simply work using the url of: +# revolt://BOT_TOKEN/CHANNEL_ID +# +# API Documentation: +# - https://api.revolt.chat/swagger/index.html +# + +import requests +from json import dumps, loads +from datetime import timedelta +from datetime import datetime +from datetime import timezone + +from .NotifyBase import NotifyBase +from ..common import NotifyImageSize +from ..common import NotifyFormat +from ..common import NotifyType +from ..utils import validate_regex +from ..utils import parse_list +from ..AppriseLocale import gettext_lazy as _ + + +class NotifyRevolt(NotifyBase): + """ + A wrapper for Revolt Notifications + + """ + # The default descriptive name associated with the Notification + service_name = 'Revolt' + + # The services URL + service_url = 'https://revolt.chat/' + + # The default secure protocol + secure_protocol = 'revolt' + + # A URL that takes you to the setup/help of the specific protocol + setup_url = 'https://github.com/caronc/apprise/wiki/Notify_revolt' + + # Revolt Channel Message + notify_url = 'https://api.revolt.chat/' + + # Revolt supports attachments but doesn't support it here (for now) + attachment_support = False + + # Allows the user to specify the NotifyImageSize object + image_size = NotifyImageSize.XY_256 + + # Revolt is kind enough to return how many more requests we're allowed to + # continue to make within it's header response as: + # X-RateLimit-Reset: The epoc time (in seconds) we can expect our + # rate-limit to be reset. + # X-RateLimit-Remaining: an integer identifying how many requests we're + # still allow to make. + request_rate_per_sec = 3 + + # Safety net + clock_skew = timedelta(seconds=2) + + # The maximum allowable characters allowed in the body per message + body_maxlen = 2000 + + # Title Maximum Length + title_maxlen = 100 + + # Define object templates + templates = ( + '{schema}://{bot_token}/{targets}', + ) + + # Defile out template tokens + template_tokens = dict(NotifyBase.template_tokens, **{ + 'bot_token': { + 'name': _('Bot Token'), + 'type': 'string', + 'private': True, + 'required': True, + }, + 'target_channel': { + 'name': _('Channel ID'), + 'type': 'string', + 'map_to': 'targets', + 'regex': (r'^[a-z0-9_-]+$', 'i'), + 'private': True, + 'required': True, + }, + 'targets': { + 'name': _('Targets'), + 'type': 'list:string', + }, + }) + + # Define our template arguments + template_args = dict(NotifyBase.template_args, **{ + 'channel': { + 'alias_of': 'targets', + }, + 'bot_token': { + 'alias_of': 'bot_token', + }, + 'icon_url': { + 'name': _('Icon URL'), + 'type': 'string' + }, + 'url': { + 'name': _('Embed URL'), + 'type': 'string', + 'map_to': 'link', + }, + 'to': { + 'alias_of': 'targets', + }, + }) + + def __init__(self, bot_token, targets, icon_url=None, link=None, + **kwargs): + super().__init__(**kwargs) + + # Bot Token + self.bot_token = validate_regex(bot_token) + if not self.bot_token: + msg = 'An invalid Revolt Bot Token ' \ + '({}) was specified.'.format(bot_token) + self.logger.warning(msg) + raise TypeError(msg) + + # Parse our Channel IDs + self.targets = [] + for target in parse_list(targets): + results = validate_regex( + target, *self.template_tokens['target_channel']['regex']) + + if not results: + self.logger.warning( + 'Dropped invalid Revolt channel ({}) specified.' + .format(target), + ) + continue + + # Add our target + self.targets.append(target) + + # Image for Embed + self.icon_url = icon_url + + # Url for embed title + self.link = link + + # For Tracking Purposes + self.ratelimit_reset = datetime.now(timezone.utc).replace(tzinfo=None) + + # Default to 1.0 + self.ratelimit_remaining = 1.0 + + return + + def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs): + """ + Perform Revolt Notification + + """ + + if len(self.targets) == 0: + self.logger.warning('There were not Revolt channels to notify.') + return False + + payload = {} + + # Acquire image_url + image_url = self.icon_url \ + if self.icon_url else self.image_url(notify_type) + + if self.notify_format == NotifyFormat.MARKDOWN: + payload['embeds'] = [{ + 'title': None if not title else title[0:self.title_maxlen], + 'description': body, + + # Our color associated with our notification + 'colour': self.color(notify_type), + 'replies': None + }] + + if image_url: + payload['embeds'][0]['icon_url'] = image_url + + if self.link: + payload['embeds'][0]['url'] = self.link + + else: + payload['content'] = \ + body if not title else "{}\n{}".format(title, body) + + has_error = False + channel_ids = list(self.targets) + for channel_id in channel_ids: + postokay, response = self._send(payload, channel_id) + if not postokay: + # Failed to send message + has_error = True + + return not has_error + + def _send(self, payload, channel_id, retries=1, **kwargs): + """ + Wrapper to the requests (post) object + + """ + + headers = { + 'User-Agent': self.app_id, + 'X-Bot-Token': self.bot_token, + 'Content-Type': 'application/json; charset=utf-8', + 'Accept': 'application/json; charset=utf-8', + } + + notify_url = '{0}channels/{1}/messages'.format( + self.notify_url, + channel_id + ) + + self.logger.debug('Revolt POST URL: %s (cert_verify=%r)' % ( + notify_url, self.verify_certificate + )) + self.logger.debug('Revolt Payload: %s' % str(payload)) + + # By default set wait to None + wait = None + + now = datetime.now(timezone.utc).replace(tzinfo=None) + if self.ratelimit_remaining <= 0.0: + # Determine how long we should wait for or if we should wait at + # all. This isn't fool-proof because we can't be sure the client + # time (calling this script) is completely synced up with the + # Discord server. One would hope we're on NTP and our clocks are + # the same allowing this to role smoothly: + if now < self.ratelimit_reset: + # We need to throttle for the difference in seconds + wait = abs( + (self.ratelimit_reset - now + self.clock_skew) + .total_seconds()) + + # Default content response object + content = {} + + # Always call throttle before any remote server i/o is made; + self.throttle(wait=wait) + + try: + r = requests.post( + notify_url, + data=dumps(payload), + headers=headers, + verify=self.verify_certificate, + timeout=self.request_timeout + ) + + try: + content = loads(r.content) + + except (AttributeError, TypeError, ValueError): + # ValueError = r.content is Unparsable + # TypeError = r.content is None + # AttributeError = r is None + content = {} + + # Handle rate limiting (if specified) + try: + # Store our rate limiting (if provided) + self.ratelimit_remaining = \ + int(r.headers.get('X-RateLimit-Remaining')) + self.ratelimit_reset = \ + now + timedelta(seconds=(int( + r.headers.get('X-RateLimit-Reset-After')) / 1000)) + + except (TypeError, ValueError): + # This is returned if we could not retrieve this + # information gracefully accept this state and move on + pass + + if r.status_code not in ( + requests.codes.ok, requests.codes.no_content): + + # Some details to debug by + self.logger.debug('Response Details:\r\n{}'.format( + content if content else r.content)) + + # We had a problem + status_str = \ + NotifyBase.http_response_code_lookup(r.status_code) + + self.logger.warning( + 'Revolt request limit reached; ' + 'instructed to throttle for %.3fs', + abs((self.ratelimit_reset - now + self.clock_skew) + .total_seconds())) + + if r.status_code == requests.codes.too_many_requests \ + and retries > 0: + + # Try again + return self._send( + payload=payload, channel_id=channel_id, + retries=retries - 1, **kwargs) + + self.logger.warning( + 'Failed to send to Revolt notification: ' + '{}{}error={}.'.format( + status_str, + ', ' if status_str else '', + r.status_code)) + + # Return; we're done + return (False, content) + + else: + self.logger.info('Sent Revolt notification.') + + except requests.RequestException as e: + self.logger.warning( + 'A Connection error occurred posting to Revolt.') + self.logger.debug('Socket Exception: %s' % str(e)) + return (False, content) + + return (True, content) + + def url(self, privacy=False, *args, **kwargs): + """ + Returns the URL built dynamically based on specified arguments. + + """ + + # Define any URL parameters + params = {} + + if self.icon_url: + params['icon_url'] = self.icon_url + + if self.link: + params['url'] = self.link + + params.update(self.url_parameters(privacy=privacy, *args, **kwargs)) + + return '{schema}://{bot_token}/{targets}/?{params}'.format( + schema=self.secure_protocol, + bot_token=self.pprint(self.bot_token, privacy, safe=''), + targets='/'.join( + [self.pprint(x, privacy, safe='') for x in self.targets]), + params=NotifyRevolt.urlencode(params), + ) + + def __len__(self): + """ + Returns the number of targets associated with this notification + """ + 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 + + # Store our bot token + bot_token = NotifyRevolt.unquote(results['host']) + + # Now fetch the Channel IDs + targets = NotifyRevolt.split_path(results['fullpath']) + + results['bot_token'] = bot_token + results['targets'] = targets + + # Support the 'to' variable so that we can support rooms this way too + # The 'to' makes it easier to use yaml configuration + if 'to' in results['qsd'] and len(results['qsd']['to']): + results['targets'] += \ + NotifyRevolt.parse_list(results['qsd']['to']) + + # Support channel id on the URL string (if specified) + if 'channel' in results['qsd']: + results['targets'] += \ + NotifyRevolt.parse_list(results['qsd']['channel']) + + # Support bot token on the URL string (if specified) + if 'bot_token' in results['qsd']: + results['bot_token'] = \ + NotifyRevolt.unquote(results['qsd']['bot_token']) + + if 'icon_url' in results['qsd']: + results['icon_url'] = \ + NotifyRevolt.unquote(results['qsd']['icon_url']) + + if 'url' in results['qsd']: + results['link'] = NotifyRevolt.unquote(results['qsd']['url']) + + if 'format' not in results['qsd'] and ( + 'url' in results or 'icon_url' in results): + # Markdown is implied + results['format'] = NotifyFormat.MARKDOWN + + return results diff --git a/libs/apprise/plugins/NotifyTelegram.py b/libs/apprise/plugins/NotifyTelegram.py index 03bc36c6f..dbea79b1a 100644 --- a/libs/apprise/plugins/NotifyTelegram.py +++ b/libs/apprise/plugins/NotifyTelegram.py @@ -297,7 +297,6 @@ class NotifyTelegram(NotifyBase): 'name': _('Target Chat ID'), 'type': 'string', 'map_to': 'targets', - 'map_to': 'targets', 'regex': (r'^((-?[0-9]{1,32})|([a-z_-][a-z0-9_-]+))$', 'i'), }, 'targets': { @@ -916,7 +915,7 @@ class NotifyTelegram(NotifyBase): """ Returns the number of targets associated with this notification """ - return len(self.targets) + return 1 if not self.targets else len(self.targets) @staticmethod def parse_url(url): diff --git a/libs/version.txt b/libs/version.txt index 67d61dd2c..15e8a7e0d 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.2 +apprise==1.7.3 apscheduler<=3.10.4 attrs==23.2.0 blinker==1.7.0 |