mirror of
https://github.com/morpheus65535/bazarr.git
synced 2025-04-24 06:37:16 -04:00
no log: latest apprise upgrade
This commit is contained in:
parent
20d235e1b5
commit
e5db62eb95
20 changed files with 762 additions and 617 deletions
|
@ -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: lead2gold@gmail.com
|
||||
License: BSD
|
||||
Keywords: Alerts Apprise API Automated Packet Reporting System AWS Boxcar BulkSMS BulkVS Burst SMS Chat CLI ClickSend D7Networks Dapnet DBus DingTalk Discord Email Emby Enigma2 Faast FCM Flock Form Gnome Google Chat Gotify Growl Guilded Home Assistant httpSMS IFTTT Join JSON Kavenegar KODI Kumulos LaMetric Line MacOSX Mailgun Mastodon Matrix Mattermost MessageBird Microsoft Misskey MQTT MSG91 MSTeams Nextcloud NextcloudTalk Notica Notifiarr Notifico Ntfy Office365 OneSignal Opsgenie PagerDuty PagerTree ParsePlatform PopcornNotify Prowl PushBullet Pushed Pushjet PushMe Push Notifications Pushover PushSafer Pushy PushDeer Reddit 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'
|
|
@ -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
|
|
@ -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()
|
||||
|
|
|
@ -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 <lead2gold@gmail.com>'
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
|
||||
def load_modules(self, path=None, name=None):
|
||||
# Reset our path cache
|
||||
self._paths_previously_scanned = set()
|
||||
|
||||
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)
|
||||
# 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
|
||||
|
||||
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
|
||||
if not self:
|
||||
# Initialize our maps
|
||||
self._module_map = {}
|
||||
self._schema_map = {}
|
||||
self._custom_module_map = {}
|
||||
|
||||
elif match.group('name') == f'{self.fname_prefix}Base':
|
||||
# keep going
|
||||
continue
|
||||
# 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)
|
||||
|
||||
# Store our notification/plugin name:
|
||||
module_name = match.group('name')
|
||||
module_pyname = '{}.{}'.format(module_name_prefix, module_name)
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
# 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
|
||||
|
||||
# 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))
|
||||
elif match.group('name') == f'{self.fname_prefix}Base':
|
||||
# keep going
|
||||
continue
|
||||
|
||||
# Assign plugin
|
||||
self._schema_map[schema] = plugin
|
||||
# Store our notification/plugin name:
|
||||
module_name = match.group('name')
|
||||
module_pyname = '{}.{}'.format(module_name_prefix, module_name)
|
||||
|
||||
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)))
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
# 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
|
||||
|
||||
# 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)))
|
||||
|
||||
# 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
|
||||
|
||||
# Store our path as a way of hashing it has been handled
|
||||
self._paths_previously_scanned.add(path)
|
||||
|
||||
if os.path.isdir(path) and not \
|
||||
os.path.isfile(os.path.join(path, '__init__.py')):
|
||||
|
||||
logger.debug('Scanning for custom plugins in: %s', path)
|
||||
for entry in os.listdir(path):
|
||||
re_match = module_re.match(entry)
|
||||
if not re_match:
|
||||
# keep going
|
||||
logger.trace('Plugin Scan: Ignoring %s', entry)
|
||||
continue
|
||||
|
||||
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
|
||||
|
||||
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)
|
||||
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
|
||||
|
||||
# Load our module
|
||||
_import_module(path)
|
||||
# Store our path as a way of hashing it has been handled
|
||||
self._paths_previously_scanned.add(path)
|
||||
|
||||
return None
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
self._paths_previously_scanned.add(path)
|
||||
|
||||
# directly load as is
|
||||
re_match = module_re.match(os.path.basename(path))
|
||||
# must be a match and must have a .py extension
|
||||
if not re_match or not re_match.group(1):
|
||||
# keep going
|
||||
logger.trace('Plugin Scan: Ignoring %s', path)
|
||||
continue
|
||||
|
||||
# Load our module
|
||||
_import_module(path)
|
||||
|
||||
return None
|
||||
|
||||
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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -1,372 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# BSD 2-Clause License
|
||||
#
|
||||
# Apprise - Push Notification Library.
|
||||
# Copyright (c) 2023, 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.
|
||||
|
||||
# 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
|
|
@ -2,7 +2,7 @@
|
|||
# BSD 2-Clause License
|
||||
#
|
||||
# Apprise - Push Notification Library.
|
||||
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
|
||||
# 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:
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
437
libs/apprise/plugins/NotifyRevolt.py
Normal file
437
libs/apprise/plugins/NotifyRevolt.py
Normal file
|
@ -0,0 +1,437 @@
|
|||
# -*- 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.
|
||||
|
||||
# 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
|
|
@ -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):
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue