Updated apprise to version 1.7.6

This commit is contained in:
morpheus65535 2024-04-15 15:00:10 -04:00
parent b2d807d9d9
commit 7578b8ef14
28 changed files with 977 additions and 313 deletions

View File

@ -1,12 +1,12 @@
Metadata-Version: 2.1 Metadata-Version: 2.1
Name: apprise Name: apprise
Version: 1.7.4 Version: 1.7.6
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 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 Keywords: Alerts Apprise API Automated Packet Reporting System AWS Boxcar BulkSMS BulkVS Burst SMS Chantify Chat CLI ClickSend D7Networks Dapnet DBus DingTalk Discord Email Emby Enigma2 FCM Feishu Flock Form Free Mobile 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
@ -20,6 +20,7 @@ Classifier: Programming Language :: Python :: 3.8
Classifier: Programming Language :: Python :: 3.9 Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10 Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11 Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: Implementation :: CPython Classifier: Programming Language :: Python :: Implementation :: CPython
Classifier: Programming Language :: Python :: Implementation :: PyPy Classifier: Programming Language :: Python :: Implementation :: PyPy
Classifier: License :: OSI Approved :: BSD License Classifier: License :: OSI Approved :: BSD License
@ -98,11 +99,12 @@ The table below identifies the services this tool supports and some example serv
| [AWS SES](https://github.com/caronc/apprise/wiki/Notify_ses) | ses:// | (TCP) 443 | ses://user@domain/AccessKeyID/AccessSecretKey/RegionName<br/>ses://user@domain/AccessKeyID/AccessSecretKey/RegionName/email1/email2/emailN | [AWS SES](https://github.com/caronc/apprise/wiki/Notify_ses) | ses:// | (TCP) 443 | ses://user@domain/AccessKeyID/AccessSecretKey/RegionName<br/>ses://user@domain/AccessKeyID/AccessSecretKey/RegionName/email1/email2/emailN
| [Bark](https://github.com/caronc/apprise/wiki/Notify_bark) | bark:// | (TCP) 80 or 443 | bark://hostname<br />bark://hostname/device_key<br />bark://hostname/device_key1/device_key2/device_keyN<br/>barks://hostname<br />barks://hostname/device_key<br />barks://hostname/device_key1/device_key2/device_keyN | [Bark](https://github.com/caronc/apprise/wiki/Notify_bark) | bark:// | (TCP) 80 or 443 | bark://hostname<br />bark://hostname/device_key<br />bark://hostname/device_key1/device_key2/device_keyN<br/>barks://hostname<br />barks://hostname/device_key<br />barks://hostname/device_key1/device_key2/device_keyN
| [Boxcar](https://github.com/caronc/apprise/wiki/Notify_boxcar) | boxcar:// | (TCP) 443 | boxcar://hostname<br />boxcar://hostname/@tag<br/>boxcar://hostname/device_token<br />boxcar://hostname/device_token1/device_token2/device_tokenN<br />boxcar://hostname/@tag/@tag2/device_token | [Boxcar](https://github.com/caronc/apprise/wiki/Notify_boxcar) | boxcar:// | (TCP) 443 | boxcar://hostname<br />boxcar://hostname/@tag<br/>boxcar://hostname/device_token<br />boxcar://hostname/device_token1/device_token2/device_tokenN<br />boxcar://hostname/@tag/@tag2/device_token
| [Chantify](https://github.com/caronc/apprise/wiki/Notify_chantify) | chantify:// | (TCP) 443 | chantify://token
| [Discord](https://github.com/caronc/apprise/wiki/Notify_discord) | discord:// | (TCP) 443 | discord://webhook_id/webhook_token<br />discord://avatar@webhook_id/webhook_token | [Discord](https://github.com/caronc/apprise/wiki/Notify_discord) | discord:// | (TCP) 443 | discord://webhook_id/webhook_token<br />discord://avatar@webhook_id/webhook_token
| [Emby](https://github.com/caronc/apprise/wiki/Notify_emby) | emby:// or embys:// | (TCP) 8096 | emby://user@hostname/<br />emby://user:password@hostname | [Emby](https://github.com/caronc/apprise/wiki/Notify_emby) | emby:// or embys:// | (TCP) 8096 | emby://user@hostname/<br />emby://user:password@hostname
| [Enigma2](https://github.com/caronc/apprise/wiki/Notify_enigma2) | enigma2:// or enigma2s:// | (TCP) 80 or 443 | enigma2://hostname | [Enigma2](https://github.com/caronc/apprise/wiki/Notify_enigma2) | enigma2:// or enigma2s:// | (TCP) 80 or 443 | enigma2://hostname
| [Faast](https://github.com/caronc/apprise/wiki/Notify_faast) | faast:// | (TCP) 443 | faast://authorizationtoken
| [FCM](https://github.com/caronc/apprise/wiki/Notify_fcm) | fcm:// | (TCP) 443 | fcm://project@apikey/DEVICE_ID<br />fcm://project@apikey/#TOPIC<br/>fcm://project@apikey/DEVICE_ID1/#topic1/#topic2/DEVICE_ID2/ | [FCM](https://github.com/caronc/apprise/wiki/Notify_fcm) | fcm:// | (TCP) 443 | fcm://project@apikey/DEVICE_ID<br />fcm://project@apikey/#TOPIC<br/>fcm://project@apikey/DEVICE_ID1/#topic1/#topic2/DEVICE_ID2/
| [Feishu](https://github.com/caronc/apprise/wiki/Notify_feishu) | feishu:// | (TCP) 443 | feishu://token
| [Flock](https://github.com/caronc/apprise/wiki/Notify_flock) | flock:// | (TCP) 443 | flock://token<br/>flock://botname@token<br/>flock://app_token/u:userid<br/>flock://app_token/g:channel_id<br/>flock://app_token/u:userid/g:channel_id | [Flock](https://github.com/caronc/apprise/wiki/Notify_flock) | flock:// | (TCP) 443 | flock://token<br/>flock://botname@token<br/>flock://app_token/u:userid<br/>flock://app_token/g:channel_id<br/>flock://app_token/u:userid/g:channel_id
| [Google Chat](https://github.com/caronc/apprise/wiki/Notify_googlechat) | gchat:// | (TCP) 443 | gchat://workspace/key/token | [Google Chat](https://github.com/caronc/apprise/wiki/Notify_googlechat) | gchat:// | (TCP) 443 | gchat://workspace/key/token
| [Gotify](https://github.com/caronc/apprise/wiki/Notify_gotify) | gotify:// or gotifys:// | (TCP) 80 or 443 | gotify://hostname/token<br />gotifys://hostname/token?priority=high | [Gotify](https://github.com/caronc/apprise/wiki/Notify_gotify) | gotify:// or gotifys:// | (TCP) 80 or 443 | gotify://hostname/token<br />gotifys://hostname/token?priority=high
@ -184,6 +186,7 @@ The table below identifies the services this tool supports and some example serv
| [DAPNET](https://github.com/caronc/apprise/wiki/Notify_dapnet) | dapnet:// | (TCP) 80 | dapnet://user:pass@callsign<br/>dapnet://user:pass@callsign1/callsign2/callsignN | [DAPNET](https://github.com/caronc/apprise/wiki/Notify_dapnet) | dapnet:// | (TCP) 80 | dapnet://user:pass@callsign<br/>dapnet://user:pass@callsign1/callsign2/callsignN
| [D7 Networks](https://github.com/caronc/apprise/wiki/Notify_d7networks) | d7sms:// | (TCP) 443 | d7sms://token@PhoneNo<br/>d7sms://token@ToPhoneNo1/ToPhoneNo2/ToPhoneNoN | [D7 Networks](https://github.com/caronc/apprise/wiki/Notify_d7networks) | d7sms:// | (TCP) 443 | d7sms://token@PhoneNo<br/>d7sms://token@ToPhoneNo1/ToPhoneNo2/ToPhoneNoN
| [DingTalk](https://github.com/caronc/apprise/wiki/Notify_dingtalk) | dingtalk:// | (TCP) 443 | dingtalk://token/<br />dingtalk://token/ToPhoneNo<br />dingtalk://token/ToPhoneNo1/ToPhoneNo2/ToPhoneNo1/ | [DingTalk](https://github.com/caronc/apprise/wiki/Notify_dingtalk) | dingtalk:// | (TCP) 443 | dingtalk://token/<br />dingtalk://token/ToPhoneNo<br />dingtalk://token/ToPhoneNo1/ToPhoneNo2/ToPhoneNo1/
| [Free-Mobile](https://github.com/caronc/apprise/wiki/Notify_freemobile) | freemobile:// | (TCP) 443 | freemobile://user@password/
[httpSMS](https://github.com/caronc/apprise/wiki/Notify_httpsms) | httpsms:// | (TCP) 443 | httpsms://ApiKey@FromPhoneNo<br/>httpsms://ApiKey@FromPhoneNo/ToPhoneNo<br/>httpsms://ApiKey@FromPhoneNo/ToPhoneNo1/ToPhoneNo2/ToPhoneNoN/ [httpSMS](https://github.com/caronc/apprise/wiki/Notify_httpsms) | httpsms:// | (TCP) 443 | httpsms://ApiKey@FromPhoneNo<br/>httpsms://ApiKey@FromPhoneNo/ToPhoneNo<br/>httpsms://ApiKey@FromPhoneNo/ToPhoneNo1/ToPhoneNo2/ToPhoneNoN/
| [Kavenegar](https://github.com/caronc/apprise/wiki/Notify_kavenegar) | kavenegar:// | (TCP) 443 | kavenegar://ApiKey/ToPhoneNo<br/>kavenegar://FromPhoneNo@ApiKey/ToPhoneNo<br/>kavenegar://ApiKey/ToPhoneNo1/ToPhoneNo2/ToPhoneNoN | [Kavenegar](https://github.com/caronc/apprise/wiki/Notify_kavenegar) | kavenegar:// | (TCP) 443 | kavenegar://ApiKey/ToPhoneNo<br/>kavenegar://FromPhoneNo@ApiKey/ToPhoneNo<br/>kavenegar://ApiKey/ToPhoneNo1/ToPhoneNo2/ToPhoneNoN
| [MessageBird](https://github.com/caronc/apprise/wiki/Notify_messagebird) | msgbird:// | (TCP) 443 | msgbird://ApiKey/FromPhoneNo<br/>msgbird://ApiKey/FromPhoneNo/ToPhoneNo<br/>msgbird://ApiKey/FromPhoneNo/ToPhoneNo1/ToPhoneNo2/ToPhoneNoN/ | [MessageBird](https://github.com/caronc/apprise/wiki/Notify_messagebird) | msgbird:// | (TCP) 443 | msgbird://ApiKey/FromPhoneNo<br/>msgbird://ApiKey/FromPhoneNo/ToPhoneNo<br/>msgbird://ApiKey/FromPhoneNo/ToPhoneNo1/ToPhoneNo2/ToPhoneNoN/

View File

@ -1,12 +1,12 @@
../../bin/apprise,sha256=ZJ-e4qqxNLtdW_DAvpuPPX5iROIiQd8I6nvg7vtAv-g,233 ../../bin/apprise,sha256=ZJ-e4qqxNLtdW_DAvpuPPX5iROIiQd8I6nvg7vtAv-g,233
apprise-1.7.4.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 apprise-1.7.6.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
apprise-1.7.4.dist-info/LICENSE,sha256=gt7qKBxRhVcdmXCYVtrWP6DtYjD0DzONet600dkU994,1343 apprise-1.7.6.dist-info/LICENSE,sha256=gt7qKBxRhVcdmXCYVtrWP6DtYjD0DzONet600dkU994,1343
apprise-1.7.4.dist-info/METADATA,sha256=Lc66iPsSCFv0zmoQX8NFuc_V5CqFYN5Yrx_gqeN8OF8,44502 apprise-1.7.6.dist-info/METADATA,sha256=z_gaX2IdNJqw4T9q7AYQri9jcIs-OTGCo3t2EgEY-mw,44823
apprise-1.7.4.dist-info/RECORD,, apprise-1.7.6.dist-info/RECORD,,
apprise-1.7.4.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 apprise-1.7.6.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
apprise-1.7.4.dist-info/WHEEL,sha256=Xo9-1PvkuimrydujYJAjF7pCkriuXBpUPEjma1nZyJ0,92 apprise-1.7.6.dist-info/WHEEL,sha256=Xo9-1PvkuimrydujYJAjF7pCkriuXBpUPEjma1nZyJ0,92
apprise-1.7.4.dist-info/entry_points.txt,sha256=71YypBuNdjAKiaLsiMG40HEfLHxkU4Mi7o_S0s0d8wI,45 apprise-1.7.6.dist-info/entry_points.txt,sha256=71YypBuNdjAKiaLsiMG40HEfLHxkU4Mi7o_S0s0d8wI,45
apprise-1.7.4.dist-info/top_level.txt,sha256=JrCRn-_rXw5LMKXkIgMSE4E0t1Ks9TYrBH54Pflwjkk,8 apprise-1.7.6.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
@ -15,13 +15,13 @@ apprise/AppriseAttachment.py,sha256=vhrktSrp8GLr32aK4KqV6BX83IpI1lxZe-pGo1wiSFM,
apprise/AppriseAttachment.pyi,sha256=R9-0dVqWpeaFrVpcREwPhGy3qHWztG5jEjYIOsbE5dM,1145 apprise/AppriseAttachment.pyi,sha256=R9-0dVqWpeaFrVpcREwPhGy3qHWztG5jEjYIOsbE5dM,1145
apprise/AppriseConfig.py,sha256=wfuR6Mb3ZLHvjvqWdFp9lVmjjDRWs65unY9qa92RkCg,16909 apprise/AppriseConfig.py,sha256=wfuR6Mb3ZLHvjvqWdFp9lVmjjDRWs65unY9qa92RkCg,16909
apprise/AppriseConfig.pyi,sha256=_mUlCnncqAq8sL01WxQTgZjnb2ic9kZXvtqZmVl-fc8,1568 apprise/AppriseConfig.pyi,sha256=_mUlCnncqAq8sL01WxQTgZjnb2ic9kZXvtqZmVl-fc8,1568
apprise/AppriseLocale.py,sha256=ISth7xC7M1WhsSNXdGZFouaA4bi07KP35m9RX-ExG48,8852 apprise/AppriseLocale.py,sha256=4uSr4Nj_rz6ISMMAfRVRk58wZVLKOofJgk2x0_E8NkQ,8994
apprise/AttachmentManager.py,sha256=EwlnjuKn3fv_pioWcmMCkyDTsO178t6vkEOD8AjAPsw,2053 apprise/AttachmentManager.py,sha256=EwlnjuKn3fv_pioWcmMCkyDTsO178t6vkEOD8AjAPsw,2053
apprise/ConfigurationManager.py,sha256=MUmGajxjgnr6FGN7xb3q0nD0VVgdTdvapBBR7CsI-rc,2058 apprise/ConfigurationManager.py,sha256=MUmGajxjgnr6FGN7xb3q0nD0VVgdTdvapBBR7CsI-rc,2058
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=xRP0-blocp9UudYh04Hb3fIEmTZWJaTv_tzjrqaB9fg,29423
apprise/URLBase.pyi,sha256=WLaRREH7FzZ5x3-qkDkupojWGFC4uFwJ1EDt02lVs8c,520 apprise/URLBase.pyi,sha256=WLaRREH7FzZ5x3-qkDkupojWGFC4uFwJ1EDt02lVs8c,520
apprise/__init__.py,sha256=oBHq9Zbcwz9DTkurqnEhbu9Q79a0TdVAZrWFIhlk__8,3368 apprise/__init__.py,sha256=ArtvoarAMnBcSfXF7L_hzq5CUJ9TUnHopiC7xafCe3c,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
@ -45,22 +45,22 @@ apprise/assets/themes/default/apprise-warning-128x128.png,sha256=pf5c4Ph7jWH7gf3
apprise/assets/themes/default/apprise-warning-256x256.png,sha256=SY-xlaiXaj420iEYKC2_fJxU-yj2SuaQg6xfPNi83bw,43708 apprise/assets/themes/default/apprise-warning-256x256.png,sha256=SY-xlaiXaj420iEYKC2_fJxU-yj2SuaQg6xfPNi83bw,43708
apprise/assets/themes/default/apprise-warning-32x32.png,sha256=97R2ywNvcwczhBoWEIgajVtWjgT8fLs4FCCz4wu0dwc,2472 apprise/assets/themes/default/apprise-warning-32x32.png,sha256=97R2ywNvcwczhBoWEIgajVtWjgT8fLs4FCCz4wu0dwc,2472
apprise/assets/themes/default/apprise-warning-72x72.png,sha256=L8moEInkO_OLxoOcuvN7rmrGZo64iJeH20o-24MQghE,7913 apprise/assets/themes/default/apprise-warning-72x72.png,sha256=L8moEInkO_OLxoOcuvN7rmrGZo64iJeH20o-24MQghE,7913
apprise/attachment/AttachBase.py,sha256=ik3hRFnr8Z9bXt69P9Ej1VST4gQbnE0C_9WQvEE-72A,13592 apprise/attachment/AttachBase.py,sha256=T3WreGrTsqqGplXJO36jm-N14X7ymSc9xt7XdTYuXVE,13656
apprise/attachment/AttachBase.pyi,sha256=w0XG_QKauiMLJ7eQ4S57IiLIURZHm_Snw7l6-ih9GP8,961 apprise/attachment/AttachBase.pyi,sha256=w0XG_QKauiMLJ7eQ4S57IiLIURZHm_Snw7l6-ih9GP8,961
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=_CMPp4QGLATfGO2-Nw57sxsQyed9z3ywgoB0vpK3KZk,13779
apprise/attachment/__init__.py,sha256=xabgXpvV05X-YRuqIt3uGYMXwYNXjHyF6Dwd8HfZCFE,1658 apprise/attachment/__init__.py,sha256=xabgXpvV05X-YRuqIt3uGYMXwYNXjHyF6Dwd8HfZCFE,1658
apprise/cli.py,sha256=h-pWSQPqQficH6J-OEp3MTGydWyt6vMYnDZvHCeAt4Y,20697 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=d1efIuQFCJr66WgpudV2DWtxY3-tuZAyMAhHXBzJ8p0,53194
apprise/config/ConfigBase.pyi,sha256=cngfobwH6v2vxYbQrObDi5Z-t5wcquWF-wR0kBCr3Eg,54 apprise/config/ConfigBase.pyi,sha256=cngfobwH6v2vxYbQrObDi5Z-t5wcquWF-wR0kBCr3Eg,54
apprise/config/ConfigFile.py,sha256=u_SDaN3OHMyaAq2X7k_T4_PRKkVsDwleqBz9YIN5lbA,6138 apprise/config/ConfigFile.py,sha256=u_SDaN3OHMyaAq2X7k_T4_PRKkVsDwleqBz9YIN5lbA,6138
apprise/config/ConfigHTTP.py,sha256=Iy6Ji8_nX3xDjFgJGLrz4ftrMlMiyKiFGzYGJ7rMSMQ,9457 apprise/config/ConfigHTTP.py,sha256=Iy6Ji8_nX3xDjFgJGLrz4ftrMlMiyKiFGzYGJ7rMSMQ,9457
apprise/config/ConfigMemory.py,sha256=epEAgNy-eJVWoQaUOvjivMWxXTofy6wAQ-NbCqYmuyE,2829 apprise/config/ConfigMemory.py,sha256=epEAgNy-eJVWoQaUOvjivMWxXTofy6wAQ-NbCqYmuyE,2829
apprise/config/__init__.py,sha256=lbsxrUpB1IYM2q7kjYhsXQGgPF-yZXJrKFE361tdIPY,1663 apprise/config/__init__.py,sha256=lbsxrUpB1IYM2q7kjYhsXQGgPF-yZXJrKFE361tdIPY,1663
apprise/conversion.py,sha256=bvTu-3TU2CPEhdroLRtd_XpDzzXqe_wyUql089IpYxs,6197 apprise/conversion.py,sha256=0VZ0eCZfksN-97Vl0TjVjwnCTgus3XTRioceSFnP-gc,6277
apprise/decorators/CustomNotifyPlugin.py,sha256=F49vOM2EVy43Pn3j8z7tgTacweMUxGhw0UX-1n2Y3c8,7836 apprise/decorators/CustomNotifyPlugin.py,sha256=i4D-sgOsBWsxO5auWCN2bgXLLPuADaaLlJ1gUKLj2bU,7972
apprise/decorators/__init__.py,sha256=e_PDAm0kQNzwDPx-NJZLPfLMd2VAABvNZtxx_iDviRM,1487 apprise/decorators/__init__.py,sha256=e_PDAm0kQNzwDPx-NJZLPfLMd2VAABvNZtxx_iDviRM,1487
apprise/decorators/notify.py,sha256=a2WupErNw1_SMAld7jPC273bskiChMpYy95BOog5A9w,5111 apprise/decorators/notify.py,sha256=a2WupErNw1_SMAld7jPC273bskiChMpYy95BOog5A9w,5111
apprise/emojis.py,sha256=ONF0t8dY9f2XlEkLUG79-ybKVAj2GqbPj2-Be97vAoI,87738 apprise/emojis.py,sha256=ONF0t8dY9f2XlEkLUG79-ybKVAj2GqbPj2-Be97vAoI,87738
@ -69,21 +69,22 @@ apprise/i18n/en/LC_MESSAGES/apprise.mo,sha256=oUTuHREmLEYN07oqYqRMJ_kU71-o5o37Ns
apprise/logger.py,sha256=131hqhed8cUj9x_mfXDEvwA2YbcYDFAYiWVK1HgxRVY,6921 apprise/logger.py,sha256=131hqhed8cUj9x_mfXDEvwA2YbcYDFAYiWVK1HgxRVY,6921
apprise/manager.py,sha256=R9w8jxQRNy6Z_XDcobkt4JYbrC4jtj2OwRw9Zrib3CA,26857 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=xdL_aIVgb4ggxRFeCdkZAbgHYZ8DWLw9pRpLZQ0rHoE,25523
apprise/plugins/NotifyBark.py,sha256=bsDvKooRy4k1Gg7tvBjv3DIx7-WZiV_mbTrkTwMtd9Q,15698 apprise/plugins/NotifyBark.py,sha256=bsDvKooRy4k1Gg7tvBjv3DIx7-WZiV_mbTrkTwMtd9Q,15698
apprise/plugins/NotifyBase.py,sha256=9MB2uv4Rv8BnoXjU52k5Mv4YQppkNPv4Y_iPwauKxKQ,29716 apprise/plugins/NotifyBase.py,sha256=G3xkF_a2BWqNSxsrnOW7NUgHjOqBCYC5zihCifWemo8,30360
apprise/plugins/NotifyBase.pyi,sha256=aKlZXRYUgG8lz_ZgGkYYJ_GKhuf18youTmMU-FlG7z8,21 apprise/plugins/NotifyBase.pyi,sha256=aKlZXRYUgG8lz_ZgGkYYJ_GKhuf18youTmMU-FlG7z8,21
apprise/plugins/NotifyBoxcar.py,sha256=vR00-WggHa1nHYWyb-f5P2V-G4f683fU_-GBlIeJvD0,12867 apprise/plugins/NotifyBoxcar.py,sha256=vR00-WggHa1nHYWyb-f5P2V-G4f683fU_-GBlIeJvD0,12867
apprise/plugins/NotifyBulkSMS.py,sha256=stPWAFCfhBP617zYK9Dgk6pNJBN_WcyJtODzo0jR1QQ,16005 apprise/plugins/NotifyBulkSMS.py,sha256=stPWAFCfhBP617zYK9Dgk6pNJBN_WcyJtODzo0jR1QQ,16005
apprise/plugins/NotifyBulkVS.py,sha256=viLGeyUDiirRRM7CgRqqElHSLYFnMugDtWE6Ytjqfaw,13290 apprise/plugins/NotifyBulkVS.py,sha256=viLGeyUDiirRRM7CgRqqElHSLYFnMugDtWE6Ytjqfaw,13290
apprise/plugins/NotifyBurstSMS.py,sha256=cN2kRETKIK5LhwpQEA8C68LKv8KEUPmXYe-nTSegGls,15550 apprise/plugins/NotifyBurstSMS.py,sha256=cN2kRETKIK5LhwpQEA8C68LKv8KEUPmXYe-nTSegGls,15550
apprise/plugins/NotifyChantify.py,sha256=GJJOAtSnVoIfKbJF_W1DTu7WsvS_zHdjO4T1XTKT87g,6673
apprise/plugins/NotifyClickSend.py,sha256=UfOJqsas6WLjQskojuJE7I_-lrb5QrkMiBZv-po_Q9c,11229 apprise/plugins/NotifyClickSend.py,sha256=UfOJqsas6WLjQskojuJE7I_-lrb5QrkMiBZv-po_Q9c,11229
apprise/plugins/NotifyD7Networks.py,sha256=4E6Fh0kQoDlMMwgZJDOXky7c7KrdMMvqprcfm29scWU,15043 apprise/plugins/NotifyD7Networks.py,sha256=4E6Fh0kQoDlMMwgZJDOXky7c7KrdMMvqprcfm29scWU,15043
apprise/plugins/NotifyDBus.py,sha256=1eVJHIL3XkFjDePMqfcll35Ie1vxggJ1iBsVFAIaF00,14379 apprise/plugins/NotifyDBus.py,sha256=1eVJHIL3XkFjDePMqfcll35Ie1vxggJ1iBsVFAIaF00,14379
apprise/plugins/NotifyDapnet.py,sha256=KuXjBU0ZrIYtoDei85NeLZ-IP810T4w5oFXH9sWiSh0,13624 apprise/plugins/NotifyDapnet.py,sha256=KuXjBU0ZrIYtoDei85NeLZ-IP810T4w5oFXH9sWiSh0,13624
apprise/plugins/NotifyDingTalk.py,sha256=NJyETgN6QjtRqtxQjfBLFVuFpURyWykRftm6WpQJVbY,12009 apprise/plugins/NotifyDingTalk.py,sha256=NJyETgN6QjtRqtxQjfBLFVuFpURyWykRftm6WpQJVbY,12009
apprise/plugins/NotifyDiscord.py,sha256=M_qmTzB7NNL5_agjYDX38KBN1jRzDBp2EMSNwEF_9Tw,26072 apprise/plugins/NotifyDiscord.py,sha256=M_qmTzB7NNL5_agjYDX38KBN1jRzDBp2EMSNwEF_9Tw,26072
apprise/plugins/NotifyEmail.py,sha256=DhAzLFX4pzzuS07QQFcv0VUOYu2PzQE7TTjlPokJcPY,38883 apprise/plugins/NotifyEmail.py,sha256=Y_ZOrdK6hTUKHLvogKpV5VqD8byzDyDSvwIVmfdsC2g,39789
apprise/plugins/NotifyEmby.py,sha256=OMVO8XsVl_XCBYNNNQi8ni2lS4voLfU8Puk1xJOAvHs,24039 apprise/plugins/NotifyEmby.py,sha256=OMVO8XsVl_XCBYNNNQi8ni2lS4voLfU8Puk1xJOAvHs,24039
apprise/plugins/NotifyEnigma2.py,sha256=Hj0Q9YOeljSwbfiuMKLqXTVX_1g_mjNUGEts7wfrwno,11498 apprise/plugins/NotifyEnigma2.py,sha256=Hj0Q9YOeljSwbfiuMKLqXTVX_1g_mjNUGEts7wfrwno,11498
apprise/plugins/NotifyFCM/__init__.py,sha256=mBFtIgIJuLIFnMB5ndx5Makjs9orVMc2oLoD7LaVT48,21669 apprise/plugins/NotifyFCM/__init__.py,sha256=mBFtIgIJuLIFnMB5ndx5Makjs9orVMc2oLoD7LaVT48,21669
@ -91,9 +92,10 @@ apprise/plugins/NotifyFCM/color.py,sha256=8iqDtadloQh2TMxkFmIFwenHqKp1pHHn1bwyWO
apprise/plugins/NotifyFCM/common.py,sha256=978uBUoNdtopCtylipGiKQdsQ8FTONxkFBp7uJMZHc8,1718 apprise/plugins/NotifyFCM/common.py,sha256=978uBUoNdtopCtylipGiKQdsQ8FTONxkFBp7uJMZHc8,1718
apprise/plugins/NotifyFCM/oauth.py,sha256=Vvbd0-rd5BPIjAneG3rILU153JIzfSZ0kaDov6hm96M,11197 apprise/plugins/NotifyFCM/oauth.py,sha256=Vvbd0-rd5BPIjAneG3rILU153JIzfSZ0kaDov6hm96M,11197
apprise/plugins/NotifyFCM/priority.py,sha256=0WuRW1y1HVnybgjlTeCZPHzt7j8SwWnC7faNcjioAOc,8163 apprise/plugins/NotifyFCM/priority.py,sha256=0WuRW1y1HVnybgjlTeCZPHzt7j8SwWnC7faNcjioAOc,8163
apprise/plugins/NotifyFaast.py,sha256=_F1633tQhk8gCfaNpZZm808f2G0S6fP0OOEetSiv0h8,6972 apprise/plugins/NotifyFeishu.py,sha256=IpcABdLZJ1vcQdZHlmASVbNOiOCIrmgKFhz1hbdskY4,7266
apprise/plugins/NotifyFlock.py,sha256=0rUIa9nToGsO8BTUgixh8Z_qdVixJeH479UNYjcE4EM,12748 apprise/plugins/NotifyFlock.py,sha256=0rUIa9nToGsO8BTUgixh8Z_qdVixJeH479UNYjcE4EM,12748
apprise/plugins/NotifyForm.py,sha256=38nL-2m1cf4gEQFQ4NpvA4j9i5_nNUgelReWFSjyV5U,17905 apprise/plugins/NotifyForm.py,sha256=38nL-2m1cf4gEQFQ4NpvA4j9i5_nNUgelReWFSjyV5U,17905
apprise/plugins/NotifyFreeMobile.py,sha256=XCkgZLc3KKGlx_9UdeoMJVcHpeQrOml9T93S-DGf4bs,6644
apprise/plugins/NotifyGnome.py,sha256=8MXTa8gZg1wTgNJfLlmq7_fl3WaYK-SX6VR91u308C4,9059 apprise/plugins/NotifyGnome.py,sha256=8MXTa8gZg1wTgNJfLlmq7_fl3WaYK-SX6VR91u308C4,9059
apprise/plugins/NotifyGoogleChat.py,sha256=lnoN17m6lZANaXcElDTP8lcuVWjIZEK8C6_iqJNAnw4,12622 apprise/plugins/NotifyGoogleChat.py,sha256=lnoN17m6lZANaXcElDTP8lcuVWjIZEK8C6_iqJNAnw4,12622
apprise/plugins/NotifyGotify.py,sha256=DNlOIHyuYitO5use9oa_REPm2Fant7y9QSaatrZFNI0,10551 apprise/plugins/NotifyGotify.py,sha256=DNlOIHyuYitO5use9oa_REPm2Fant7y9QSaatrZFNI0,10551
@ -109,7 +111,7 @@ apprise/plugins/NotifyKumulos.py,sha256=eCEW2ZverZqETOLHVWMC4E8Ll6rEhhEWOSD73RD8
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/NotifyLunaSea.py,sha256=woN8XdkwAjhgxAXp7Zj4XsWLybNL80l4W3Dx5BvobZg,14459
apprise/plugins/NotifyMQTT.py,sha256=PFLwESgR8dMZvVFHxmOZ8xfy-YqyX5b2kl_e8Z1lo-0,19537 apprise/plugins/NotifyMQTT.py,sha256=cnuG4f3bYYNPhEj9qDX8SLmnxLVT9G1b8J5w6-mQGKY,19545
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
apprise/plugins/NotifyMacOSX.py,sha256=y2fGpSZXomFiNwKbWImrXQUMVM4JR4uPCnsWpnxQrFA,8271 apprise/plugins/NotifyMacOSX.py,sha256=y2fGpSZXomFiNwKbWImrXQUMVM4JR4uPCnsWpnxQrFA,8271
@ -124,7 +126,7 @@ apprise/plugins/NotifyNextcloudTalk.py,sha256=dLl_g7Knq5PVcadbzDuQsxbGHTZlC4r-pQ
apprise/plugins/NotifyNotica.py,sha256=yHmk8HiNFjzoI4Gewo_nBRrx9liEmhT95k1d10wqhYg,12990 apprise/plugins/NotifyNotica.py,sha256=yHmk8HiNFjzoI4Gewo_nBRrx9liEmhT95k1d10wqhYg,12990
apprise/plugins/NotifyNotifiarr.py,sha256=ADwLJO9eenfLkNa09tXMGSBTM4c3zTY0SEePvyB8WYA,15857 apprise/plugins/NotifyNotifiarr.py,sha256=ADwLJO9eenfLkNa09tXMGSBTM4c3zTY0SEePvyB8WYA,15857
apprise/plugins/NotifyNotifico.py,sha256=Qe9jMN_M3GL4XlYIWkAf-w_Hf65g9Hde4bVuytGhUW4,12035 apprise/plugins/NotifyNotifico.py,sha256=Qe9jMN_M3GL4XlYIWkAf-w_Hf65g9Hde4bVuytGhUW4,12035
apprise/plugins/NotifyNtfy.py,sha256=TkDs6jOc30XQn2O2BJ14-nE_cohPdJiSS8DpYXc9hoE,27953 apprise/plugins/NotifyNtfy.py,sha256=AtJt2zH35mMQTwRDxKia93NPy6-4rtixplP53zIYV2M,27979
apprise/plugins/NotifyOffice365.py,sha256=8TxsVsdbUghmNj0kceMlmoZzTOKQTgn3priI8JuRuHE,25190 apprise/plugins/NotifyOffice365.py,sha256=8TxsVsdbUghmNj0kceMlmoZzTOKQTgn3priI8JuRuHE,25190
apprise/plugins/NotifyOneSignal.py,sha256=gsw7ckW7xLiJDRUb7eJHNe_4bvdBXmt6_YsB1u_ghjw,18153 apprise/plugins/NotifyOneSignal.py,sha256=gsw7ckW7xLiJDRUb7eJHNe_4bvdBXmt6_YsB1u_ghjw,18153
apprise/plugins/NotifyOpsgenie.py,sha256=zJWpknjoHq35Iv9w88ucR62odaeIN3nrGFPtYnhDdjA,20515 apprise/plugins/NotifyOpsgenie.py,sha256=zJWpknjoHq35Iv9w88ucR62odaeIN3nrGFPtYnhDdjA,20515
@ -144,7 +146,7 @@ apprise/plugins/NotifyPushy.py,sha256=mmWcnu905Fvc8ihYXvZ7lVYErGZH5Q-GbBNS20v5r4
apprise/plugins/NotifyRSyslog.py,sha256=W42LT90X65-pNoU7KdhdX1PBcmsz9RyV376CDa_H3CI,11982 apprise/plugins/NotifyRSyslog.py,sha256=W42LT90X65-pNoU7KdhdX1PBcmsz9RyV376CDa_H3CI,11982
apprise/plugins/NotifyReddit.py,sha256=E78OSyDQfUalBEcg71sdMsNBOwdj7cVBnELrhrZEAXY,25785 apprise/plugins/NotifyReddit.py,sha256=E78OSyDQfUalBEcg71sdMsNBOwdj7cVBnELrhrZEAXY,25785
apprise/plugins/NotifyRevolt.py,sha256=DRA9Xylwl6leVjVFuJcP4L1cG49CIBtnQdxh4BKnAZ4,14500 apprise/plugins/NotifyRevolt.py,sha256=DRA9Xylwl6leVjVFuJcP4L1cG49CIBtnQdxh4BKnAZ4,14500
apprise/plugins/NotifyRocketChat.py,sha256=GTEfT-upQ56tJgE0kuc59l4uQGySj_d15wjdcARR9Ko,24624 apprise/plugins/NotifyRocketChat.py,sha256=Cb_nasX0-G3FoPMYvNk55RJ-tHuXUCTLUn2wTSi4IcI,25738
apprise/plugins/NotifyRyver.py,sha256=yhHPMLGeJtcHwBKSPPk0OBfp59DgTvXio1R59JhrJu4,11823 apprise/plugins/NotifyRyver.py,sha256=yhHPMLGeJtcHwBKSPPk0OBfp59DgTvXio1R59JhrJu4,11823
apprise/plugins/NotifySES.py,sha256=wtRmpAZkS5mQma6sdiaPT6U1xcgoj77CB9mNFvSEAw8,33545 apprise/plugins/NotifySES.py,sha256=wtRmpAZkS5mQma6sdiaPT6U1xcgoj77CB9mNFvSEAw8,33545
apprise/plugins/NotifySMSEagle.py,sha256=voFNqOewD9OC1eRctD0YdUB_ZSWsb06rjUwBfCcxPYA,24161 apprise/plugins/NotifySMSEagle.py,sha256=voFNqOewD9OC1eRctD0YdUB_ZSWsb06rjUwBfCcxPYA,24161
@ -162,7 +164,7 @@ apprise/plugins/NotifyStreamlabs.py,sha256=lx3N8T2ufUWFYIZ-kU_rOv50YyGWBqLSCKk7x
apprise/plugins/NotifySynology.py,sha256=_jTqfgWeOuSi_I8geMOraHBVFtDkvm9mempzymrmeAo,11105 apprise/plugins/NotifySynology.py,sha256=_jTqfgWeOuSi_I8geMOraHBVFtDkvm9mempzymrmeAo,11105
apprise/plugins/NotifySyslog.py,sha256=J9Kain2bb-PDNiG5Ydb0q678cYjNE_NjZFqMG9oEXM0,10617 apprise/plugins/NotifySyslog.py,sha256=J9Kain2bb-PDNiG5Ydb0q678cYjNE_NjZFqMG9oEXM0,10617
apprise/plugins/NotifyTechulusPush.py,sha256=m43_Qj1scPcgCRX5Dr2Ul7nxMbaiVxNzm_HRuNmfgoA,7253 apprise/plugins/NotifyTechulusPush.py,sha256=m43_Qj1scPcgCRX5Dr2Ul7nxMbaiVxNzm_HRuNmfgoA,7253
apprise/plugins/NotifyTelegram.py,sha256=Bim4mmPcefHNpvbNSy3pmLuCXRw5IVVWUNUB1SkIhDM,35624 apprise/plugins/NotifyTelegram.py,sha256=XE7PC9LRzcrfE2bpLKyor5lO_7B9LS4Xw1UlUmA4a2A,37187
apprise/plugins/NotifyThreema.py,sha256=C_C3j0fJWgeF2uB7ceJFXOdC6Lt0TFBInFMs5Xlg04M,11885 apprise/plugins/NotifyThreema.py,sha256=C_C3j0fJWgeF2uB7ceJFXOdC6Lt0TFBInFMs5Xlg04M,11885
apprise/plugins/NotifyTwilio.py,sha256=WCo8eTI9OF1rtg3ueHHRDXt4Lp45eZ6h3IdTZVf5HM8,15976 apprise/plugins/NotifyTwilio.py,sha256=WCo8eTI9OF1rtg3ueHHRDXt4Lp45eZ6h3IdTZVf5HM8,15976
apprise/plugins/NotifyTwist.py,sha256=nZA73CYVe-p0tkVMy5q3vFRyflLM4yjUo9LECvkUwgc,28841 apprise/plugins/NotifyTwist.py,sha256=nZA73CYVe-p0tkVMy5q3vFRyflLM4yjUo9LECvkUwgc,28841
@ -175,7 +177,7 @@ apprise/plugins/NotifyWhatsApp.py,sha256=PtzW0ue3d2wZ8Pva_LG29jUcpRRP03TFxO5SME_
apprise/plugins/NotifyWindows.py,sha256=QgWJfJF8AE6RWr-L81YYVZNWrnImK9Qr3B991HWanqU,8563 apprise/plugins/NotifyWindows.py,sha256=QgWJfJF8AE6RWr-L81YYVZNWrnImK9Qr3B991HWanqU,8563
apprise/plugins/NotifyXBMC.py,sha256=5hDuOTP3Kwtp4NEMaokNjWyEKEkQcN_fSx-cUPJvhaU,12096 apprise/plugins/NotifyXBMC.py,sha256=5hDuOTP3Kwtp4NEMaokNjWyEKEkQcN_fSx-cUPJvhaU,12096
apprise/plugins/NotifyXML.py,sha256=WJnmdvXseuTRgioVMRqpR8a09cDfTpPTfuFlTnT_TfI,16973 apprise/plugins/NotifyXML.py,sha256=WJnmdvXseuTRgioVMRqpR8a09cDfTpPTfuFlTnT_TfI,16973
apprise/plugins/NotifyZulip.py,sha256=mbZoPiQXFbcaJ5UYDbkX4HJPAvRzPEAB-rsOlF9SD4o,13755 apprise/plugins/NotifyZulip.py,sha256=M8cSL7nZvtBYyTX6045g34tyn2vyybltgD1CoI4Xa7A,13968
apprise/plugins/__init__.py,sha256=jTfLmW47kZC_Wf5eFFta2NoD2J-7_E7JaPrrVMIECkU,18725 apprise/plugins/__init__.py,sha256=jTfLmW47kZC_Wf5eFFta2NoD2J-7_E7JaPrrVMIECkU,18725
apprise/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 apprise/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
apprise/utils.py,sha256=SjRU2tb1UsVnTCTXPUyXVz3WpRbDWwAHH-d3ll38EHY,53185 apprise/utils.py,sha256=SjRU2tb1UsVnTCTXPUyXVz3WpRbDWwAHH-d3ll38EHY,53185

View File

View File

@ -219,6 +219,9 @@ class AppriseLocale:
try: try:
# Acquire our locale # Acquire our locale
lang = locale.getlocale()[0] lang = locale.getlocale()[0]
# Compatibility for Python >= 3.12
if lang == 'C':
lang = AppriseLocale._default_language
except (ValueError, TypeError) as e: except (ValueError, TypeError) as e:
# This occurs when an invalid locale was parsed from the # This occurs when an invalid locale was parsed from the

View File

@ -669,6 +669,79 @@ class URLBase:
'verify': 'yes' if self.verify_certificate else 'no', 'verify': 'yes' if self.verify_certificate else 'no',
} }
@staticmethod
def post_process_parse_url_results(results):
"""
After parsing the URL, this function applies a bit of extra logic to
support extra entries like `pass` becoming `password`, etc
This function assumes that parse_url() was called previously setting
up the basics to be checked
"""
# if our URL ends with an 's', then assume our secure flag is set.
results['secure'] = (results['schema'][-1] == 's')
# QSD Checking (over-rides all)
qsd_exists = True if isinstance(results.get('qsd'), dict) else False
if qsd_exists and 'verify' in results['qsd']:
# Pulled from URL String
results['verify'] = parse_bool(
results['qsd'].get('verify', True))
elif 'verify' in results:
# Pulled from YAML Configuratoin
results['verify'] = parse_bool(results.get('verify', True))
else:
# Support SSL Certificate 'verify' keyword. Default to being
# enabled
results['verify'] = True
# Password overrides
if 'pass' in results:
results['password'] = results['pass']
del results['pass']
if qsd_exists:
if 'password' in results['qsd']:
results['password'] = results['qsd']['password']
if 'pass' in results['qsd']:
results['password'] = results['qsd']['pass']
# User overrides
if 'user' in results['qsd']:
results['user'] = results['qsd']['user']
# parse_url() always creates a 'password' and 'user' entry in the
# results returned. Entries are set to None if they weren't
# specified
if results['password'] is None and 'user' in results['qsd']:
# Handle cases where the user= provided in 2 locations, we want
# the original to fall back as a being a password (if one
# wasn't otherwise defined) e.g.
# mailtos://PASSWORD@hostname?user=admin@mail-domain.com
# - in the above, the PASSWORD gets lost in the parse url()
# since a user= over-ride is specified.
presults = parse_url(results['url'])
if presults:
# Store our Password
results['password'] = presults['user']
# Store our socket read timeout if specified
if 'rto' in results['qsd']:
results['rto'] = results['qsd']['rto']
# Store our socket connect timeout if specified
if 'cto' in results['qsd']:
results['cto'] = results['qsd']['cto']
if 'port' in results['qsd']:
results['port'] = results['qsd']['port']
return results
@staticmethod @staticmethod
def parse_url(url, verify_host=True, plus_to_space=False, def parse_url(url, verify_host=True, plus_to_space=False,
strict_port=False): strict_port=False):
@ -698,53 +771,7 @@ class URLBase:
# We're done; we failed to parse our url # We're done; we failed to parse our url
return results return results
# if our URL ends with an 's', then assume our secure flag is set. return URLBase.post_process_parse_url_results(results)
results['secure'] = (results['schema'][-1] == 's')
# Support SSL Certificate 'verify' keyword. Default to being enabled
results['verify'] = True
if 'verify' in results['qsd']:
results['verify'] = parse_bool(
results['qsd'].get('verify', True))
# Password overrides
if 'password' in results['qsd']:
results['password'] = results['qsd']['password']
if 'pass' in results['qsd']:
results['password'] = results['qsd']['pass']
# User overrides
if 'user' in results['qsd']:
results['user'] = results['qsd']['user']
# parse_url() always creates a 'password' and 'user' entry in the
# results returned. Entries are set to None if they weren't specified
if results['password'] is None and 'user' in results['qsd']:
# Handle cases where the user= provided in 2 locations, we want
# the original to fall back as a being a password (if one wasn't
# otherwise defined)
# e.g.
# mailtos://PASSWORD@hostname?user=admin@mail-domain.com
# - the PASSWORD gets lost in the parse url() since a user=
# over-ride is specified.
presults = parse_url(results['url'])
if presults:
# Store our Password
results['password'] = presults['user']
# Store our socket read timeout if specified
if 'rto' in results['qsd']:
results['rto'] = results['qsd']['rto']
# Store our socket connect timeout if specified
if 'cto' in results['qsd']:
results['cto'] = results['qsd']['cto']
if 'port' in results['qsd']:
results['port'] = results['qsd']['port']
return results
@staticmethod @staticmethod
def http_response_code_lookup(code, response_mask=None): def http_response_code_lookup(code, response_mask=None):

View File

@ -27,7 +27,7 @@
# POSSIBILITY OF SUCH DAMAGE. # POSSIBILITY OF SUCH DAMAGE.
__title__ = 'Apprise' __title__ = 'Apprise'
__version__ = '1.7.4' __version__ = '1.7.6'
__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>'

View File

@ -253,7 +253,7 @@ class AttachBase(URLBase):
return self.detected_mimetype \ return self.detected_mimetype \
if self.detected_mimetype else self.unknown_mimetype if self.detected_mimetype else self.unknown_mimetype
def exists(self): def exists(self, retrieve_if_missing=True):
""" """
Simply returns true if the object has downloaded and stored the Simply returns true if the object has downloaded and stored the
attachment AND the attachment has not expired. attachment AND the attachment has not expired.
@ -282,7 +282,7 @@ class AttachBase(URLBase):
# The file is not present # The file is not present
pass pass
return self.download() return False if not retrieve_if_missing else self.download()
def invalidate(self): def invalidate(self):
""" """

View File

@ -29,6 +29,7 @@
import re import re
import os import os
import requests import requests
import threading
from tempfile import NamedTemporaryFile from tempfile import NamedTemporaryFile
from .AttachBase import AttachBase from .AttachBase import AttachBase
from ..common import ContentLocation from ..common import ContentLocation
@ -56,6 +57,9 @@ class AttachHTTP(AttachBase):
# Web based requests are remote/external to our current location # Web based requests are remote/external to our current location
location = ContentLocation.HOSTED location = ContentLocation.HOSTED
# thread safe loading
_lock = threading.Lock()
def __init__(self, headers=None, **kwargs): def __init__(self, headers=None, **kwargs):
""" """
Initialize HTTP Object Initialize HTTP Object
@ -96,9 +100,6 @@ class AttachHTTP(AttachBase):
# our content is inaccessible # our content is inaccessible
return False return False
# Ensure any existing content set has been invalidated
self.invalidate()
# prepare header # prepare header
headers = { headers = {
'User-Agent': self.app_id, 'User-Agent': self.app_id,
@ -117,16 +118,28 @@ class AttachHTTP(AttachBase):
url += self.fullpath url += self.fullpath
self.logger.debug('HTTP POST URL: %s (cert_verify=%r)' % (
url, self.verify_certificate,
))
# Where our request object will temporarily live. # Where our request object will temporarily live.
r = None r = None
# Always call throttle before any remote server i/o is made # Always call throttle before any remote server i/o is made
self.throttle() self.throttle()
with self._lock:
if self.exists(retrieve_if_missing=False):
# Due to locking; it's possible a concurrent thread already
# handled the retrieval in which case we can safely move on
self.logger.trace(
'HTTP Attachment %s already retrieved',
self._temp_file.name)
return True
# Ensure any existing content set has been invalidated
self.invalidate()
self.logger.debug(
'HTTP Attachment Fetch URL: %s (cert_verify=%r)' % (
url, self.verify_certificate))
try: try:
# Make our request # Make our request
with requests.get( with requests.get(
@ -149,12 +162,13 @@ class AttachHTTP(AttachBase):
file_size = 0 file_size = 0
# Perform a little Q/A on file limitations and restrictions # Perform a little Q/A on file limitations and restrictions
if self.max_file_size > 0 and file_size > self.max_file_size: if self.max_file_size > 0 and \
file_size > self.max_file_size:
# The content retrieved is to large # The content retrieved is to large
self.logger.error( self.logger.error(
'HTTP response exceeds allowable maximum file length ' 'HTTP response exceeds allowable maximum file '
'({}KB): {}'.format( 'length ({}KB): {}'.format(
int(self.max_file_size / 1024), int(self.max_file_size / 1024),
self.url(privacy=True))) self.url(privacy=True)))
@ -171,8 +185,11 @@ class AttachHTTP(AttachBase):
if result: if result:
self.detected_name = result.group('name').strip() self.detected_name = result.group('name').strip()
# Create a temporary file to work with # Create a temporary file to work with; delete must be set
self._temp_file = NamedTemporaryFile() # to False or it isn't compatible with Microsoft Windows
# instances. In lieu of this, __del__ will clean up the
# file for us.
self._temp_file = NamedTemporaryFile(delete=False)
# Get our chunk size # Get our chunk size
chunk_size = self.chunk_size chunk_size = self.chunk_size
@ -180,21 +197,24 @@ class AttachHTTP(AttachBase):
# Track all bytes written to disk # Track all bytes written to disk
bytes_written = 0 bytes_written = 0
# If we get here, we can now safely write our content to disk # If we get here, we can now safely write our content to
# disk
for chunk in r.iter_content(chunk_size=chunk_size): for chunk in r.iter_content(chunk_size=chunk_size):
# filter out keep-alive chunks # filter out keep-alive chunks
if chunk: if chunk:
self._temp_file.write(chunk) self._temp_file.write(chunk)
bytes_written = self._temp_file.tell() bytes_written = self._temp_file.tell()
# Prevent a case where Content-Length isn't provided # Prevent a case where Content-Length isn't
# we don't want to fetch beyond our limits # provided. In this case we don't want to fetch
# beyond our limits
if self.max_file_size > 0: if self.max_file_size > 0:
if bytes_written > self.max_file_size: if bytes_written > self.max_file_size:
# The content retrieved is to large # The content retrieved is to large
self.logger.error( self.logger.error(
'HTTP response exceeds allowable maximum ' 'HTTP response exceeds allowable '
'file length ({}KB): {}'.format( 'maximum file length '
'({}KB): {}'.format(
int(self.max_file_size / 1024), int(self.max_file_size / 1024),
self.url(privacy=True))) self.url(privacy=True)))
@ -206,15 +226,16 @@ class AttachHTTP(AttachBase):
elif bytes_written + chunk_size \ elif bytes_written + chunk_size \
> self.max_file_size: > self.max_file_size:
# Adjust out next read to accomodate up to our # Adjust out next read to accomodate up to
# limit +1. This will prevent us from readig # our limit +1. This will prevent us from
# to much into our memory buffer # reading to much into our memory buffer
self.max_file_size - bytes_written + 1 self.max_file_size - bytes_written + 1
# Ensure our content is flushed to disk for post-processing # Ensure our content is flushed to disk for post-processing
self._temp_file.flush() self._temp_file.flush()
# Set our minimum requirements for a successful download() call # Set our minimum requirements for a successful download()
# call
self.download_path = self._temp_file.name self.download_path = self._temp_file.name
if not self.detected_name: if not self.detected_name:
self.detected_name = os.path.basename(self.fullpath) self.detected_name = os.path.basename(self.fullpath)
@ -254,11 +275,30 @@ class AttachHTTP(AttachBase):
Close our temporary file Close our temporary file
""" """
if self._temp_file: if self._temp_file:
self.logger.trace(
'Attachment cleanup of %s', self._temp_file.name)
self._temp_file.close() self._temp_file.close()
try:
# Ensure our file is removed (if it exists)
os.unlink(self._temp_file.name)
except OSError:
pass
# Reset our temporary file to prevent from entering
# this block again
self._temp_file = None self._temp_file = None
super().invalidate() super().invalidate()
def __del__(self):
"""
Tidy memory if open
"""
with self._lock:
self.invalidate()
def url(self, privacy=False, *args, **kwargs): def url(self, privacy=False, *args, **kwargs):
""" """
Returns the URL built dynamically based on specified arguments. Returns the URL built dynamically based on specified arguments.

View File

@ -1184,6 +1184,9 @@ class ConfigBase(URLBase):
# Prepare our Asset Object # Prepare our Asset Object
_results['asset'] = asset _results['asset'] = asset
# Handle post processing of result set
_results = URLBase.post_process_parse_url_results(_results)
# Store our preloaded entries # Store our preloaded entries
preloaded.append({ preloaded.append({
'results': _results, 'results': _results,

View File

@ -58,8 +58,8 @@ def markdown_to_html(content):
""" """
Converts specified content from markdown to HTML. Converts specified content from markdown to HTML.
""" """
return markdown(content, extensions=[
return markdown(content) 'markdown.extensions.nl2br', 'markdown.extensions.tables'])
def text_to_html(content): def text_to_html(content):

View File

@ -147,6 +147,10 @@ class CustomNotifyPlugin(NotifyBase):
self._default_args = {} self._default_args = {}
# Some variables do not need to be set
if 'secure' in kwargs:
del kwargs['secure']
# Apply our updates based on what was parsed # Apply our updates based on what was parsed
dict_full_update(self._default_args, self._base_args) dict_full_update(self._default_args, self._base_args)
dict_full_update(self._default_args, kwargs) dict_full_update(self._default_args, kwargs)

View File

@ -203,6 +203,13 @@ class NotifyAprs(NotifyBase):
"type": "string", "type": "string",
"map_to": "targets", "map_to": "targets",
}, },
"delay": {
"name": _("Resend Delay"),
"type": "float",
"min": 0.0,
"max": 5.0,
"default": 0.0,
},
"locale": { "locale": {
"name": _("Locale"), "name": _("Locale"),
"type": "choice:string", "type": "choice:string",
@ -212,7 +219,7 @@ class NotifyAprs(NotifyBase):
} }
) )
def __init__(self, targets=None, locale=None, **kwargs): def __init__(self, targets=None, locale=None, delay=None, **kwargs):
""" """
Initialize APRS Object Initialize APRS Object
""" """
@ -272,6 +279,28 @@ class NotifyAprs(NotifyBase):
self.logger.warning(msg) self.logger.warning(msg)
raise TypeError(msg) raise TypeError(msg)
# Update our delay
if delay is None:
self.delay = NotifyAprs.template_args["delay"]["default"]
else:
try:
self.delay = float(delay)
if self.delay < NotifyAprs.template_args["delay"]["min"]:
raise ValueError()
elif self.delay >= NotifyAprs.template_args["delay"]["max"]:
raise ValueError()
except (TypeError, ValueError):
msg = "Unsupported APRS-IS delay ({}) specified. ".format(
delay)
self.logger.warning(msg)
raise TypeError(msg)
# Bump up our request_rate
self.request_rate_per_sec += self.delay
# Set the transmitter group # Set the transmitter group
self.locale = \ self.locale = \
NotifyAprs.template_args["locale"]["default"] \ NotifyAprs.template_args["locale"]["default"] \
@ -674,6 +703,10 @@ class NotifyAprs(NotifyBase):
# Store our locale if not default # Store our locale if not default
params['locale'] = self.locale params['locale'] = self.locale
if self.delay != NotifyAprs.template_args["delay"]["default"]:
# Store our locale if not default
params['delay'] = "{:.2f}".format(self.delay)
# Extend our parameters # Extend our parameters
params.update(self.url_parameters(privacy=privacy, *args, **kwargs)) params.update(self.url_parameters(privacy=privacy, *args, **kwargs))
@ -727,6 +760,10 @@ class NotifyAprs(NotifyBase):
# All entries after the hostname are additional targets # All entries after the hostname are additional targets
results["targets"].extend(NotifyAprs.split_path(results["fullpath"])) results["targets"].extend(NotifyAprs.split_path(results["fullpath"]))
# Get Delay (if set)
if 'delay' in results['qsd'] and len(results['qsd']['delay']):
results['delay'] = NotifyAprs.unquote(results['qsd']['delay'])
# Support the 'to' variable so that we can support rooms this way too # Support the 'to' variable so that we can support rooms this way too
# The 'to' makes it easier to use yaml configuration # The 'to' makes it easier to use yaml configuration
if "to" in results["qsd"] and len(results["qsd"]["to"]): if "to" in results["qsd"] and len(results["qsd"]["to"]):

View File

@ -457,6 +457,19 @@ class NotifyBase(URLBase):
# Handle situations where the title is None # Handle situations where the title is None
title = '' if not title else title title = '' if not title else title
# Truncate flag set with attachments ensures that only 1
# attachment passes through. In the event there could be many
# services specified, we only want to do this logic once.
# The logic is only applicable if ther was more then 1 attachment
# specified
overflow = self.overflow_mode if overflow is None else overflow
if attach and len(attach) > 1 and overflow == OverflowMode.TRUNCATE:
# Save first attachment
_attach = AppriseAttachment(attach[0], asset=self.asset)
else:
# reference same attachment
_attach = attach
# Apply our overflow (if defined) # Apply our overflow (if defined)
for chunk in self._apply_overflow( for chunk in self._apply_overflow(
body=body, title=title, overflow=overflow, body=body, title=title, overflow=overflow,
@ -465,7 +478,7 @@ class NotifyBase(URLBase):
# Send notification # Send notification
yield dict( yield dict(
body=chunk['body'], title=chunk['title'], body=chunk['body'], title=chunk['title'],
notify_type=notify_type, attach=attach, notify_type=notify_type, attach=_attach,
body_format=body_format body_format=body_format
) )
@ -485,7 +498,7 @@ class NotifyBase(URLBase):
}, },
{ {
title: 'the title goes here', title: 'the title goes here',
body: 'the message body goes here', body: 'the continued message body goes here',
}, },
] ]

View File

@ -26,118 +26,111 @@
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE. # POSSIBILITY OF SUCH DAMAGE.
# Chantify
# 1. Visit https://chanify.net/
# The API URL will look something like this:
# https://api.chanify.net/v1/sender/token
#
import requests import requests
from .NotifyBase import NotifyBase from .NotifyBase import NotifyBase
from ..common import NotifyImageSize
from ..common import NotifyType from ..common import NotifyType
from ..utils import parse_bool
from ..AppriseLocale import gettext_lazy as _
from ..utils import validate_regex from ..utils import validate_regex
from ..AppriseLocale import gettext_lazy as _
class NotifyFaast(NotifyBase): class NotifyChantify(NotifyBase):
""" """
A wrapper for Faast Notifications A wrapper for Chantify Notifications
""" """
# The default descriptive name associated with the Notification # The default descriptive name associated with the Notification
service_name = 'Faast' service_name = _('Chantify')
# The services URL # The services URL
service_url = 'http://www.faast.io/' service_url = 'https://chanify.net/'
# The default protocol (this is secure for faast) # The default secure protocol
protocol = 'faast' secure_protocol = 'chantify'
# A URL that takes you to the setup/help of the specific protocol # A URL that takes you to the setup/help of the specific protocol
setup_url = 'https://github.com/caronc/apprise/wiki/Notify_faast' setup_url = 'https://github.com/caronc/apprise/wiki/Notify_chantify'
# Faast uses the http protocol with JSON requests # Notification URL
notify_url = 'https://www.appnotifications.com/account/notifications.json' notify_url = 'https://api.chanify.net/v1/sender/{token}/'
# Allows the user to specify the NotifyImageSize object
image_size = NotifyImageSize.XY_72
# Define object templates # Define object templates
templates = ( templates = (
'{schema}://{authtoken}', '{schema}://{token}',
) )
# Define our template tokens # The title is not used
title_maxlen = 0
# Define our tokens; these are the minimum tokens required required to
# be passed into this function (as arguments). The syntax appends any
# previously defined in the base package and builds onto them
template_tokens = dict(NotifyBase.template_tokens, **{ template_tokens = dict(NotifyBase.template_tokens, **{
'authtoken': { 'token': {
'name': _('Authorization Token'), 'name': _('Token'),
'type': 'string', 'type': 'string',
'private': True, 'private': True,
'required': True, 'required': True,
'regex': (r'^[A-Z0-9_-]+$', 'i'),
}, },
}) })
# Define our template arguments # Define our template arguments
template_args = dict(NotifyBase.template_args, **{ template_args = dict(NotifyBase.template_args, **{
'image': { 'token': {
'name': _('Include Image'), 'alias_of': 'token',
'type': 'bool',
'default': True,
'map_to': 'include_image',
}, },
}) })
def __init__(self, authtoken, include_image=True, **kwargs): def __init__(self, token, **kwargs):
""" """
Initialize Faast Object Initialize Chantify Object
""" """
super().__init__(**kwargs) super().__init__(**kwargs)
# Store the Authentication Token self.token = validate_regex(
self.authtoken = validate_regex(authtoken) token, *self.template_tokens['token']['regex'])
if not self.authtoken: if not self.token:
msg = 'An invalid Faast Authentication Token ' \ msg = 'The Chantify token specified ({}) is invalid.'\
'({}) was specified.'.format(authtoken) .format(token)
self.logger.warning(msg) self.logger.warning(msg)
raise TypeError(msg) raise TypeError(msg)
# Associate an image with our post
self.include_image = include_image
return return
def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs): def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs):
""" """
Perform Faast Notification Send our notification
""" """
# prepare our headers
headers = { headers = {
'User-Agent': self.app_id, 'User-Agent': self.app_id,
'Content-Type': 'multipart/form-data' 'Content-Type': 'application/x-www-form-urlencoded',
} }
# prepare JSON Object # Our Message
payload = { payload = {
'user_credentials': self.authtoken, 'text': body
'title': title,
'message': body,
} }
# Acquire our image if we're configured to do so self.logger.debug('Chantify GET URL: %s (cert_verify=%r)' % (
image_url = None if not self.include_image \ self.notify_url, self.verify_certificate))
else self.image_url(notify_type) self.logger.debug('Chantify Payload: %s' % str(payload))
if image_url:
payload['icon_url'] = image_url
self.logger.debug('Faast POST URL: %s (cert_verify=%r)' % (
self.notify_url, self.verify_certificate,
))
self.logger.debug('Faast Payload: %s' % str(payload))
# Always call throttle before any remote server i/o is made # Always call throttle before any remote server i/o is made
self.throttle() self.throttle()
try: try:
r = requests.post( r = requests.post(
self.notify_url, self.notify_url.format(token=self.token),
data=payload, data=payload,
headers=headers, headers=headers,
verify=self.verify_certificate, verify=self.verify_certificate,
@ -146,10 +139,10 @@ class NotifyFaast(NotifyBase):
if r.status_code != requests.codes.ok: if r.status_code != requests.codes.ok:
# We had a problem # We had a problem
status_str = \ status_str = \
NotifyFaast.http_response_code_lookup(r.status_code) NotifyChantify.http_response_code_lookup(r.status_code)
self.logger.warning( self.logger.warning(
'Failed to send Faast notification:' 'Failed to send Chantify notification: '
'{}{}error={}.'.format( '{}{}error={}.'.format(
status_str, status_str,
', ' if status_str else '', ', ' if status_str else '',
@ -161,12 +154,12 @@ class NotifyFaast(NotifyBase):
return False return False
else: else:
self.logger.info('Sent Faast notification.') self.logger.info('Sent Chantify notification.')
except requests.RequestException as e: except requests.RequestException as e:
self.logger.warning( self.logger.warning(
'A Connection error occurred sending Faast notification.', 'A Connection error occurred sending Chantify '
) 'notification.')
self.logger.debug('Socket Exception: %s' % str(e)) self.logger.debug('Socket Exception: %s' % str(e))
# Return; we're done # Return; we're done
@ -179,18 +172,13 @@ class NotifyFaast(NotifyBase):
Returns the URL built dynamically based on specified arguments. Returns the URL built dynamically based on specified arguments.
""" """
# Define any URL parameters # Prepare our parameters
params = { params = self.url_parameters(privacy=privacy, *args, **kwargs)
'image': 'yes' if self.include_image else 'no',
}
# Extend our parameters return '{schema}://{token}/?{params}'.format(
params.update(self.url_parameters(privacy=privacy, *args, **kwargs)) schema=self.secure_protocol,
token=self.pprint(self.token, privacy, safe=''),
return '{schema}://{authtoken}/?{params}'.format( params=NotifyChantify.urlencode(params),
schema=self.protocol,
authtoken=self.pprint(self.authtoken, privacy, safe=''),
params=NotifyFaast.urlencode(params),
) )
@staticmethod @staticmethod
@ -200,16 +188,19 @@ class NotifyFaast(NotifyBase):
us to re-instantiate this object. us to re-instantiate this object.
""" """
# parse_url already handles getting the `user` and `password` fields
# populated.
results = NotifyBase.parse_url(url, verify_host=False) results = NotifyBase.parse_url(url, verify_host=False)
if not results: if not results:
# We're done early as we couldn't load the results # We're done early as we couldn't load the results
return results return results
# Store our authtoken using the host # Allow over-ride
results['authtoken'] = NotifyFaast.unquote(results['host']) if 'token' in results['qsd'] and len(results['qsd']['token']):
results['token'] = NotifyChantify.unquote(results['qsd']['token'])
# Include image with our post else:
results['include_image'] = \ results['token'] = NotifyChantify.unquote(results['host'])
parse_bool(results['qsd'].get('image', True))
return results return results

View File

@ -45,7 +45,7 @@ from .NotifyBase import NotifyBase
from ..URLBase import PrivacyMode from ..URLBase import PrivacyMode
from ..common import NotifyFormat, NotifyType from ..common import NotifyFormat, NotifyType
from ..conversion import convert_between from ..conversion import convert_between
from ..utils import is_email, parse_emails from ..utils import is_email, parse_emails, is_hostname
from ..AppriseLocale import gettext_lazy as _ from ..AppriseLocale import gettext_lazy as _
from ..logger import logger from ..logger import logger
@ -566,13 +566,21 @@ class NotifyEmail(NotifyBase):
# Apply any defaults based on certain known configurations # Apply any defaults based on certain known configurations
self.NotifyEmailDefaults(secure_mode=secure_mode, **kwargs) self.NotifyEmailDefaults(secure_mode=secure_mode, **kwargs)
if self.user and self.host: if self.user:
if self.host:
# Prepare the bases of our email # Prepare the bases of our email
self.from_addr = [self.app_id, '{}@{}'.format( self.from_addr = [self.app_id, '{}@{}'.format(
re.split(r'[\s@]+', self.user)[0], re.split(r'[\s@]+', self.user)[0],
self.host, self.host,
)] )]
else:
result = is_email(self.user)
if result:
# Prepare the bases of our email and include domain
self.host = result['domain']
self.from_addr = [self.app_id, self.user]
if from_addr: if from_addr:
result = is_email(from_addr) result = is_email(from_addr)
if result: if result:
@ -1037,11 +1045,25 @@ class NotifyEmail(NotifyBase):
us to re-instantiate this object. us to re-instantiate this object.
""" """
results = NotifyBase.parse_url(url) results = NotifyBase.parse_url(url, verify_host=False)
if not results: if not results:
# We're done early as we couldn't load the results # We're done early as we couldn't load the results
return results return results
# Prepare our target lists
results['targets'] = []
if not is_hostname(results['host'], ipv4=False, ipv6=False,
underscore=False):
if is_email(NotifyEmail.unquote(results['host'])):
# Don't lose defined email addresses
results['targets'].append(NotifyEmail.unquote(results['host']))
# Detect if we have a valid hostname or not; be sure to reset it's
# value if invalid; we'll attempt to figure this out later on
results['host'] = ''
# The From address is a must; either through the use of templates # The From address is a must; either through the use of templates
# from= entry and/or merging the user and hostname together, this # from= entry and/or merging the user and hostname together, this
# must be calculated or parse_url will fail. # must be calculated or parse_url will fail.
@ -1052,7 +1074,7 @@ class NotifyEmail(NotifyBase):
# Get our potential email targets; if none our found we'll just # Get our potential email targets; if none our found we'll just
# add one to ourselves # add one to ourselves
results['targets'] = NotifyEmail.split_path(results['fullpath']) results['targets'] += NotifyEmail.split_path(results['fullpath'])
# Attempt to detect 'to' email address # Attempt to detect 'to' email address
if 'to' in results['qsd'] and len(results['qsd']['to']): if 'to' in results['qsd'] and len(results['qsd']['to']):

View File

@ -0,0 +1,231 @@
# -*- 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.
# Feishu
# 1. Visit https://open.feishu.cn
# Custom Bot Setup
# https://open.feishu.cn/document/client-docs/bot-v3/add-custom-bot
#
import requests
from json import dumps
from .NotifyBase import NotifyBase
from ..common import NotifyType
from ..utils import validate_regex
from ..AppriseLocale import gettext_lazy as _
class NotifyFeishu(NotifyBase):
"""
A wrapper for Feishu Notifications
"""
# The default descriptive name associated with the Notification
service_name = _('Feishu')
# The services URL
service_url = 'https://open.feishu.cn/'
# The default secure protocol
secure_protocol = 'feishu'
# A URL that takes you to the setup/help of the specific protocol
setup_url = 'https://github.com/caronc/apprise/wiki/Notify_feishu'
# Notification URL
notify_url = 'https://open.feishu.cn/open-apis/bot/v2/hook/{token}/'
# Define object templates
templates = (
'{schema}://{token}',
)
# The title is not used
title_maxlen = 0
# Limit is documented to be 20K message sizes. This number safely
# allows padding around that size.
body_maxlen = 19985
# Define our tokens; these are the minimum tokens required required to
# be passed into this function (as arguments). The syntax appends any
# previously defined in the base package and builds onto them
template_tokens = dict(NotifyBase.template_tokens, **{
'token': {
'name': _('Token'),
'type': 'string',
'private': True,
'required': True,
'regex': (r'^[A-Z0-9_-]+$', 'i'),
},
})
# Define our template arguments
template_args = dict(NotifyBase.template_args, **{
'token': {
'alias_of': 'token',
},
})
def __init__(self, token, **kwargs):
"""
Initialize Feishu Object
"""
super().__init__(**kwargs)
self.token = validate_regex(
token, *self.template_tokens['token']['regex'])
if not self.token:
msg = 'The Feishu token specified ({}) is invalid.'\
.format(token)
self.logger.warning(msg)
raise TypeError(msg)
return
def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs):
"""
Send our notification
"""
# prepare our headers
headers = {
'User-Agent': self.app_id,
'Content-Type': "application/json",
}
# Our Message
payload = {
'msg_type': 'text',
"content": {
"text": body,
}
}
self.logger.debug('Feishu GET URL: %s (cert_verify=%r)' % (
self.notify_url, self.verify_certificate))
self.logger.debug('Feishu Payload: %s' % str(payload))
# Always call throttle before any remote server i/o is made
self.throttle()
try:
r = requests.post(
self.notify_url.format(token=self.token),
data=dumps(payload).encode('utf-8'),
headers=headers,
verify=self.verify_certificate,
timeout=self.request_timeout,
)
#
# Sample Responses
#
# Valid:
# {
# "code": 0,
# "data": {},
# "msg": "success"
# }
# Invalid (non 200 response):
# {
# "code": 9499,
# "msg": "Bad Request",
# "data": {}
# }
if r.status_code != requests.codes.ok:
# We had a problem
status_str = \
NotifyFeishu.http_response_code_lookup(r.status_code)
self.logger.warning(
'Failed to send Feishu notification: '
'{}{}error={}.'.format(
status_str,
', ' if status_str else '',
r.status_code))
self.logger.debug('Response Details:\r\n{}'.format(r.content))
# Return; we're done
return False
else:
self.logger.info('Sent Feishu notification.')
except requests.RequestException as e:
self.logger.warning(
'A Connection error occurred sending Feishu '
'notification.')
self.logger.debug('Socket Exception: %s' % str(e))
# Return; we're done
return False
return True
def url(self, privacy=False, *args, **kwargs):
"""
Returns the URL built dynamically based on specified arguments.
"""
# Prepare our parameters
params = self.url_parameters(privacy=privacy, *args, **kwargs)
return '{schema}://{token}/?{params}'.format(
schema=self.secure_protocol,
token=self.pprint(self.token, privacy, safe=''),
params=NotifyFeishu.urlencode(params),
)
@staticmethod
def parse_url(url):
"""
Parses the URL and returns enough arguments that can allow
us to re-instantiate this object.
"""
# parse_url already handles getting the `user` and `password` fields
# populated.
results = NotifyBase.parse_url(url, verify_host=False)
if not results:
# We're done early as we couldn't load the results
return results
# Allow over-ride
if 'token' in results['qsd'] and len(results['qsd']['token']):
results['token'] = NotifyFeishu.unquote(results['qsd']['token'])
else:
results['token'] = NotifyFeishu.unquote(results['host'])
return results

View File

@ -0,0 +1,204 @@
# -*- 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.
# Free Mobile
# 1. Visit https://mobile.free.fr/
# the URL will look something like this:
# https://smsapi.free-mobile.fr/sendmsg
#
import requests
from json import dumps
from .NotifyBase import NotifyBase
from ..common import NotifyType
from ..AppriseLocale import gettext_lazy as _
class NotifyFreeMobile(NotifyBase):
"""
A wrapper for Free-Mobile Notifications
"""
# The default descriptive name associated with the Notification
service_name = _('Free-Mobile')
# The services URL
service_url = 'https://mobile.free.fr/'
# The default secure protocol
secure_protocol = 'freemobile'
# A URL that takes you to the setup/help of the specific protocol
setup_url = 'https://github.com/caronc/apprise/wiki/Notify_freemobile'
# Plain Text Notification URL
notify_url = 'https://smsapi.free-mobile.fr/sendmsg'
# Define object templates
templates = (
'{schema}://{user}@{password}',
)
# The title is not used
title_maxlen = 0
# SMS Messages are restricted in size
body_maxlen = 160
# Define our tokens; these are the minimum tokens required required to
# be passed into this function (as arguments). The syntax appends any
# previously defined in the base package and builds onto them
template_tokens = dict(NotifyBase.template_tokens, **{
'user': {
'name': _('Username'),
'type': 'string',
'required': True,
},
'password': {
'name': _('Password'),
'type': 'string',
'private': True,
'required': True,
},
})
def __init__(self, **kwargs):
"""
Initialize Free Mobile Object
"""
super().__init__(**kwargs)
if not (self.user and self.password):
msg = 'A FreeMobile user and password ' \
'combination was not provided.'
self.logger.warning(msg)
raise TypeError(msg)
return
def url(self, privacy=False, *args, **kwargs):
"""
Returns the URL built dynamically based on specified arguments.
"""
# Prepare our parameters
params = self.url_parameters(privacy=privacy, *args, **kwargs)
return '{schema}://{user}@{password}/?{params}'.format(
schema=self.secure_protocol,
user=self.user,
password=self.pprint(self.password, privacy, safe=''),
params=NotifyFreeMobile.urlencode(params),
)
def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs):
"""
Send our notification
"""
# prepare our headers
headers = {
'User-Agent': self.app_id,
}
# Prepare our payload
payload = {
'user': self.user,
'pass': self.password,
'msg': body
}
self.logger.debug('Free Mobile GET URL: %s (cert_verify=%r)' % (
self.notify_url, self.verify_certificate))
self.logger.debug('Free Mobile Payload: %s' % str(payload))
# Always call throttle before any remote server i/o is made
self.throttle()
try:
r = requests.post(
self.notify_url,
data=dumps(payload).encode('utf-8'),
headers=headers,
verify=self.verify_certificate,
timeout=self.request_timeout,
)
if r.status_code != requests.codes.ok:
# We had a problem
status_str = \
NotifyFreeMobile.http_response_code_lookup(r.status_code)
self.logger.warning(
'Failed to send Free Mobile notification: '
'{}{}error={}.'.format(
status_str,
', ' if status_str else '',
r.status_code))
self.logger.debug('Response Details:\r\n{}'.format(r.content))
# Return; we're done
return False
else:
self.logger.info('Sent Free Mobile notification.')
except requests.RequestException as e:
self.logger.warning(
'A Connection error occurred sending Free Mobile '
'notification.')
self.logger.debug('Socket Exception: %s' % str(e))
# Return; we're done
return False
return True
@staticmethod
def parse_url(url):
"""
Parses the URL and returns enough arguments that can allow
us to re-instantiate this object.
"""
# parse_url already handles getting the `user` and `password` fields
# populated.
results = NotifyBase.parse_url(url, verify_host=False)
if not results:
# We're done early as we couldn't load the results
return results
# The hostname can act as the password if specified and a password
# was otherwise not (specified):
if not results.get('password'):
results['password'] = NotifyFreeMobile.unquote(results['host'])
return results

View File

@ -89,7 +89,7 @@ class NotifyMQTT(NotifyBase):
requirements = { requirements = {
# Define our required packaging in order to work # Define our required packaging in order to work
'packages_required': 'paho-mqtt' 'packages_required': 'paho-mqtt < 2.0.0'
} }
# The default descriptive name associated with the Notification # The default descriptive name associated with the Notification

View File

@ -698,7 +698,7 @@ class NotifyNtfy(NotifyBase):
""" """
Returns the number of targets associated with this notification Returns the number of targets associated with this notification
""" """
return len(self.topics) return 1 if not self.topics else len(self.topics)
@staticmethod @staticmethod
def parse_url(url): def parse_url(url):

View File

@ -59,6 +59,9 @@ class RocketChatAuthMode:
# providing a webhook # providing a webhook
WEBHOOK = "webhook" WEBHOOK = "webhook"
# Support token submission
TOKEN = "token"
# Providing a username and password (default) # Providing a username and password (default)
BASIC = "basic" BASIC = "basic"
@ -66,6 +69,7 @@ class RocketChatAuthMode:
# Define our authentication modes # Define our authentication modes
ROCKETCHAT_AUTH_MODES = ( ROCKETCHAT_AUTH_MODES = (
RocketChatAuthMode.WEBHOOK, RocketChatAuthMode.WEBHOOK,
RocketChatAuthMode.TOKEN,
RocketChatAuthMode.BASIC, RocketChatAuthMode.BASIC,
) )
@ -107,6 +111,8 @@ class NotifyRocketChat(NotifyBase):
templates = ( templates = (
'{schema}://{user}:{password}@{host}:{port}/{targets}', '{schema}://{user}:{password}@{host}:{port}/{targets}',
'{schema}://{user}:{password}@{host}/{targets}', '{schema}://{user}:{password}@{host}/{targets}',
'{schema}://{user}:{token}@{host}:{port}/{targets}',
'{schema}://{user}:{token}@{host}/{targets}',
'{schema}://{webhook}@{host}', '{schema}://{webhook}@{host}',
'{schema}://{webhook}@{host}:{port}', '{schema}://{webhook}@{host}:{port}',
'{schema}://{webhook}@{host}/{targets}', '{schema}://{webhook}@{host}/{targets}',
@ -135,6 +141,11 @@ class NotifyRocketChat(NotifyBase):
'type': 'string', 'type': 'string',
'private': True, 'private': True,
}, },
'token': {
'name': _('API Token'),
'map_to': 'password',
'private': True,
},
'webhook': { 'webhook': {
'name': _('Webhook'), 'name': _('Webhook'),
'type': 'string', 'type': 'string',
@ -230,13 +241,20 @@ class NotifyRocketChat(NotifyBase):
if self.webhook is not None: if self.webhook is not None:
# Just a username was specified, we treat this as a webhook # Just a username was specified, we treat this as a webhook
self.mode = RocketChatAuthMode.WEBHOOK self.mode = RocketChatAuthMode.WEBHOOK
elif self.password and len(self.password) > 32:
self.mode = RocketChatAuthMode.TOKEN
else: else:
self.mode = RocketChatAuthMode.BASIC self.mode = RocketChatAuthMode.BASIC
if self.mode == RocketChatAuthMode.BASIC \ self.logger.debug(
"Auto-Detected Rocketchat Auth Mode: %s", self.mode)
if self.mode in (RocketChatAuthMode.BASIC, RocketChatAuthMode.TOKEN) \
and not (self.user and self.password): and not (self.user and self.password):
# Username & Password is required for Rocket Chat to work # Username & Password is required for Rocket Chat to work
msg = 'No Rocket.Chat user/pass combo was specified.' msg = 'No Rocket.Chat {} was specified.'.format(
'user/pass combo' if self.mode == RocketChatAuthMode.BASIC else
'user/apikey')
self.logger.warning(msg) self.logger.warning(msg)
raise TypeError(msg) raise TypeError(msg)
@ -245,6 +263,13 @@ class NotifyRocketChat(NotifyBase):
self.logger.warning(msg) self.logger.warning(msg)
raise TypeError(msg) raise TypeError(msg)
if self.mode == RocketChatAuthMode.TOKEN:
# Set our headers for further communication
self.headers.update({
'X-User-Id': self.user,
'X-Auth-Token': self.password,
})
# Validate recipients and drop bad ones: # Validate recipients and drop bad ones:
for recipient in parse_list(targets): for recipient in parse_list(targets):
result = IS_CHANNEL.match(recipient) result = IS_CHANNEL.match(recipient)
@ -309,12 +334,13 @@ class NotifyRocketChat(NotifyBase):
params.update(self.url_parameters(privacy=privacy, *args, **kwargs)) params.update(self.url_parameters(privacy=privacy, *args, **kwargs))
# Determine Authentication # Determine Authentication
if self.mode == RocketChatAuthMode.BASIC: if self.mode in (RocketChatAuthMode.BASIC, RocketChatAuthMode.TOKEN):
auth = '{user}:{password}@'.format( auth = '{user}:{password}@'.format(
user=NotifyRocketChat.quote(self.user, safe=''), user=NotifyRocketChat.quote(self.user, safe=''),
password=self.pprint( password=self.pprint(
self.password, privacy, mode=PrivacyMode.Secret, safe=''), self.password, privacy, mode=PrivacyMode.Secret, safe=''),
) )
else: else:
auth = '{user}{webhook}@'.format( auth = '{user}{webhook}@'.format(
user='{}:'.format(NotifyRocketChat.quote(self.user, safe='')) user='{}:'.format(NotifyRocketChat.quote(self.user, safe=''))
@ -359,7 +385,10 @@ class NotifyRocketChat(NotifyBase):
# Call the _send_ function applicable to whatever mode we're in # Call the _send_ function applicable to whatever mode we're in
# - calls _send_webhook_notification if the mode variable is set # - calls _send_webhook_notification if the mode variable is set
# - calls _send_basic_notification if the mode variable is not set # - calls _send_basic_notification if the mode variable is not set
return getattr(self, '_send_{}_notification'.format(self.mode))( return getattr(self, '_send_{}_notification'.format(
RocketChatAuthMode.WEBHOOK
if self.mode == RocketChatAuthMode.WEBHOOK
else RocketChatAuthMode.BASIC))(
body=body, title=title, notify_type=notify_type, **kwargs) body=body, title=title, notify_type=notify_type, **kwargs)
def _send_webhook_notification(self, body, title='', def _send_webhook_notification(self, body, title='',
@ -412,7 +441,7 @@ class NotifyRocketChat(NotifyBase):
""" """
# Track whether we authenticated okay # Track whether we authenticated okay
if not self.login(): if self.mode == RocketChatAuthMode.BASIC and not self.login():
return False return False
# prepare JSON Object # prepare JSON Object
@ -432,9 +461,7 @@ class NotifyRocketChat(NotifyBase):
channel = channels.pop(0) channel = channels.pop(0)
payload['channel'] = channel payload['channel'] = channel
if not self._send( if not self._send(payload, notify_type=notify_type, **kwargs):
payload, notify_type=notify_type, headers=self.headers,
**kwargs):
# toggle flag # toggle flag
has_error = True has_error = True
@ -447,13 +474,12 @@ class NotifyRocketChat(NotifyBase):
room = rooms.pop(0) room = rooms.pop(0)
payload['roomId'] = room payload['roomId'] = room
if not self._send( if not self._send(payload, notify_type=notify_type, **kwargs):
payload, notify_type=notify_type, headers=self.headers,
**kwargs):
# toggle flag # toggle flag
has_error = True has_error = True
if self.mode == RocketChatAuthMode.BASIC:
# logout # logout
self.logout() self.logout()
@ -476,7 +502,7 @@ class NotifyRocketChat(NotifyBase):
return payload return payload
def _send(self, payload, notify_type, path='api/v1/chat.postMessage', def _send(self, payload, notify_type, path='api/v1/chat.postMessage',
headers={}, **kwargs): **kwargs):
""" """
Perform Notify Rocket.Chat Notification Perform Notify Rocket.Chat Notification
""" """
@ -487,6 +513,9 @@ class NotifyRocketChat(NotifyBase):
api_url, self.verify_certificate)) api_url, self.verify_certificate))
self.logger.debug('Rocket.Chat Payload: %s' % str(payload)) self.logger.debug('Rocket.Chat Payload: %s' % str(payload))
# Copy our existing headers
headers = self.headers.copy()
# Apply minimum headers # Apply minimum headers
headers.update({ headers.update({
'User-Agent': self.app_id, 'User-Agent': self.app_id,

View File

@ -82,6 +82,34 @@ IS_CHAT_ID_RE = re.compile(
) )
class TelegramMarkdownVersion:
"""
Telegram Markdown Version
"""
# Classic (Original Telegram Markdown)
ONE = 'MARKDOWN'
# Supports strikethrough and many other items
TWO = 'MarkdownV2'
TELEGRAM_MARKDOWN_VERSION_MAP = {
# v1
"v1": TelegramMarkdownVersion.ONE,
"1": TelegramMarkdownVersion.ONE,
# v2
"v2": TelegramMarkdownVersion.TWO,
"2": TelegramMarkdownVersion.TWO,
"default": TelegramMarkdownVersion.TWO,
}
TELEGRAM_MARKDOWN_VERSIONS = {
# Note: This also acts as a reverse lookup mapping
TelegramMarkdownVersion.ONE: 'v1',
TelegramMarkdownVersion.TWO: 'v2',
}
class TelegramContentPlacement: class TelegramContentPlacement:
""" """
The Telegram Content Placement The Telegram Content Placement
@ -333,6 +361,12 @@ class NotifyTelegram(NotifyBase):
'name': _('Topic Thread ID'), 'name': _('Topic Thread ID'),
'type': 'int', 'type': 'int',
}, },
'mdv': {
'name': _('Markdown Version'),
'type': 'choice:string',
'values': ('v1', 'v2'),
'default': 'v2',
},
'to': { 'to': {
'alias_of': 'targets', 'alias_of': 'targets',
}, },
@ -346,7 +380,7 @@ class NotifyTelegram(NotifyBase):
def __init__(self, bot_token, targets, detect_owner=True, def __init__(self, bot_token, targets, detect_owner=True,
include_image=False, silent=None, preview=None, topic=None, include_image=False, silent=None, preview=None, topic=None,
content=None, **kwargs): content=None, mdv=None, **kwargs):
""" """
Initialize Telegram Object Initialize Telegram Object
""" """
@ -361,6 +395,17 @@ class NotifyTelegram(NotifyBase):
self.logger.warning(err) self.logger.warning(err)
raise TypeError(err) raise TypeError(err)
# Get our Markdown Version
self.markdown_ver = \
TELEGRAM_MARKDOWN_VERSION_MAP[NotifyTelegram.
template_args['mdv']['default']] \
if mdv is None else \
next((
v for k, v in TELEGRAM_MARKDOWN_VERSION_MAP.items()
if str(mdv).lower().startswith(k)),
TELEGRAM_MARKDOWN_VERSION_MAP[NotifyTelegram.
template_args['mdv']['default']])
# Define whether or not we should make audible alarms # Define whether or not we should make audible alarms
self.silent = self.template_args['silent']['default'] \ self.silent = self.template_args['silent']['default'] \
if silent is None else bool(silent) if silent is None else bool(silent)
@ -717,8 +762,7 @@ class NotifyTelegram(NotifyBase):
# Prepare Message Body # Prepare Message Body
if self.notify_format == NotifyFormat.MARKDOWN: if self.notify_format == NotifyFormat.MARKDOWN:
_payload['parse_mode'] = 'MARKDOWN' _payload['parse_mode'] = self.markdown_ver
_payload['text'] = body _payload['text'] = body
else: # HTML else: # HTML
@ -886,6 +930,7 @@ class NotifyTelegram(NotifyBase):
'silent': 'yes' if self.silent else 'no', 'silent': 'yes' if self.silent else 'no',
'preview': 'yes' if self.preview else 'no', 'preview': 'yes' if self.preview else 'no',
'content': self.content, 'content': self.content,
'mdv': TELEGRAM_MARKDOWN_VERSIONS[self.markdown_ver],
} }
if self.topic: if self.topic:
@ -990,6 +1035,10 @@ class NotifyTelegram(NotifyBase):
# Store our bot token # Store our bot token
results['bot_token'] = bot_token results['bot_token'] = bot_token
# Support Markdown Version
if 'mdv' in results['qsd'] and len(results['qsd']['mdv']):
results['mdv'] = results['qsd']['mdv']
# Support Thread Topic # Support Thread Topic
if 'topic' in results['qsd'] and len(results['qsd']['topic']): if 'topic' in results['qsd'] and len(results['qsd']['topic']):
results['topic'] = results['qsd']['topic'] results['topic'] = results['qsd']['topic']

View File

@ -163,6 +163,9 @@ class NotifyZulip(NotifyBase):
'to': { 'to': {
'alias_of': 'targets', 'alias_of': 'targets',
}, },
'token': {
'alias_of': 'token',
},
}) })
# The default hostname to append to a defined organization # The default hostname to append to a defined organization
@ -377,21 +380,24 @@ class NotifyZulip(NotifyBase):
# The botname # The botname
results['botname'] = NotifyZulip.unquote(results['user']) results['botname'] = NotifyZulip.unquote(results['user'])
# The first token is stored in the hostname # The organization is stored in the hostname
results['organization'] = NotifyZulip.unquote(results['host']) results['organization'] = NotifyZulip.unquote(results['host'])
# Now fetch the remaining tokens # Store our targets
try: results['targets'] = NotifyZulip.split_path(results['fullpath'])
results['token'] = \
NotifyZulip.split_path(results['fullpath'])[0]
except IndexError: if 'token' in results['qsd'] and len(results['qsd']['token']):
# Store our token if specified
results['token'] = NotifyZulip.unquote(results['qsd']['token'])
elif results['targets']:
# First item is the token
results['token'] = results['targets'].pop(0)
else:
# no token # no token
results['token'] = None results['token'] = None
# Get unquoted entries
results['targets'] = NotifyZulip.split_path(results['fullpath'])[1:]
# Support the 'to' variable so that we can support rooms this way too # Support the 'to' variable so that we can support rooms this way too
# The 'to' makes it easier to use yaml configuration # The 'to' makes it easier to use yaml configuration
if 'to' in results['qsd'] and len(results['qsd']['to']): if 'to' in results['qsd'] and len(results['qsd']['to']):

View File

@ -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.4 apprise==1.7.6
apscheduler<=3.10.4 apscheduler<=3.10.4
attrs==23.2.0 attrs==23.2.0
blinker==1.7.0 blinker==1.7.0