feat: Discord/Slack/ntfy/Matrix targets, command templates, delete protection, email/matrix bots
- Discord, Slack, ntfy, Matrix notification target types with clients and dispatch - MatrixBot model + API + frontend in Bots tab - Command template system fully wired into all handler commands - Default command templates seeded (EN/RU, 14 slots each) - Command template editor with variables reference including child fields - Delete protection on all 10 entity types (409 with consumer details) - Provider type selector on template config forms - Target type selector as dropdown with all 7 types - Response template selector on command config form - CLAUDE.md: mandatory server restart rule, child properties rule
This commit is contained in:
@@ -17,10 +17,20 @@ _TEST_MESSAGES: dict[str, dict[str, str]] = {
|
||||
"en": {
|
||||
"telegram": "\u2705 Test message from <b>Notify Bridge</b>",
|
||||
"webhook": "Test notification from Notify Bridge",
|
||||
"email": "Test email from Notify Bridge",
|
||||
"discord": "Test message from **Notify Bridge**",
|
||||
"slack": "Test message from *Notify Bridge*",
|
||||
"ntfy": "Test notification from Notify Bridge",
|
||||
"matrix": "Test message from Notify Bridge",
|
||||
},
|
||||
"ru": {
|
||||
"telegram": "\u2705 Тестовое сообщение от <b>Notify Bridge</b>",
|
||||
"webhook": "Тестовое уведомление от Notify Bridge",
|
||||
"email": "Тестовое письмо от Notify Bridge",
|
||||
"discord": "Тестовое сообщение от **Notify Bridge**",
|
||||
"slack": "Тестовое сообщение от *Notify Bridge*",
|
||||
"ntfy": "Тестовое уведомление от Notify Bridge",
|
||||
"matrix": "Тестовое сообщение от Notify Bridge",
|
||||
},
|
||||
}
|
||||
|
||||
@@ -50,12 +60,17 @@ async def send_to_target(target: NotificationTarget, message: str) -> dict:
|
||||
"""
|
||||
try:
|
||||
receivers = await _load_receivers(target.id)
|
||||
if target.type == "telegram":
|
||||
return await _send_telegram_broadcast(target, message, receivers)
|
||||
elif target.type == "webhook":
|
||||
return await _send_webhook_broadcast(target, message, receivers)
|
||||
elif target.type == "email":
|
||||
return await _send_email_broadcast(target, message, receivers)
|
||||
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, receivers)
|
||||
return {"success": False, "error": f"Unknown target type: {target.type}"}
|
||||
except Exception as e:
|
||||
_LOGGER.error("Send failed: %s", e)
|
||||
@@ -188,6 +203,107 @@ async def _send_email_broadcast(target: NotificationTarget, message: str, receiv
|
||||
return {"success": False, "error": "No valid email receivers"}
|
||||
|
||||
|
||||
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"}
|
||||
|
||||
results: list[dict] = []
|
||||
async with aiohttp.ClientSession() as session:
|
||||
if target.type == "discord":
|
||||
from notify_bridge_core.notifications.discord.client import DiscordClient
|
||||
client = DiscordClient(session)
|
||||
for recv in receivers:
|
||||
url = recv.get("webhook_url")
|
||||
if url:
|
||||
results.append(await client.send(url, message, username=target.config.get("username")))
|
||||
elif target.type == "slack":
|
||||
from notify_bridge_core.notifications.slack.client import SlackClient
|
||||
client = SlackClient(session)
|
||||
for recv in receivers:
|
||||
url = recv.get("webhook_url")
|
||||
if url:
|
||||
results.append(await client.send(url, message, username=target.config.get("username")))
|
||||
|
||||
return _aggregate(results)
|
||||
|
||||
|
||||
async def _send_ntfy_broadcast(target: NotificationTarget, message: str, receivers: list[dict]) -> dict:
|
||||
"""Broadcast to ntfy topics."""
|
||||
server_url = target.config.get("server_url", "https://ntfy.sh")
|
||||
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"}
|
||||
|
||||
from notify_bridge_core.notifications.ntfy.client import NtfyClient
|
||||
results: list[dict] = []
|
||||
async with aiohttp.ClientSession() as session:
|
||||
client = NtfyClient(session)
|
||||
for recv in receivers:
|
||||
topic = recv.get("topic")
|
||||
if topic:
|
||||
results.append(await client.send(
|
||||
server_url, topic, message,
|
||||
title="Notify Bridge",
|
||||
priority=recv.get("priority", 3),
|
||||
auth_token=auth_token,
|
||||
))
|
||||
|
||||
return _aggregate(results)
|
||||
|
||||
|
||||
async def _send_matrix_broadcast(target: NotificationTarget, message: str, receivers: list[dict]) -> dict:
|
||||
"""Broadcast to Matrix rooms."""
|
||||
from notify_bridge_core.notifications.matrix.client import MatrixClient
|
||||
from ..database.models import MatrixBot
|
||||
|
||||
matrix_bot_id = target.config.get("matrix_bot_id")
|
||||
if not matrix_bot_id:
|
||||
return {"success": False, "error": "No Matrix bot configured for this target"}
|
||||
|
||||
engine = get_engine()
|
||||
async with AsyncSession(engine) as session:
|
||||
bot = await session.get(MatrixBot, matrix_bot_id)
|
||||
if not bot:
|
||||
return {"success": False, "error": "Matrix bot not found"}
|
||||
homeserver = bot.homeserver_url
|
||||
access_token = bot.access_token
|
||||
|
||||
if not receivers:
|
||||
return {"success": False, "error": "No receivers configured"}
|
||||
|
||||
results: list[dict] = []
|
||||
async with aiohttp.ClientSession() as http:
|
||||
client = MatrixClient(http, homeserver, access_token)
|
||||
for recv in receivers:
|
||||
room_id = recv.get("room_id")
|
||||
if room_id:
|
||||
results.append(await client.send_message(room_id, message, html_message=message))
|
||||
|
||||
return _aggregate(results)
|
||||
|
||||
|
||||
def _aggregate(results: list[dict]) -> dict:
|
||||
"""Aggregate broadcast results."""
|
||||
successes = sum(1 for r in results if r.get("success"))
|
||||
if successes == len(results) and results:
|
||||
return {"success": True, "receivers": len(results)}
|
||||
elif successes > 0:
|
||||
return {"success": True, "receivers": len(results), "partial_failures": len(results) - successes}
|
||||
elif results:
|
||||
return results[0]
|
||||
return {"success": False, "error": "No valid receivers"}
|
||||
|
||||
|
||||
# --- Public API used by routes ---
|
||||
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ from ..database.engine import get_engine
|
||||
from ..database.models import (
|
||||
EmailBot,
|
||||
EventLog,
|
||||
MatrixBot,
|
||||
NotificationTarget,
|
||||
NotificationTracker,
|
||||
NotificationTrackerState,
|
||||
@@ -162,7 +163,7 @@ async def check_tracker(tracker_id: int) -> dict[str, Any]:
|
||||
template_slots[event_key] = tmpl_text
|
||||
|
||||
target_config = dict(target.config)
|
||||
# Inject SMTP config for email targets from EmailBot
|
||||
# Inject bot credentials for bot-backed target types
|
||||
if target.type == "email":
|
||||
email_bot_id = target.config.get("email_bot_id")
|
||||
if email_bot_id:
|
||||
@@ -177,6 +178,13 @@ async def check_tracker(tracker_id: int) -> dict[str, Any]:
|
||||
"from_name": email_bot.name,
|
||||
"use_tls": email_bot.smtp_use_tls,
|
||||
}
|
||||
elif target.type == "matrix":
|
||||
matrix_bot_id = target.config.get("matrix_bot_id")
|
||||
if matrix_bot_id:
|
||||
matrix_bot = await session.get(MatrixBot, matrix_bot_id)
|
||||
if matrix_bot:
|
||||
target_config["homeserver_url"] = matrix_bot.homeserver_url
|
||||
target_config["access_token"] = matrix_bot.access_token
|
||||
|
||||
link_data.append({
|
||||
"target_type": target.type,
|
||||
|
||||
Reference in New Issue
Block a user