All checks were successful
Java Maven 3.9.9 & JDK 26 CI/CD Pipeline / build-and-deploy (push) Successful in 3m22s
127 lines
4.5 KiB
Python
127 lines
4.5 KiB
Python
import base64
|
|
import hashlib
|
|
import html
|
|
import os
|
|
import re
|
|
from urllib.parse import parse_qs, urlparse
|
|
|
|
import httpx
|
|
|
|
from .base import ImageAttachment, Notifier
|
|
from .image_utils import compressed_preview
|
|
|
|
|
|
def _truncate_utf8(value: str, limit: int) -> str:
|
|
data = value.encode("utf-8")
|
|
if len(data) <= limit:
|
|
return value
|
|
return data[:limit - 3].decode("utf-8", errors="ignore") + "..."
|
|
|
|
|
|
def _format_plain_text(title: str, text: str) -> str:
|
|
plain = re.sub(r"<br\s*/?>", "\n", text, flags=re.IGNORECASE)
|
|
plain = re.sub(r"<[^>]+>", "", plain)
|
|
plain = html.unescape(plain)
|
|
if not title:
|
|
return plain.strip()
|
|
|
|
field_icons = {
|
|
"发送人:": "👤",
|
|
"类别:": "🗂",
|
|
"待行动:": "✅",
|
|
"截止:": "⏰",
|
|
}
|
|
section_icons = {
|
|
"摘要:": "📌",
|
|
"原因:": "📝",
|
|
"原文:": "💬",
|
|
}
|
|
output: list[str] = []
|
|
for line in plain.splitlines():
|
|
stripped = line.strip()
|
|
matched = False
|
|
for label, icon in field_icons.items():
|
|
if stripped.startswith(label):
|
|
output.append(f"{icon} {stripped}")
|
|
matched = True
|
|
break
|
|
if matched:
|
|
continue
|
|
for label, icon in section_icons.items():
|
|
if stripped.startswith(label):
|
|
if output and output[-1] != "":
|
|
output.append("")
|
|
output.append(f"{icon} {label[:-1]}")
|
|
value = stripped[len(label):].strip()
|
|
if value:
|
|
output.append(value)
|
|
matched = True
|
|
break
|
|
if not matched:
|
|
output.append(line)
|
|
|
|
body = "\n".join(output).strip()
|
|
if not title:
|
|
return body
|
|
icon = "🔴" if "重要" in title else "🔔"
|
|
return f"{icon} {title}\n━━━━━━━━━━━━\n\n{body}" if body else f"{icon} {title}"
|
|
|
|
|
|
class WecomWebhookNotifier(Notifier):
|
|
name = "wecom_webhook"
|
|
|
|
def __init__(self, urls: list[str] | None = None) -> None:
|
|
configured = urls or []
|
|
if not configured:
|
|
configured = [part.strip() for part in os.getenv(
|
|
"WECOM_WEBHOOK_URLS", ""
|
|
).split(";") if part.strip()]
|
|
self.urls = [self._validate_url(url) for url in configured]
|
|
if not self.urls:
|
|
raise RuntimeError("WECOM_WEBHOOK_URLS is not configured")
|
|
|
|
@staticmethod
|
|
def _validate_url(value: str) -> str:
|
|
parsed = urlparse(value.strip())
|
|
key = parse_qs(parsed.query).get("key", [])
|
|
if (parsed.scheme != "https" or parsed.hostname != "qyapi.weixin.qq.com"
|
|
or parsed.path != "/cgi-bin/webhook/send" or not key or not key[0]):
|
|
raise RuntimeError("invalid Enterprise WeChat webhook URL")
|
|
return value.strip()
|
|
|
|
async def send(self, title: str, text: str,
|
|
images: list[ImageAttachment] | None = None) -> None:
|
|
content = _truncate_utf8(_format_plain_text(title, text), 2048)
|
|
failures = 0
|
|
async with httpx.AsyncClient(timeout=90, trust_env=False) as client:
|
|
for url in self.urls:
|
|
try:
|
|
response = await client.post(
|
|
url,
|
|
json={"msgtype": "text", "text": {"content": content}},
|
|
)
|
|
data = response.json()
|
|
ok = response.status_code < 400 and data.get("errcode") == 0
|
|
if ok:
|
|
for image in images or []:
|
|
preview = compressed_preview(image)
|
|
encoded = base64.b64encode(preview.data).decode("ascii")
|
|
digest = hashlib.md5(preview.data).hexdigest()
|
|
response = await client.post(
|
|
url,
|
|
json={
|
|
"msgtype": "image",
|
|
"image": {"base64": encoded, "md5": digest},
|
|
},
|
|
)
|
|
data = response.json()
|
|
if response.status_code >= 400 or data.get("errcode") != 0:
|
|
ok = False
|
|
break
|
|
except Exception:
|
|
ok = False
|
|
if not ok:
|
|
failures += 1
|
|
if failures:
|
|
raise RuntimeError(f"Enterprise WeChat webhook failed for {failures} target(s)")
|