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 4 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
45 changes: 44 additions & 1 deletion interfaces/Config/templates/config_notify.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -363,6 +363,49 @@
</fieldset>
</div>
</div>
<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')</span>
</div>
<hr />
<span class="desc">$T('explain-apprise_extra_urls')</span>
<!--#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('default')}" />
</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>
</form>
</div><!-- /colmask -->

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
13 changes: 12 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 @@ -54,7 +55,17 @@ notify2==0.3.1; sys_platform != 'win32' and sys_platform != 'darwin'
# Uncomment line below or manually install after installing requirements.
# pygobject>=3.10.2; sys_platform != 'win32' and sys_platform != 'darwin'

# PyYAML does not have binary packages for Mac Users
PyYAML==6.0.1 --no-binary=PyYAML; sys_platform == 'darwin'
PyYAML==6.0.1; sys_platform != 'darwin'
Copy link
Member

Choose a reason for hiding this comment

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

We can remove this special darwin stuff here as this is only for run-from-source users, for who pip will get the correct version. The --no-binary in build_release will fix it for the release.


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

# 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
Binary file added sabnzbd/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 sabnzbd/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 sabnzbd/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 sabnzbd/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.
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
107 changes: 106 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,19 @@ 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:
return sabnzbd.config.get_config(section, "%s_target_%s" % (section, notification_type))().strip()

except TypeError:
logging.debug("Incorrect Notify option %s:%s_target_%s", section, section, notification_type)
return None

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 +179,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 +284,92 @@ 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(os.path.dirname(__file__), "apprise", "apprise-{TYPE}.png"),
Copy link
Member

Choose a reason for hiding this comment

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

I don't think we should have any images in the /sabnzbd directory, it's only for code.
We should put it in /icons/apprise.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Do you want me to hardcode /icons/apprise or find it relative to the directory. This is the edge case with Apprise, there are like 70 services that just use the icons found on the web, but then there are like 30 that use the local icons stored on the server (True self-hosted vs pulling from the cloud).

This was why i originally stored the images here and then cross-referenced them here via both web and locally (it's now i do it with apprise, so it's just 1 directory to manage - link.

At the end of the day, to truly get the best use of Apprise, you need some images stored locally. If you want, i can just not set a mask over-ride at all, and you'll use the ones that ship with Apprise instead of some custom SABnzbd ones i rigged up quickly?

Copy link
Member

Choose a reason for hiding this comment

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

I can fix this one up when all the code is fine, we have some constants to handle this :)

image_url_mask="https://raw.githubusercontent.com/sabnzbd/sabnzbd.github.io/master/images/icons/apprise/{TYPE}.png",
image_url_logo="https://raw.githubusercontent.com/sabnzbd/sabnzbd.github.io/master/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 isinstance(target, str):
if target:
Copy link
Member

Choose a reason for hiding this comment

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

This can be just 1 line:

Suggested change
if target := get_target(notification_type, "apprise"):
if isinstance(target, str):
if target:
if target := get_target(notification_type, "apprise"):

Copy link
Contributor Author

@caronc caronc Feb 9, 2024

Choose a reason for hiding this comment

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

The only catch is that target can return None, or False. Also if it's an empty string, we want to process it. That means an over-ride was specified, but no URLs were provided (so to use the default). If i do if target :=, how is it going to handle an empty string case (perhaps it's already skipped over at this point, so default logic won't be applied? - in short, i want to get inside that if-block if we get an empty string returned.

I'll see if i can refactor get_target a bet better so it can conform to your minimized example.

Edit: Done

# Store our URL and assign our key
if not apobj.add(target):
logging.warning(
"Key: %s - %s", notification_type, T("One or more Apprise URLs could not be loaded.")
)

else:
# Use default list
apobj.add(urls)

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="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 URLs"), #: 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(
"Optionally override the default URL(s) for specific cases. To disable a notification for a specific category:"
" simply enable it below, but do not provide it a URL to trigger off of. Alternatively, use comma and/or "
Copy link
Member

Choose a reason for hiding this comment

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

I don't understand this. What can users do here? And how is it different from unchecking a notification-type?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It's the same thing. You can drop this if you like. Was just trying to make it clear that anything specified in the individual entries overrides the default (does not stack with it)

"space to identify more then one Apprise URL overide."
), #: 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