feat: comprehensive code review fixes + receivers-only architecture
Security:
- Refuse startup with default secret_key in production (was just logging)
- Settings endpoint now requires admin role
- Password validation on initial setup
- DOM-based HTML sanitizer replaces regex in template previews
- Add *.log to .gitignore
Performance & reliability:
- Token refresh deduplication prevents race condition on concurrent 401s
- Theme media query listener registered once (no leak)
- IconPicker uses $derived instead of function call per render
- Snackbar uses single-batch state update instead of while loop
- Replace 11 inline hover handlers with CSS :hover in layout
Architecture - receivers-only:
- Delivery endpoints (chat_id, email, url, room_id, topic) now stored
exclusively in TargetReceiver rows, never in target.config
- Migration extracts existing delivery fields to receiver rows
- Notifier and dispatcher remove all config fallbacks
- Frontend targets page shows receivers list per target with
add/remove/toggle/test per receiver
- Single-receiver test endpoint: POST /targets/{id}/receivers/{id}/test
Code quality:
- Extract AuthLayout.svelte from login/setup (150 lines CSS dedup)
- Split telegram-bots page (754→51 lines + 3 tab components)
- Split notification-trackers page (547→432 lines + 4 components)
- Deduplicate _send_reply into shared handler.send_reply()
- Add locale column to template models, replace name-based detection
- Fix delete_notification_tracker dead protection check
- Fix check_telegram_bot query (filter by type, remove bogus OR)
- Add graceful scheduler shutdown in lifespan
- Consistent /bots?tab=telegram URLs across all nav links
i18n:
- Error page, chat actions, target types, provider types internationalized
- All new receiver UI strings in EN + RU
This commit is contained in:
@@ -86,13 +86,8 @@ async def _send_telegram_broadcast(target: NotificationTarget, message: str, rec
|
||||
if not bot_token:
|
||||
return {"success": False, "error": "Missing bot_token"}
|
||||
|
||||
# Fall back to legacy chat_id if no receivers
|
||||
if not receivers:
|
||||
chat_id = target.config.get("chat_id")
|
||||
if chat_id:
|
||||
receivers = [{"chat_id": str(chat_id)}]
|
||||
else:
|
||||
return {"success": False, "error": "No receivers configured"}
|
||||
return {"success": False, "error": "No receivers configured"}
|
||||
|
||||
results: list[dict] = []
|
||||
async with aiohttp.ClientSession() as session:
|
||||
@@ -121,14 +116,8 @@ async def _send_telegram_broadcast(target: NotificationTarget, message: str, rec
|
||||
async def _send_webhook_broadcast(target: NotificationTarget, message: str, receivers: list[dict]) -> dict:
|
||||
from notify_bridge_core.notifications.webhook.client import WebhookClient
|
||||
|
||||
# Fall back to legacy url if no receivers
|
||||
if not receivers:
|
||||
url = target.config.get("url")
|
||||
headers = target.config.get("headers", {})
|
||||
if url:
|
||||
receivers = [{"url": url, "headers": headers}]
|
||||
else:
|
||||
return {"success": False, "error": "No receivers configured"}
|
||||
return {"success": False, "error": "No receivers configured"}
|
||||
|
||||
results: list[dict] = []
|
||||
async with aiohttp.ClientSession() as session:
|
||||
@@ -206,11 +195,7 @@ async def _send_email_broadcast(target: NotificationTarget, message: str, receiv
|
||||
async def _send_webhook_like_broadcast(target: NotificationTarget, message: str, receivers: list[dict]) -> dict:
|
||||
"""Broadcast for Discord and Slack — both use webhook URLs as receivers."""
|
||||
if not receivers:
|
||||
webhook_url = target.config.get("webhook_url")
|
||||
if webhook_url:
|
||||
receivers = [{"webhook_url": webhook_url}]
|
||||
else:
|
||||
return {"success": False, "error": "No receivers configured"}
|
||||
return {"success": False, "error": "No receivers configured"}
|
||||
|
||||
results: list[dict] = []
|
||||
async with aiohttp.ClientSession() as session:
|
||||
@@ -238,11 +223,7 @@ async def _send_ntfy_broadcast(target: NotificationTarget, message: str, receive
|
||||
auth_token = target.config.get("auth_token")
|
||||
|
||||
if not receivers:
|
||||
topic = target.config.get("topic")
|
||||
if topic:
|
||||
receivers = [{"topic": topic}]
|
||||
else:
|
||||
return {"success": False, "error": "No receivers configured"}
|
||||
return {"success": False, "error": "No receivers configured"}
|
||||
|
||||
from notify_bridge_core.notifications.ntfy.client import NtfyClient
|
||||
results: list[dict] = []
|
||||
@@ -307,6 +288,26 @@ def _aggregate(results: list[dict]) -> dict:
|
||||
# --- Public API used by routes ---
|
||||
|
||||
|
||||
async def send_to_receiver(target: NotificationTarget, receiver_config: dict, message: str) -> dict:
|
||||
"""Send a message to a single receiver of a target."""
|
||||
try:
|
||||
send_fn = {
|
||||
"telegram": _send_telegram_broadcast,
|
||||
"webhook": _send_webhook_broadcast,
|
||||
"email": _send_email_broadcast,
|
||||
"discord": _send_webhook_like_broadcast,
|
||||
"slack": _send_webhook_like_broadcast,
|
||||
"ntfy": _send_ntfy_broadcast,
|
||||
"matrix": _send_matrix_broadcast,
|
||||
}.get(target.type)
|
||||
if send_fn:
|
||||
return await send_fn(target, message, [receiver_config])
|
||||
return {"success": False, "error": f"Unknown target type: {target.type}"}
|
||||
except Exception as e:
|
||||
_LOGGER.error("Send to receiver failed: %s", e)
|
||||
return {"success": False, "error": str(e)}
|
||||
|
||||
|
||||
async def send_test_notification(target: NotificationTarget, locale: str = "en") -> dict:
|
||||
"""Send a simple test message."""
|
||||
message = _get_test_message(locale, target.type)
|
||||
|
||||
Reference in New Issue
Block a user