Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Apprise Integration #2796

Merged
merged 10 commits into from
Feb 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/build_release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ jobs:
python3 --version
pip3 install --upgrade pip wheel

pip3 install --upgrade -r requirements.txt --no-binary cffi --no-dependencies
pip3 install --upgrade -r requirements.txt --no-binary cffi,PyYAML --no-dependencies

pip3 uninstall cryptography -y
pip3 download -r builder/osx/requirements.txt --platform macosx_10_12_universal2 --only-binary :all: --no-dependencies --dest .
Expand Down
1 change: 0 additions & 1 deletion builder/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ pyinstaller-hooks-contrib==2024.1
altgraph==0.17.4
wrapt==1.16.0
setuptools==69.1.0
certifi

# Required on 32bit Windows, exclude it based on Python-version
importlib_metadata==7.0.1; python_version < '3.10'
Expand Down
Binary file added icons/apprise/apprise-failure.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added icons/apprise/apprise-info.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added icons/apprise/apprise-success.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added icons/apprise/apprise-warning.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
45 changes: 44 additions & 1 deletion interfaces/Config/templates/config_notify.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,49 @@
</div>
</div>
<!--#end if#-->
<div class="section" id="apprise">
<div class="col2">
<h3>$T('section-Apprise')</h3>
<table>
<tr>
<td><input type="checkbox" name="apprise_enable" id="apprise_enable" value="1" <!--#if int($apprise_enable) > 0 then 'checked="checked"' else ""#--> /></td>
<td><label for="apprise_enable"> $T('opt-apprise_enable')</label></td>
</tr>
</table>
<em>$T('explain-apprise_enable')</em>
$show_cat_box('apprise')
</div>
<div class="col1" <!--#if int($apprise_enable) > 0 then '' else 'style="display:none"'#-->>
<fieldset>
<div class="field-pair">
<label class="config" for="apprise_urls">$T('opt-apprise_urls')</label>
<input type="text" name="apprise_urls" id="apprise_urls" value="$apprise_urls" />
<span class="desc">$T('explain-apprise_urls'). <br>$T('readwiki')</span>
</div>
<div class="field-pair">
<span class="desc">$T('explain-apprise_extra_urls')</span>
</div>
<!--#set $section_label = 'apprise'#-->
<!--#for $type in $notify_types#-->
<div class="field-pair">
<label class="config" for="${section_label}_target_${type}">
$T($notify_types[$type]).replace('/', ' / ')
</label>
<input type="checkbox" name="${section_label}_target_${type}_enable" id="${section_label}_target_${type}_enable" value="1" <!--#if int($getVar($section_label + '_target_' + $type + '_enable')) > 0 then 'checked="checked"' else ""#--> />
<input type="text" name="${section_label}_target_${type}" id="${section_label}_target_${type}" value="$getVar($section_label + '_target_' + $type)" placeholder="$T('opt-apprise_urls')" />
</div>
<!--#end for#-->

<div class="field-pair no-field-pair-bg">
<button class="btn btn-default saveButton"><span class="glyphicon glyphicon-ok"></span> $T('button-saveChanges')</button>
<button class="btn btn-default" type="button" id="test_apprise"><span class="glyphicon glyphicon-comment"></span> $T('testNotify')</button>
</div>
<div class="field-pair result-box">
<div class="alert"></div>
</div>
</fieldset>
</div>
</div>
<div class="section" id="nscript">
<div class="col2">
<h3>$T('section-NScript') <a href="$help_uri#nscript" target="_blank"><span class="glyphicon glyphicon-question-sign"></span></a></h3>
Expand Down Expand Up @@ -426,7 +469,7 @@ jQuery(document).ready(function(){
}
})
}
jQuery('#test_email, #test_notif, #test_windows, #test_pushbullet, #test_pushover, #test_prowl, #test_osd, #test_nscript').click(function () {
jQuery('#test_email, #test_notif, #test_windows, #test_apprise, #test_pushbullet, #test_pushover, #test_prowl, #test_osd, #test_nscript').click(function () {
testNotification(this)
})
});
Expand Down
19 changes: 18 additions & 1 deletion requirements.txt
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pyYAML is needed by Apprise. Zoggys advice to define these 2 entries was a really good one. I don't think we should remove it from the requirements.txt.

Also, the way it was gave every system that had the ability to leverage the binary package to do so yet still be compatible for the systems that couldn't

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# Main requirements
# Note that not all sub-dependencies are listed, but only ones we know could cause trouble
apprise==1.7.2
sabctools==8.1.0
cheetah3==3.2.6.post1
cffi==1.16.0
Expand Down Expand Up @@ -48,6 +49,22 @@ pyobjc-framework-Cocoa==10.1; sys_platform == 'darwin'
# Linux notifications
notify2==0.3.1; sys_platform != 'win32' and sys_platform != 'darwin'

# Apprise Requirements
requests==2.31.0
requests-oauthlib==1.3.1
Safihre marked this conversation as resolved.
Show resolved Hide resolved
PyYAML==6.0.1
markdown==3.5.2
paho-mqtt==1.6.1

# Requests Requirements
charset_normalizer==3.3.2
idna==3.6
urllib3==2.2.0
certifi==2024.2.2
oauthlib==3.2.2
PyJWT==2.8.0
blinker==1.7.0

# Optional support for *nix tray icon.
# Note that pygobject depends on pycairo, which requires pkg-config and cairo headers.
# See https://pycairo.readthedocs.io/en/latest/getting_started.html
Expand All @@ -57,4 +74,4 @@ notify2==0.3.1; sys_platform != 'win32' and sys_platform != 'darwin'
# Optional support for system power management on *nix.
# Requires libdbus-1-dev to be installed.
# Uncomment line below or manually install after installing requirements.
# dbus-python; sys_platform != 'win32' and sys_platform != 'darwin'
# dbus-python; sys_platform != 'win32' and sys_platform != 'darwin'
10 changes: 9 additions & 1 deletion sabnzbd/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -657,7 +657,7 @@ def _api_warnings(name, kwargs):
LOG_JSON_RE = re.compile(rb"'(apikey|api|username|password)': '(.*?)'", re.I)
LOG_INI_HIDE_RE = re.compile(
rb"(apikey|api|user|username|password|email_pwd|email_account|email_to|email_from|pushover_token|pushover_userkey"
rb"|pushbullet_apikey|prowl_apikey|growl_password|growl_server|IPv[4|6] address|Public address IPv[4|6]-only|Local IPv6 address)\s?=.*",
rb"|apprise_(target_[a-z_]+|urls)|pushbullet_apikey|prowl_apikey|growl_password|growl_server|IPv[4|6] address|Public address IPv[4|6]-only|Local IPv6 address)\s?=.*",
re.I,
)
LOG_HASH_RE = re.compile(rb"([a-zA-Z\d]{25})", re.I)
Expand Down Expand Up @@ -851,6 +851,13 @@ def _api_test_pushbullet(name, kwargs):
return report(error=res)


def _api_test_apprise(name, kwargs):
"""API: send a test Apprise notification, return result"""
logging.info("Sending Apprise notification")
res = sabnzbd.notifier.send_apprise("SABnzbd", T("Test Notification"), "other", force=True, test=kwargs)
return report(error=res)


def _api_test_nscript(name, kwargs):
"""API: execute a test notification script, return result"""
logging.info("Executing notification script")
Expand Down Expand Up @@ -1023,6 +1030,7 @@ def _api_gc_stats(name, kwargs):
"test_osd": (_api_test_osd, 3),
"test_pushover": (_api_test_pushover, 3),
"test_pushbullet": (_api_test_pushbullet, 3),
"test_apprise": (_api_test_apprise, 3),
"test_prowl": (_api_test_prowl, 3),
"test_nscript": (_api_test_nscript, 3),
}
Expand Down
29 changes: 29 additions & 0 deletions sabnzbd/cfg.py
Original file line number Diff line number Diff line change
Expand Up @@ -613,6 +613,35 @@ def validate_default_if_empty(root: str, value: str, default: str) -> Tuple[None
pushbullet_prio_queue_done = OptionBool("pushbullet", "pushbullet_prio_queue_done", False)
pushbullet_prio_other = OptionBool("pushbullet", "pushbullet_prio_other", True)

# [apprise]
apprise_enable = OptionBool("apprise", "apprise_enable")
apprise_cats = OptionList("apprise", "apprise_cats", ["*"])
apprise_urls = OptionStr("apprise", "apprise_urls")
apprise_target_startup = OptionStr("apprise", "apprise_target_startup")
apprise_target_startup_enable = OptionBool("apprise", "apprise_target_startup_enable", False)
apprise_target_download = OptionStr("apprise", "apprise_target_download")
apprise_target_download_enable = OptionBool("apprise", "apprise_target_download_enable", False)
apprise_target_pause_resume = OptionStr("apprise", "apprise_target_pause_resume")
apprise_target_pause_resume_enable = OptionBool("apprise", "apprise_target_pause_resume_enable", False)
apprise_target_pp = OptionStr("apprise", "apprise_target_pp")
apprise_target_pp_enable = OptionBool("apprise", "apprise_target_pp_enable", False)
apprise_target_complete = OptionStr("apprise", "apprise_target_complete")
apprise_target_complete_enable = OptionBool("apprise", "apprise_target_complete_enable", True)
apprise_target_failed = OptionStr("apprise", "apprise_target_failed")
apprise_target_failed_enable = OptionBool("apprise", "apprise_target_failed_enable", True)
apprise_target_disk_full = OptionStr("apprise", "apprise_target_disk_full")
apprise_target_disk_full_enable = OptionBool("apprise", "apprise_target_disk_full_enable", False)
apprise_target_new_login = OptionStr("apprise", "apprise_target_new_login")
apprise_target_new_login_enable = OptionBool("apprise", "apprise_target_new_login_enable", True)
apprise_target_warning = OptionStr("apprise", "apprise_target_warning")
apprise_target_warning_enable = OptionBool("apprise", "apprise_target_warning_enable", False)
apprise_target_error = OptionStr("apprise", "apprise_target_error")
apprise_target_error_enable = OptionBool("apprise", "apprise_target_error_enable", False)
apprise_target_queue_done = OptionStr("apprise", "apprise_target_queue_done")
apprise_target_query_done_enable = OptionBool("apprise", "apprise_target_queue_done_enable", False)
apprise_target_other = OptionStr("apprise", "apprise_target_other")
apprise_target_other_enable = OptionBool("apprise", "apprise_target_other_enable", True)

# [nscript]
nscript_enable = OptionBool("nscript", "nscript_enable")
nscript_cats = OptionList("nscript", "nscript_cats", ["*"])
Expand Down
29 changes: 29 additions & 0 deletions sabnzbd/interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -2092,6 +2092,35 @@ def make_item(job):
"pushbullet_prio_other",
"pushbullet_prio_new_login",
),
"apprise": (
"apprise_enable",
"apprise_cats",
"apprise_urls",
"apprise_target_startup",
"apprise_target_startup_enable",
"apprise_target_download",
"apprise_target_download_enable",
"apprise_target_pause_resume",
"apprise_target_pause_resume_enable",
"apprise_target_pp",
"apprise_target_pp_enable",
"apprise_target_complete",
"apprise_target_complete_enable",
"apprise_target_failed",
"apprise_target_failed_enable",
"apprise_target_disk_full",
"apprise_target_disk_full_enable",
"apprise_target_warning",
"apprise_target_warning_enable",
"apprise_target_error",
"apprise_target_error_enable",
"apprise_target_queue_done",
"apprise_target_queue_done_enable",
"apprise_target_other",
"apprise_target_other_enable",
"apprise_target_new_login",
"apprise_target_new_login_enable",
),
"nscript": (
"nscript_enable",
"nscript_cats",
Expand Down
106 changes: 105 additions & 1 deletion sabnzbd/notifier.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@
import http.client
import json
from threading import Thread
from typing import Optional, Dict
from typing import Optional, Dict, Union
import apprise
Safihre marked this conversation as resolved.
Show resolved Hide resolved

import sabnzbd
import sabnzbd.cfg
Expand Down Expand Up @@ -115,6 +116,20 @@ def get_prio(notification_type: str, section: str) -> int:
return -1000


def get_target(notification_type: str, section: str) -> Union[str, bool, None]:
"""Check target of `notification_type` in `section` if enabled is set"""
try:
if sabnzbd.config.get_config(section, "%s_target_%s_enable" % (section, notification_type))() > 0:
if result := sabnzbd.config.get_config(section, "%s_target_%s" % (section, notification_type))():
return result
# Use Default
return True
except TypeError:
logging.debug("Incorrect Notify option %s:%s_target_%s", section, section, notification_type)
return False
return False


def check_cat(section: str, job_cat: str, keyword: Optional[str] = None) -> bool:
"""Check if `job_cat` is enabled in `section`.
* = All, if no other categories selected.
Expand Down Expand Up @@ -165,6 +180,11 @@ def send_notification(
if sabnzbd.cfg.pushbullet_apikey() and check_classes(notification_type, "pushbullet"):
Thread(target=send_pushbullet, args=(title, msg, notification_type)).start()

# Apprise
if sabnzbd.cfg.apprise_enable() and check_cat("apprise", job_cat):
if sabnzbd.cfg.apprise_urls() and check_classes(notification_type, "apprise"):
Thread(target=send_apprise, args=(title, msg, notification_type)).start()

# Notification script.
if sabnzbd.cfg.nscript_enable() and check_cat("nscript", job_cat):
if sabnzbd.cfg.nscript_script():
Expand Down Expand Up @@ -265,6 +285,90 @@ def send_prowl(title, msg, notification_type, force=False, test=None):
return ""


def send_apprise(title, msg, notification_type, force=False, test=None):
"""send apprise message"""
logging.debug("Sending Apprise notification")
if test:
urls = test.get("apprise_urls")
else:
urls = sabnzbd.cfg.apprise_urls()

# Notification mapper
n_map = {
# Startup/Shutdown
"startup": apprise.common.NotifyType.INFO,
# Pause/Resume
"pause_resume": apprise.common.NotifyType.INFO,
# Added NZB
"download": apprise.common.NotifyType.INFO,
# Post-processing started
"pp": apprise.common.NotifyType.INFO,
# Job finished
"complete": apprise.common.NotifyType.SUCCESS,
# Job failed
"failed": apprise.common.NotifyType.FAILURE,
# Warning
"warning": apprise.common.NotifyType.WARNING,
# Error
"error": apprise.common.NotifyType.FAILURE,
# Disk full
"disk_full": apprise.common.NotifyType.WARNING,
# Queue finished
"queue_done": apprise.common.NotifyType.INFO,
# User logged in
"new_login": apprise.common.NotifyType.INFO,
# Other Messages
"other": apprise.common.NotifyType.INFO,
}

# Prepare our Asset Object
asset = apprise.AppriseAsset(
app_id="SABnzbd",
app_desc="SABnzbd Notification",
app_url="https://sabnzbd.org/",
image_path_mask=os.path.join(sabnzbd.DIR_PROG, "icons", "apprise", "apprise-{TYPE}.png"),
image_url_mask="https://sabnzbd.org/images/icons/apprise/{TYPE}.png",
image_url_logo="https://sabnzbd.org/images/icons/apple-touch-icon-180x180-precomposed.png",
)

# Initialize our Apprise Instance
apobj = apprise.Apprise(asset=asset)

if not test:
# Get a list of tags that are set to use the common list
if target := get_target(notification_type, "apprise"):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah sorry I missed that about the empty string!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no worries; it makes a bit more sense now.. True - use Default, otherwise string means there are URLs to load.

if target is True:
# Use default list
apobj.add(urls)
elif not apobj.add(target):
# Target is string of URLs to over-ride with
# Store our URL and assign our key
logging.warning("%s - %s", notification_type, T("One or more Apprise URLs could not be loaded."))
else:
# Nothing to notify
return ""
else:
# Use default list
apobj.add(urls)

try:
# The below notifies anything added to our list
if not apobj.notify(
body=msg,
title=title,
notify_type=n_map[notification_type],
body_format=apprise.NotifyFormat.TEXT,
):
return T("Failed to send one or more Apprise Notifications")

except:
logging.warning(T("Failed to send Apprise message"))
logging.info("Traceback: ", exc_info=True)
return T("Failed to send Apprise message")

return ""


def send_pushover(title, msg, notification_type, force=False, test=None):
"""Send message to pushover"""
logging.debug("Sending Pushover notification")
Expand Down
10 changes: 10 additions & 0 deletions sabnzbd/skintext.py
Original file line number Diff line number Diff line change
Expand Up @@ -671,6 +671,16 @@
"explain-pushbullet_apikey": TT("Your personal Pushbullet API key (required)"), #: Pushbullet settings
"opt-pushbullet_device": TT("Device"), #: Pushbullet settings
"explain-pushbullet_device": TT("Device to which message should be sent"), #: Pushbullet settings
"section-Apprise": TT("Apprise"), #: Header for Apprise notification section
"opt-apprise_enable": TT("Enable Apprise notifications"), #: Apprise settings
"explain-apprise_enable": TT(
"Send notifications using Apprise to almost any notification service"
), #: Apprise settings
"opt-apprise_urls": TT("Default Apprise URLs"), #: Apprise settings
"explain-apprise_urls": TT("Use a comma and/or space to identify more then one URL."), #: Apprise settings
"explain-apprise_extra_urls": TT(
"Override the default URLs for specific notification types below, if desired."
), #: Apprise settings
"section-NScript": TT("Notification Script"), #: Header for Notification Script notification section
"opt-nscript_enable": TT("Enable notification script"), #: Notification Script settings
"opt-nscript_script": TT("Script"), #: Notification Script settings
Expand Down