Make Apprise strings customizable. Closes #212.

This commit is contained in:
Jack Kingsman
2026-04-21 19:39:10 -07:00
parent 2f412e1a93
commit ecb4c99a43
7 changed files with 549 additions and 60 deletions
+136 -9
View File
@@ -1049,7 +1049,8 @@ class TestAppriseFormatBody:
from app.fanout.apprise_mod import _format_body
body = _format_body(
{"type": "PRIV", "text": "hi", "sender_name": "Alice"}, include_path=False
{"type": "PRIV", "text": "hi", "sender_name": "Alice"},
body_format_dm="**DM:** {sender_name}: {text}",
)
assert body == "**DM:** Alice: hi"
@@ -1058,7 +1059,7 @@ class TestAppriseFormatBody:
body = _format_body(
{"type": "CHAN", "text": "hi", "sender_name": "Bob", "channel_name": "#general"},
include_path=False,
body_format_channel="**{channel_name}:** {sender_name}: {text}",
)
assert body == "**#general:** Bob: hi"
@@ -1072,7 +1073,7 @@ class TestAppriseFormatBody:
"sender_name": "Bob",
"channel_name": "#general",
},
include_path=False,
body_format_channel="**{channel_name}:** {sender_name}: {text}",
)
assert body == "**#general:** Bob: hi"
@@ -1086,7 +1087,7 @@ class TestAppriseFormatBody:
"sender_name": "Alice",
"paths": [{"path": "2027"}],
},
include_path=True,
body_format_dm="**DM:** {sender_name}: {text} **via:** [{hops_backticked}]",
)
assert "**via:**" in body
assert "`20`" in body
@@ -1097,7 +1098,7 @@ class TestAppriseFormatBody:
body = _format_body(
{"type": "PRIV", "text": "hi", "sender_name": "Alice"},
include_path=True,
body_format_dm="**DM:** {sender_name}: {text} **via:** [{hops_backticked}]",
)
assert "`direct`" in body
@@ -1112,7 +1113,7 @@ class TestAppriseFormatBody:
"sender_name": "Alice",
"paths": [{"path": "aabbccdd", "path_len": 2}],
},
include_path=True,
body_format_dm="**DM:** {sender_name}: {text} **via:** [{hops_backticked}]",
)
assert "**via:**" in body
assert "`aabb`" in body
@@ -1129,7 +1130,7 @@ class TestAppriseFormatBody:
"sender_name": "Alice",
"paths": [{"path": "aabbccddeeff", "path_len": 2}],
},
include_path=True,
body_format_dm="**DM:** {sender_name}: {text} **via:** [{hops_backticked}]",
)
assert "**via:**" in body
assert "`aabbcc`" in body
@@ -1147,7 +1148,7 @@ class TestAppriseFormatBody:
"channel_name": "#general",
"paths": [{"path": "aabbccdd", "path_len": 2}],
},
include_path=True,
body_format_channel="**{channel_name}:** {sender_name}: {text} **via:** [{hops_backticked}]",
)
assert "**#general:**" in body
assert "`aabb`" in body
@@ -1164,12 +1165,118 @@ class TestAppriseFormatBody:
"sender_name": "Alice",
"paths": [{"path": "aabb"}],
},
include_path=True,
body_format_dm="**DM:** {sender_name}: {text} **via:** [{hops_backticked}]",
)
assert "**via:**" in body
assert "`aa`" in body
assert "`bb`" in body
def test_default_format_strings(self):
"""Default format strings produce expected output."""
from app.fanout.apprise_mod import _format_body
body = _format_body(
{
"type": "PRIV",
"text": "hi",
"sender_name": "Alice",
"paths": [{"path": "2a3b"}],
},
)
assert body == "**DM:** Alice: hi **via:** [`2a`, `3b`]"
def test_custom_format_with_rssi(self):
"""Custom format string can include rssi/snr."""
from app.fanout.apprise_mod import _format_body
body = _format_body(
{
"type": "PRIV",
"text": "hi",
"sender_name": "Alice",
"paths": [{"path": "2a", "rssi": -95, "snr": 6.5}],
},
body_format_dm="From {sender_name}: {text} (rssi: {rssi}, snr: {snr})",
)
assert body == "From Alice: hi (rssi: -95, snr: 6.5)"
def test_unknown_placeholder_left_as_is(self):
"""Unknown {placeholders} pass through unchanged."""
from app.fanout.apprise_mod import _format_body
body = _format_body(
{"type": "PRIV", "text": "hi", "sender_name": "Alice"},
body_format_dm="{sender_name}: {text} {unknown_var}",
)
assert body == "Alice: hi {unknown_var}"
def test_none_fields_render_empty(self):
"""None optional fields render as empty string, not 'None'."""
from app.fanout.apprise_mod import _format_body
body = _format_body(
{"type": "PRIV", "text": "hi", "sender_name": "Alice"},
body_format_dm="{sender_name}: {text} rssi={rssi}",
)
assert body == "Alice: hi rssi="
assert "None" not in body
def test_hops_direct_when_no_paths(self):
"""hops is 'direct' when no path data exists."""
from app.fanout.apprise_mod import _format_body
body = _format_body(
{"type": "CHAN", "text": "hi", "sender_name": "Bob", "channel_name": "#gen"},
body_format_channel="{channel_name} {hops}",
)
assert body == "#gen direct"
def test_hops_direct_when_empty_path(self):
"""hops is 'direct' when path string is empty."""
from app.fanout.apprise_mod import _format_body
body = _format_body(
{
"type": "PRIV",
"text": "hi",
"sender_name": "Alice",
"paths": [{"path": ""}],
},
body_format_dm="{hops}",
)
assert body == "direct"
def test_no_re_expansion_of_substituted_values(self):
"""Placeholders in message text must not be expanded by later passes."""
from app.fanout.apprise_mod import _format_body
body = _format_body(
{"type": "PRIV", "text": "hello {sender_name}", "sender_name": "Alice"},
body_format_dm="{sender_name}: {text}",
)
assert body == "Alice: hello {sender_name}"
@pytest.mark.asyncio
async def test_empty_format_string_uses_default(self):
"""Empty format strings in config should produce default output, not blank."""
from unittest.mock import patch as _patch
from app.fanout.apprise_mod import AppriseModule
mod = AppriseModule(
"test",
{"urls": "json://localhost", "body_format_dm": "", "body_format_channel": " "},
)
with _patch("app.fanout.apprise_mod._send_sync", return_value=True) as mock_send:
await mod.on_message(
{"type": "PRIV", "text": "hi", "outgoing": False, "sender_name": "Alice"}
)
mock_send.assert_called_once()
body = mock_send.call_args[0][1]
assert "Alice" in body
assert "hi" in body
assert body != ""
class TestAppriseNormalizeDiscordUrl:
def test_discord_scheme(self):
@@ -1233,6 +1340,26 @@ class TestAppriseValidation:
_validate_apprise_config({"urls": "discord://123/abc"})
def test_validate_apprise_config_accepts_format_strings(self):
from app.routers.fanout import _validate_apprise_config
_validate_apprise_config(
{
"urls": "discord://123/abc",
"body_format_dm": "DM from {sender_name}: {text}",
"body_format_channel": "{channel_name}: {text}",
}
)
def test_validate_apprise_config_rejects_non_string_format(self):
from fastapi import HTTPException
from app.routers.fanout import _validate_apprise_config
with pytest.raises(HTTPException) as exc_info:
_validate_apprise_config({"urls": "discord://123/abc", "body_format_dm": 123})
assert exc_info.value.status_code == 400
def test_enforce_scope_apprise_strips_raw_packets(self):
from app.routers.fanout import _enforce_scope
+7 -4
View File
@@ -1171,7 +1171,8 @@ class TestFanoutAppriseIntegration:
config={
"urls": f"json://127.0.0.1:{apprise_capture_server.port}",
"preserve_identity": True,
"include_path": False,
"body_format_dm": "**DM:** {sender_name}: {text}",
"body_format_channel": "**{channel_name}:** {sender_name}: {text}",
},
scope={"messages": "all", "raw_packets": "none"},
enabled=True,
@@ -1212,7 +1213,8 @@ class TestFanoutAppriseIntegration:
name="Channel Apprise",
config={
"urls": f"json://127.0.0.1:{apprise_capture_server.port}",
"include_path": False,
"body_format_dm": "**DM:** {sender_name}: {text}",
"body_format_channel": "**{channel_name}:** {sender_name}: {text}",
},
scope={"messages": "all", "raw_packets": "none"},
enabled=True,
@@ -1541,13 +1543,14 @@ class TestFanoutAppriseIntegration:
@pytest.mark.asyncio
async def test_apprise_includes_routing_path(self, apprise_capture_server, integration_db):
"""Apprise with include_path=True shows routing hops in the body."""
"""Apprise with hops in format string shows routing hops in the body."""
cfg = await FanoutConfigRepository.create(
config_type="apprise",
name="Path Apprise",
config={
"urls": f"json://127.0.0.1:{apprise_capture_server.port}",
"include_path": True,
"body_format_dm": "**DM:** {sender_name}: {text} **via:** [{hops_backticked}]",
"body_format_channel": "**{channel_name}:** {sender_name}: {text} **via:** [{hops_backticked}]",
},
scope={"messages": "all", "raw_packets": "none"},
enabled=True,
+1 -1
View File
@@ -2,4 +2,4 @@
# run ``run_migrations`` to completion assert ``get_version == LATEST`` and
# ``applied == LATEST - starting_version`` so only this constant needs to
# change, not every individual assertion.
LATEST_SCHEMA_VERSION = 59
LATEST_SCHEMA_VERSION = 60