Files
AIcreat/app/services/im.py
2026-04-06 14:20:53 +08:00

81 lines
2.7 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
from __future__ import annotations
import hashlib
import hmac
import base64
import logging
import time
from urllib.parse import quote_plus
import httpx
from urllib.parse import urlparse
from app.config import settings
from app.schemas import IMPublishRequest, PublishResponse
logger = logging.getLogger(__name__)
class IMPublisher:
async def publish(self, req: IMPublishRequest, request_id: str = "") -> PublishResponse:
rid = request_id or "-"
if not settings.im_webhook_url:
logger.warning("im_skipped rid=%s reason=empty_webhook_url", rid)
return PublishResponse(ok=False, detail="缺少 IM_WEBHOOK_URL 配置")
parsed = urlparse(settings.im_webhook_url)
host = parsed.netloc or "(invalid_url)"
logger.info(
"im_publish_start rid=%s webhook_host=%s sign_enabled=%s title_chars=%d body_truncated_to=3800",
rid,
host,
bool(settings.im_secret),
len(req.title or ""),
)
webhook = self._with_signature(settings.im_webhook_url, settings.im_secret)
payload = {
"msg_type": "post",
"content": {
"post": {
"zh_cn": {
"title": req.title,
"content": [[{"tag": "text", "text": req.body_markdown[:3800]}]],
}
}
},
}
async with httpx.AsyncClient(timeout=20) as client:
logger.info("im_http_post rid=%s method=POST timeout_s=20", rid)
r = await client.post(webhook, json=payload)
try:
data = r.json()
except Exception:
data = {"status_code": r.status_code, "text": r.text}
logger.info(
"im_http_response rid=%s status=%s body_preview=%s",
rid,
r.status_code,
str(data)[:500],
)
if r.status_code >= 400:
logger.warning("im_push_failed rid=%s http_status=%s", rid, r.status_code)
return PublishResponse(ok=False, detail=f"IM 推送失败: {data}", data=data)
logger.info("im_push_ok rid=%s", rid)
return PublishResponse(ok=True, detail="IM 推送成功", data=data)
def _with_signature(self, webhook: str, secret: str | None) -> str:
# 兼容飞书 webhook 签名参数timestamp/sign
if not secret:
return webhook
ts = str(int(time.time()))
string_to_sign = f"{ts}\n{secret}".encode("utf-8")
sign = base64.b64encode(hmac.new(string_to_sign, b"", hashlib.sha256).digest()).decode("utf-8")
connector = "&" if "?" in webhook else "?"
return f"{webhook}{connector}timestamp={ts}&sign={quote_plus(sign)}"