Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
30 changes: 30 additions & 0 deletions apprise_api/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# Copyright (C) 2025 Chris Caron <lead2gold@gmail.com>
# All rights reserved.
#
# This code is licensed under the MIT License.
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files(the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions :
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
"""
Apprise API package.
"""

__all__ = ["__version__"]

__version__: str = "1.2.6"

10 changes: 7 additions & 3 deletions apprise_api/api/context_processors.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
#
# Copyright (C) 2020 Chris Caron <lead2gold@gmail.com>
# Copyright (C) 2025 Chris Caron <lead2gold@gmail.com>
# All rights reserved.
#
# This code is licensed under the MIT License.
Expand All @@ -26,6 +26,7 @@
import apprise

from .utils import ConfigCache, gen_unique_config_id
from apprise_api import __version__


def stateful_mode(request):
Expand All @@ -51,9 +52,12 @@ def admin_enabled(request):

def apprise_version(request):
"""
Returns the current version of apprise loaded under the hood
Returns the current versions of the Apprise Library and API under the hood
"""
return {"APPRISE_VERSION": apprise.__version__}
return {
"APPRISE_LIB_VERSION": apprise.__version__,
"APPRISE_API_VERSION": __version__,
}


def default_config_id(request):
Expand Down
3 changes: 2 additions & 1 deletion apprise_api/api/templates/base.html
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,9 @@
<img class="left" src="{% static "logo.png" %}" alt="{% trans "Apprise Logo" %}" />
</a>
<h1>{% trans "Apprise API" %}</h1>
<a class="apprise-api-ver" href="https://github.com/caronc/apprise-api" target="_new">v{{APPRISE_API_VERSION}}</a>
<ul>
<li><a class="apprise-lib-ver" href="https://github.com/caronc/apprise" target="_new">APPRISE v{{APPRISE_VERSION}}</a></li>
<li><a class="apprise-lib-ver" href="https://github.com/caronc/apprise" target="_new">APPRISE LIBRARY v{{APPRISE_LIB_VERSION}}</a></li>
<li class="theme"><a href="{{ request.path }}?theme={{request.next_theme}}"><i class="material-icons">invert_colors</i></a></li>
</ul>
</div>
Expand Down
46 changes: 37 additions & 9 deletions apprise_api/api/templates/config.html
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,20 @@
{% load i18n %}
{% block body %}
{% if STATEFUL_MODE != 'disabled' %}
<h4>{% trans "Management for Config ID:" %} <code class="config-id">{{ key }}</code></h4>
<h4>
{% trans "Management for Config ID:" %}
<span class="config-id-wrapper">
<code class="config-id">{{ key }}</code>
<button
type="button"
class="btn-flat btn-small config-id-copy"
title="{% trans 'Copy Config ID to Clipboard' %}"
>
<i class="material-icons">content_copy</i>
</button>
</span>
</h4>

<div class="row">
<div class="col s12">
<ul class="tabs config-overview">
Expand Down Expand Up @@ -197,7 +210,9 @@ <h4>{% trans "Persistent Store Endpoints" %}</h4>
})
}

function showCopyToast(anchorEl) {
function showCopyToast(anchorEl, message) {
const msg = message || '{% trans "URL copied to clipboard" %}';

// Try SweetAlert2 first so we can anchor to the card
if (window.Swal && Swal.fire) {
const card = anchorEl
Expand All @@ -208,7 +223,7 @@ <h4>{% trans "Persistent Store Endpoints" %}</h4>
toast: true,
target: card, // anchor inside the card
position: 'top-end',
title: '{% trans "URL copied to clipboard" %}',
title: msg,
showConfirmButton: false,
timer: 1600,
timerProgressBar: true,
Expand All @@ -221,16 +236,17 @@ <h4>{% trans "Persistent Store Endpoints" %}</h4>

// Fallback to Materialize toast if Swal is not available
if (window.M && M.toast) {
M.toast({ html: '{% trans "URL copied to clipboard" %}' });
M.toast({ html: msg });
}
}

async function copyToClipboard(text, anchorEl) {

async function copyToClipboard(text, anchorEl, message) {
// Prefer modern Clipboard API
if (navigator.clipboard && navigator.clipboard.writeText) {
try {
await navigator.clipboard.writeText(text);
showCopyToast(anchorEl);
showCopyToast(anchorEl, message);
return;
} catch (e) {
console.warn('navigator.clipboard failed, falling back', e);
Expand All @@ -249,9 +265,7 @@ <h4>{% trans "Persistent Store Endpoints" %}</h4>

try {
document.execCommand('copy');
showCopyToast(anchorEl);
} catch (e) {
console.warn('document.execCommand("copy") failed', e);
showCopyToast(anchorEl, message);
} finally {
document.body.removeChild(textarea);
}
Expand Down Expand Up @@ -811,6 +825,20 @@ <h4>{% trans "Persistent Store Endpoints" %}</h4>
/* Initialize our page */
main_init();

// Config ID copy-to-clipboard
const configIdEl = document.querySelector('code.config-id');
const configCopyBtn = document.querySelector('.config-id-copy');
if (configIdEl && configCopyBtn) {
configCopyBtn.addEventListener('click', function (e) {
e.preventDefault();
copyToClipboard(
configIdEl.textContent.trim(),
configCopyBtn,
'{% trans "Config ID copied to clipboard" %}'
);
}, false);
}

{% if not CONFIG_LOCK %}
/* Initialze our configuration */
config_init();
Expand Down
50 changes: 50 additions & 0 deletions apprise_api/api/tests/test_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
from unittest.mock import patch

from django.test import SimpleTestCase, override_settings


Expand Down Expand Up @@ -65,3 +67,51 @@ def test_manage_status_code(self):
# An invalid key was specified
response = self.client.get("/cfg/valid-key")
assert response.status_code == 200

def test_get_config(self):
"""
Test retrieving configuration
"""

# our key to use
key = "test_cfg_config_"

# No content saved to the location yet
response = self.client.post("/cfg/{}".format(key))
self.assertEqual(response.status_code, 204)

# Add some content
response = self.client.post("/add/{}".format(key), {"urls": "mailto://user:pass@yahoo.ca"})
assert response.status_code == 200

# Handle case when we try to retrieve our content but we have no idea
# what the format is in. Essentialy there had to have been disk
# corruption here or someone meddling with the backend.
with patch("gzip.open", side_effect=OSError):
response = self.client.post("/cfg/{}".format(key))
assert response.status_code == 500

# Now we should be able to see our content
response = self.client.post("/cfg/{}".format(key))
assert response.status_code == 200

# Add a YAML file
response = self.client.post(
"/add/{}".format(key),
{
"format": "yaml",
"config": """
urls:
- dbus://""",
},
)
assert response.status_code == 200

# Now retrieve our YAML configuration
response = self.client.post("/cfg/{}".format(key))
assert response.status_code == 200

# Verify that the correct Content-Type is set in the header of the
# response
assert "Content-Type" in response
assert response["Content-Type"].startswith("text/yaml")
Loading
Loading