forked from erp-dev/erp
277 lines
12 KiB
Python
277 lines
12 KiB
Python
import logging
|
|
import json
|
|
|
|
from api_v1.utils.wecom_webhook import send_wecom_webhook_message
|
|
from notifier.jpush import send_jpush_payload
|
|
from notifier.models import NotifierChannelEnum
|
|
from notifier.message_api import send_message_api_news_to_agents, send_message_api_text_message
|
|
from notifier.serializers import (
|
|
MessageNewsRequestSerializer,
|
|
MessageTemplatePayloadSerializer,
|
|
MessageTextRequestSerializer,
|
|
)
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class BaseNotifierBackend:
|
|
channel = ""
|
|
|
|
def notify(self, *, notifier, content: str, context: dict) -> dict:
|
|
raise NotImplementedError
|
|
|
|
|
|
class WeComWebhookNotifierBackend(BaseNotifierBackend):
|
|
channel = NotifierChannelEnum.WECOM_WEBHOOK
|
|
|
|
def notify(self, *, notifier, content: str, context: dict) -> dict:
|
|
content = (content or "").strip()
|
|
if not content:
|
|
raise ValueError("企业微信机器人消息 content 不能为空")
|
|
msgtype = str(notifier.get_config_value("msgtype", "markdown") or "markdown").strip().lower()
|
|
timeout_seconds = float(notifier.get_config_value("timeout_seconds", 10.0) or 10.0)
|
|
key = str(notifier.get_config_value("key", "") or "").strip()
|
|
response = send_wecom_webhook_message(
|
|
content=content,
|
|
msgtype=msgtype,
|
|
key=key or None,
|
|
timeout_seconds=timeout_seconds,
|
|
)
|
|
if not response.ok:
|
|
raise RuntimeError(f"WeCom webhook 返回失败: errcode={response.errcode}, errmsg={response.errmsg}")
|
|
result = {
|
|
"channel": self.channel,
|
|
"msgtype": msgtype,
|
|
"errcode": response.errcode,
|
|
"errmsg": response.errmsg,
|
|
}
|
|
logger.info("[notifier.backends] wecom webhook sent: notifier_id=%s result=%s", notifier.id, result)
|
|
return result
|
|
|
|
|
|
class MessageAPINotifierBackend(BaseNotifierBackend):
|
|
channel = NotifierChannelEnum.MESSAGE_API
|
|
|
|
def _load_rendered_payload(self, *, content: str) -> dict:
|
|
content = (content or "").strip()
|
|
if not content:
|
|
raise ValueError("消息发送 API 模板渲染结果不能为空")
|
|
try:
|
|
payload = json.loads(content)
|
|
except json.JSONDecodeError as exc:
|
|
raise ValueError(f"消息发送 API 模板渲染结果不是合法 JSON: {exc}") from exc
|
|
if not isinstance(payload, dict):
|
|
raise ValueError("消息发送 API 模板渲染结果必须是 JSON 对象")
|
|
|
|
serializer = MessageTemplatePayloadSerializer(data=payload)
|
|
serializer.is_valid(raise_exception=True)
|
|
return serializer.validated_data
|
|
|
|
def notify(self, *, notifier, content: str, context: dict) -> dict:
|
|
rendered_payload = self._load_rendered_payload(content=content)
|
|
msgtype = rendered_payload["msgtype"]
|
|
|
|
if msgtype == "text":
|
|
payload = {
|
|
"agent_id": rendered_payload.get("agent_id", notifier.get_config_value("agent_id")),
|
|
"content": rendered_payload["content"],
|
|
}
|
|
resolved_user_ids = rendered_payload.get("user_ids") or notifier.get_config_value("user_ids")
|
|
if resolved_user_ids:
|
|
payload["user_ids"] = resolved_user_ids
|
|
serializer = MessageTextRequestSerializer(data=payload)
|
|
serializer.is_valid(raise_exception=True)
|
|
data = serializer.validated_data
|
|
response = send_message_api_text_message(
|
|
agent_id=data["agent_id"],
|
|
content=data["content"],
|
|
user_ids=data.get("user_ids"),
|
|
timeout_seconds=float(notifier.get_config_value("timeout_seconds", 10.0) or 10.0),
|
|
)
|
|
result = {
|
|
"channel": self.channel,
|
|
"msgtype": "text",
|
|
"agent_id": data["agent_id"],
|
|
"errcode": response.errcode,
|
|
"errmsg": response.errmsg,
|
|
}
|
|
logger.info("[notifier.backends] message api text sent: notifier_id=%s result=%s", notifier.id, result)
|
|
return result
|
|
|
|
payload = {
|
|
"agent_ids": rendered_payload.get("agent_ids", notifier.get_config_value("agent_ids")),
|
|
"title": rendered_payload["title"],
|
|
"description": rendered_payload["description"],
|
|
"url": rendered_payload["url"],
|
|
"image_url": rendered_payload["image_url"],
|
|
}
|
|
resolved_user_ids = rendered_payload.get("user_ids") or notifier.get_config_value("user_ids")
|
|
if resolved_user_ids:
|
|
payload["user_ids"] = resolved_user_ids
|
|
serializer = MessageNewsRequestSerializer(data=payload)
|
|
serializer.is_valid(raise_exception=True)
|
|
data = serializer.validated_data
|
|
results = send_message_api_news_to_agents(
|
|
agent_ids=data["agent_ids"],
|
|
title=data["title"],
|
|
description=data["description"],
|
|
url=data["url"],
|
|
image_url=data["image_url"],
|
|
user_ids=data.get("user_ids"),
|
|
timeout_seconds=float(notifier.get_config_value("timeout_seconds", 10.0) or 10.0),
|
|
)
|
|
if not any(item.get("ok") for item in results):
|
|
raise RuntimeError("All agent sends failed")
|
|
|
|
result = {
|
|
"channel": self.channel,
|
|
"msgtype": "news",
|
|
"agent_ids": data["agent_ids"],
|
|
"ok_count": sum(1 for item in results if item.get("ok")),
|
|
"total_count": len(results),
|
|
"results": results,
|
|
}
|
|
logger.info("[notifier.backends] message api news sent: notifier_id=%s result=%s", notifier.id, result)
|
|
return result
|
|
|
|
|
|
class JPushNotifierBackend(BaseNotifierBackend):
|
|
channel = NotifierChannelEnum.JPUSH
|
|
|
|
def _load_rendered_payload(self, *, content: str) -> dict:
|
|
content = (content or "").strip()
|
|
if not content:
|
|
raise ValueError("极光推送模板渲染结果不能为空")
|
|
try:
|
|
payload = json.loads(content)
|
|
except json.JSONDecodeError as exc:
|
|
raise ValueError(f"极光推送模板渲染结果不是合法 JSON: {exc}") from exc
|
|
if not isinstance(payload, dict):
|
|
raise ValueError("极光推送模板渲染结果必须是 JSON 对象")
|
|
return payload
|
|
|
|
def _resolve_aliases(self, *, notifier, rendered_payload: dict, context: dict) -> list[str]:
|
|
aliases = rendered_payload.get("aliases") or notifier.get_config_value("aliases")
|
|
if aliases:
|
|
return self._normalize_aliases(aliases)
|
|
|
|
recipient_source = str(
|
|
rendered_payload.get("recipient_source")
|
|
or notifier.get_config_value("recipient_source", "participants")
|
|
or "participants"
|
|
)
|
|
employee_ids = []
|
|
if recipient_source == "participants":
|
|
employee_ids = context.get("participant_ids") or []
|
|
elif recipient_source == "creator":
|
|
employee_ids = [context.get("creator_id")]
|
|
elif recipient_source == "participants_and_creator":
|
|
employee_ids = [*(context.get("participant_ids") or []), context.get("creator_id")]
|
|
else:
|
|
raise ValueError(f"不支持的极光接收人来源: {recipient_source}")
|
|
|
|
alias_prefix = str(notifier.get_config_value("alias_prefix", "emp_") or "emp_")
|
|
resolved = []
|
|
for employee_id in employee_ids:
|
|
if employee_id is None or employee_id == "":
|
|
continue
|
|
resolved.append(f"{alias_prefix}{int(employee_id)}")
|
|
return self._normalize_aliases(resolved)
|
|
|
|
def _normalize_aliases(self, values) -> list[str]:
|
|
aliases = []
|
|
for value in values or []:
|
|
alias = str(value or "").strip()
|
|
if not alias:
|
|
raise ValueError("极光 aliases 中不能包含空值")
|
|
if len(alias.encode("utf-8")) > 40:
|
|
raise ValueError(f"极光 alias 超过 40 字节限制: {alias}")
|
|
aliases.append(alias)
|
|
aliases = list(dict.fromkeys(aliases))
|
|
if not aliases:
|
|
raise ValueError("极光推送接收 alias 不能为空")
|
|
if len(aliases) > 1000:
|
|
raise ValueError("极光单次推送 alias 不能超过 1000 个")
|
|
return aliases
|
|
|
|
def _build_payload(self, *, notifier, rendered_payload: dict, context: dict) -> dict:
|
|
aliases = self._resolve_aliases(
|
|
notifier=notifier,
|
|
rendered_payload=rendered_payload,
|
|
context=context,
|
|
)
|
|
platform = rendered_payload.get("platform") or notifier.get_config_value("platform", "all")
|
|
title = str(rendered_payload.get("title") or notifier.get_config_value("title", "任务提醒")).strip()
|
|
alert = str(rendered_payload.get("alert") or rendered_payload.get("content") or "").strip()
|
|
if not alert:
|
|
raise ValueError("极光推送 alert 不能为空")
|
|
|
|
extras = rendered_payload.get("extras") or {}
|
|
if not isinstance(extras, dict):
|
|
raise ValueError("极光推送 extras 必须是 JSON 对象")
|
|
|
|
notification = {
|
|
"alert": alert,
|
|
"android": {
|
|
"alert": alert,
|
|
"title": title,
|
|
"extras": extras,
|
|
},
|
|
"ios": {
|
|
"alert": alert,
|
|
"sound": rendered_payload.get("ios_sound") or notifier.get_config_value("ios_sound", "default"),
|
|
"extras": extras,
|
|
},
|
|
}
|
|
android_channel_id = rendered_payload.get("android_channel_id") or notifier.get_config_value("android_channel_id")
|
|
if android_channel_id:
|
|
notification["android"]["channel_id"] = str(android_channel_id)
|
|
android_intent = rendered_payload.get("android_intent") or notifier.get_config_value("android_intent")
|
|
if android_intent:
|
|
notification["android"]["intent"] = {"url": str(android_intent)}
|
|
android_category = rendered_payload.get("android_category") or notifier.get_config_value("android_category")
|
|
if android_category:
|
|
notification["android"]["category"] = str(android_category)
|
|
|
|
payload = {
|
|
"platform": platform,
|
|
"audience": {"alias": aliases},
|
|
"notification": notification,
|
|
"options": {
|
|
"apns_production": bool(notifier.get_config_value("apns_production", False)),
|
|
},
|
|
}
|
|
time_to_live = rendered_payload.get("time_to_live", notifier.get_config_value("time_to_live"))
|
|
if time_to_live is not None:
|
|
payload["options"]["time_to_live"] = int(time_to_live)
|
|
|
|
message = rendered_payload.get("message")
|
|
if message:
|
|
if not isinstance(message, dict):
|
|
raise ValueError("极光推送 message 必须是 JSON 对象")
|
|
payload["message"] = message
|
|
return payload
|
|
|
|
def notify(self, *, notifier, content: str, context: dict) -> dict:
|
|
rendered_payload = self._load_rendered_payload(content=content)
|
|
payload = self._build_payload(
|
|
notifier=notifier,
|
|
rendered_payload=rendered_payload,
|
|
context=context,
|
|
)
|
|
response = send_jpush_payload(
|
|
app_key=notifier.get_config_value("app_key"),
|
|
master_secret=notifier.get_config_value("master_secret"),
|
|
payload=payload,
|
|
timeout_seconds=float(notifier.get_config_value("timeout_seconds", 10.0) or 10.0),
|
|
)
|
|
result = {
|
|
"channel": self.channel,
|
|
"aliases": payload["audience"]["alias"],
|
|
"msg_id": response.raw.get("msg_id"),
|
|
"sendno": response.raw.get("sendno"),
|
|
"raw": response.raw,
|
|
}
|
|
logger.info("[notifier.backends] jpush sent: notifier_id=%s result=%s", notifier.id, result)
|
|
return result
|