159 lines
4.8 KiB
Python
159 lines
4.8 KiB
Python
"""Send webhook notifications and write notification logs."""
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
from datetime import datetime, timezone
|
|
from typing import Any
|
|
|
|
import httpx
|
|
from sqlalchemy.orm import Session
|
|
|
|
from app.models.webhook_config import WebhookConfig
|
|
from app.models.notification_log import NotificationLog
|
|
from app.utils.dingtalk import (
|
|
dingtalk_signed_url,
|
|
format_dingtalk_rate_changed,
|
|
format_dingtalk_status,
|
|
)
|
|
|
|
|
|
def _now_iso() -> str:
|
|
return datetime.now(timezone.utc).astimezone().isoformat(timespec="seconds")
|
|
|
|
|
|
def _log(
|
|
db: Session,
|
|
webhook: WebhookConfig,
|
|
event_type: str,
|
|
payload: dict[str, Any],
|
|
status: str,
|
|
response_text: str,
|
|
) -> None:
|
|
entry = NotificationLog(
|
|
webhook_config_id=webhook.id,
|
|
webhook_name=webhook.name,
|
|
event_type=event_type,
|
|
payload_json=json.dumps(payload, ensure_ascii=False),
|
|
status=status,
|
|
response_text=response_text[:2000] if response_text else None,
|
|
)
|
|
db.add(entry)
|
|
db.commit()
|
|
|
|
|
|
def _send_generic(url: str, payload: dict[str, Any], timeout: float = 15.0) -> str:
|
|
resp = httpx.post(
|
|
url,
|
|
json=payload,
|
|
headers={"Content-Type": "application/json", "User-Agent": "SmartUp/1.0"},
|
|
timeout=timeout,
|
|
)
|
|
resp.raise_for_status()
|
|
return resp.text[:500]
|
|
|
|
|
|
def _send_dingtalk(url: str, secret: str, payload: dict[str, Any], timeout: float = 15.0) -> str:
|
|
signed = dingtalk_signed_url(url, secret) if secret else url
|
|
resp = httpx.post(
|
|
signed,
|
|
json=payload,
|
|
headers={"Content-Type": "application/json", "User-Agent": "SmartUp/1.0"},
|
|
timeout=timeout,
|
|
)
|
|
resp.raise_for_status()
|
|
result = resp.json()
|
|
if result.get("errcode", 0) != 0:
|
|
raise RuntimeError(f"DingTalk error: {resp.text[:300]}")
|
|
return resp.text[:500]
|
|
|
|
|
|
def send_rate_changed(
|
|
db: Session,
|
|
upstream_id: int,
|
|
upstream_name: str,
|
|
base_url: str,
|
|
changes: list[dict[str, Any]],
|
|
) -> None:
|
|
webhooks = (
|
|
db.query(WebhookConfig)
|
|
.filter(WebhookConfig.enabled == True)
|
|
.all()
|
|
)
|
|
changed_at = _now_iso()
|
|
generic_payload = {
|
|
"event": "upstream_rate_changed",
|
|
"upstream": {"id": upstream_id, "name": upstream_name, "base_url": base_url},
|
|
"changed_at": changed_at,
|
|
"changes": changes,
|
|
}
|
|
for wh in webhooks:
|
|
events = json.loads(wh.events_json or "[]")
|
|
if "upstream_rate_changed" not in events:
|
|
continue
|
|
try:
|
|
if wh.type == "dingtalk":
|
|
msg = format_dingtalk_rate_changed(upstream_name, changed_at, changes)
|
|
resp_text = _send_dingtalk(wh.url, wh.secret, msg)
|
|
else:
|
|
resp_text = _send_generic(wh.url, generic_payload)
|
|
_log(db, wh, "upstream_rate_changed", generic_payload, "success", resp_text)
|
|
except Exception as exc:
|
|
_log(db, wh, "upstream_rate_changed", generic_payload, "failed", str(exc))
|
|
|
|
|
|
def send_status_event(
|
|
db: Session,
|
|
upstream_id: int,
|
|
upstream_name: str,
|
|
base_url: str,
|
|
event: str,
|
|
error: str = "",
|
|
) -> None:
|
|
webhooks = (
|
|
db.query(WebhookConfig)
|
|
.filter(WebhookConfig.enabled == True)
|
|
.all()
|
|
)
|
|
changed_at = _now_iso()
|
|
generic_payload = {
|
|
"event": event,
|
|
"upstream": {"id": upstream_id, "name": upstream_name, "base_url": base_url},
|
|
"changed_at": changed_at,
|
|
"error": error,
|
|
}
|
|
for wh in webhooks:
|
|
events = json.loads(wh.events_json or "[]")
|
|
if event not in events:
|
|
continue
|
|
try:
|
|
if wh.type == "dingtalk":
|
|
msg = format_dingtalk_status(upstream_name, event, changed_at, error)
|
|
resp_text = _send_dingtalk(wh.url, wh.secret, msg)
|
|
else:
|
|
resp_text = _send_generic(wh.url, generic_payload)
|
|
_log(db, wh, event, generic_payload, "success", resp_text)
|
|
except Exception as exc:
|
|
_log(db, wh, event, generic_payload, "failed", str(exc))
|
|
|
|
|
|
def send_test_notification(db: Session, webhook: WebhookConfig) -> tuple[bool, str]:
|
|
payload = {
|
|
"event": "test",
|
|
"message": "SmartUp webhook test notification",
|
|
"sent_at": _now_iso(),
|
|
}
|
|
try:
|
|
if webhook.type == "dingtalk":
|
|
msg = {
|
|
"msgtype": "text",
|
|
"text": {"content": "✅ SmartUp webhook 测试通知\n配置正常,连接成功。"},
|
|
}
|
|
resp_text = _send_dingtalk(webhook.url, webhook.secret, msg)
|
|
else:
|
|
resp_text = _send_generic(webhook.url, payload)
|
|
_log(db, webhook, "test", payload, "success", resp_text)
|
|
return True, "发送成功"
|
|
except Exception as exc:
|
|
_log(db, webhook, "test", payload, "failed", str(exc))
|
|
return False, str(exc)
|