214 lines
8.7 KiB
Python
214 lines
8.7 KiB
Python
from __future__ import annotations
|
|
|
|
import asyncio
|
|
import json
|
|
import logging
|
|
|
|
from fastapi import APIRouter, Request, Response
|
|
|
|
from .dedupe import AliyunSmsDeduplicator
|
|
from .format import mask_phone
|
|
from .monitor import AliyunSmsMonitor
|
|
from .signature import verify_callback_signature
|
|
from .types import InboundSmsMessage, SmsReportMessage, TemplateSmsReport
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
router = APIRouter(prefix="/webhook/aliyun-sms", tags=["aliyun-sms"])
|
|
|
|
|
|
class AliyunSmsWebhookHandler:
|
|
def __init__(self, gateway):
|
|
self._gateway = gateway
|
|
self._monitor = AliyunSmsMonitor()
|
|
self._mo_dedupe = AliyunSmsDeduplicator(max_size=10000, ttl=86400)
|
|
self._report_dedupe = AliyunSmsDeduplicator(max_size=50000, ttl=86400)
|
|
|
|
async def handle_mo_callback(self, request: Request) -> Response:
|
|
raw_body = await request.body()
|
|
|
|
account = self._gateway.account
|
|
if account.access_key_secret:
|
|
if not verify_callback_signature(raw_body, request.headers, account.access_key_secret):
|
|
logger.warning("阿里云短信 MO 回调签名验证失败: account=%s", account.account_id)
|
|
return Response(content='{"code":"signature_invalid"}', media_type="application/json", status_code=403)
|
|
|
|
asyncio.create_task(self._process_mo_messages(raw_body))
|
|
|
|
return Response(content='{"code":"ok"}', media_type="application/json", status_code=200)
|
|
|
|
async def handle_report_callback(self, request: Request) -> Response:
|
|
raw_body = await request.body()
|
|
|
|
account = self._gateway.account
|
|
if account.access_key_secret:
|
|
if not verify_callback_signature(raw_body, request.headers, account.access_key_secret):
|
|
logger.warning("阿里云短信 回执回调签名验证失败: account=%s", account.account_id)
|
|
return Response(content='{"code":"signature_invalid"}', media_type="application/json", status_code=403)
|
|
|
|
asyncio.create_task(self._process_report_messages(raw_body))
|
|
|
|
return Response(content='{"code":"ok"}', media_type="application/json", status_code=200)
|
|
|
|
async def handle_template_report_callback(self, request: Request) -> Response:
|
|
raw_body = await request.body()
|
|
|
|
account = self._gateway.account
|
|
if account.access_key_secret:
|
|
if not verify_callback_signature(raw_body, request.headers, account.access_key_secret):
|
|
logger.warning("阿里云模板审核回调签名验证失败: account=%s", account.account_id)
|
|
return Response(content='{"code":"signature_invalid"}', media_type="application/json", status_code=403)
|
|
|
|
asyncio.create_task(self._process_template_report(raw_body))
|
|
|
|
return Response(content='{"code":"ok"}', media_type="application/json", status_code=200)
|
|
|
|
async def _process_template_report(self, raw_body: bytes):
|
|
try:
|
|
data = json.loads(raw_body)
|
|
except json.JSONDecodeError as e:
|
|
logger.error("阿里云模板审核回调 JSON 解析失败: %s", e)
|
|
return
|
|
|
|
messages = data if isinstance(data, list) else [data]
|
|
|
|
for msg_data in messages:
|
|
report = TemplateSmsReport(
|
|
template_code=msg_data.get("template_code", ""),
|
|
template_status=msg_data.get("template_status", ""),
|
|
reject_reason=msg_data.get("reject_reason", ""),
|
|
create_date=msg_data.get("create_date", ""),
|
|
)
|
|
|
|
status_info = "通过" if report.template_status == "approved" else f"驳回({report.reject_reason})"
|
|
logger.info(
|
|
"阿里云模板审核状态变更: template_code=%s status=%s",
|
|
report.template_code,
|
|
status_info,
|
|
)
|
|
|
|
async def _process_mo_messages(self, raw_body: bytes):
|
|
try:
|
|
data = json.loads(raw_body)
|
|
except json.JSONDecodeError as e:
|
|
logger.error("阿里云短信 MO 回调 JSON 解析失败: %s", e)
|
|
return
|
|
|
|
messages = data if isinstance(data, list) else [data]
|
|
|
|
for msg_data in messages:
|
|
seq_id = msg_data.get("sequence_id", 0)
|
|
dedupe_key = str(seq_id)
|
|
if self._mo_dedupe.is_duplicate(dedupe_key):
|
|
logger.debug("阿里云短信 MO 消息重复: sequence_id=%s", seq_id)
|
|
continue
|
|
self._mo_dedupe.mark_seen(dedupe_key)
|
|
|
|
inbound = InboundSmsMessage(
|
|
dest_code=msg_data.get("dest_code", ""),
|
|
send_time=msg_data.get("send_time", ""),
|
|
content=msg_data.get("content", ""),
|
|
sign_name=msg_data.get("sign_name", ""),
|
|
sequence_id=seq_id,
|
|
phone_number=msg_data.get("phone_number", ""),
|
|
extended_code=msg_data.get("extended_code", ""),
|
|
)
|
|
|
|
queue = self._gateway.queue
|
|
session_manager = self._gateway.session_manager
|
|
session = session_manager.resolve_session(inbound.phone_number)
|
|
session_id = session.session_id
|
|
|
|
await queue.put(
|
|
{
|
|
"channel_type": "aliyun-sms",
|
|
"account_id": self._gateway.account.account_id,
|
|
"message": inbound,
|
|
"message_type": "mo",
|
|
"session_id": session_id,
|
|
}
|
|
)
|
|
|
|
logger.debug(
|
|
"阿里云短信 MO 消息已入队: phone=%s seq_id=%s",
|
|
mask_phone(inbound.phone_number, self._gateway.account.phone_mask_enabled),
|
|
seq_id,
|
|
)
|
|
|
|
unified = self._monitor.convert_to_unified(inbound)
|
|
logger.debug("阿里云短信 MO 转换为 UnifiedMessage: %s", json.dumps(unified, ensure_ascii=False))
|
|
|
|
async def _process_report_messages(self, raw_body: bytes):
|
|
try:
|
|
data = json.loads(raw_body)
|
|
except json.JSONDecodeError as e:
|
|
logger.error("阿里云短信 回执回调 JSON 解析失败: %s", e)
|
|
return
|
|
|
|
messages = data if isinstance(data, list) else [data]
|
|
|
|
for msg_data in messages:
|
|
biz_id = msg_data.get("biz_id", "")
|
|
report_key = f"{biz_id}_{msg_data.get('report_time', '')}"
|
|
if self._report_dedupe.is_duplicate(report_key):
|
|
continue
|
|
self._report_dedupe.mark_seen(report_key)
|
|
|
|
report = SmsReportMessage(
|
|
phone_number=msg_data.get("phone_number", ""),
|
|
send_time=msg_data.get("send_time", ""),
|
|
report_time=msg_data.get("report_time", ""),
|
|
success=msg_data.get("success", False),
|
|
err_code=msg_data.get("err_code", ""),
|
|
err_msg=msg_data.get("err_msg", ""),
|
|
biz_id=biz_id,
|
|
out_id=msg_data.get("out_id", ""),
|
|
sms_size=msg_data.get("sms_size", 1),
|
|
)
|
|
|
|
if report.success:
|
|
logger.info(
|
|
"短信送达: phone=%s biz_id=%s",
|
|
mask_phone(report.phone_number, self._gateway.account.phone_mask_enabled),
|
|
biz_id,
|
|
)
|
|
else:
|
|
logger.warning(
|
|
"短信发送失败: phone=%s biz_id=%s err=%s",
|
|
mask_phone(report.phone_number, self._gateway.account.phone_mask_enabled),
|
|
biz_id,
|
|
report.err_code,
|
|
)
|
|
|
|
|
|
_sms_webhook_handlers: dict[str, AliyunSmsWebhookHandler] = {}
|
|
|
|
|
|
def register_webhook_handler(handler: AliyunSmsWebhookHandler):
|
|
_sms_webhook_handlers[handler._gateway.account.account_id] = handler
|
|
|
|
|
|
def unregister_webhook_handler(account_id: str):
|
|
_sms_webhook_handlers.pop(account_id, None)
|
|
|
|
|
|
@router.post("/callback")
|
|
async def aliyun_sms_mo_callback(request: Request):
|
|
if not _sms_webhook_handlers:
|
|
return Response(content='{"code":"handler not available"}', media_type="application/json", status_code=503)
|
|
return await _sms_webhook_handlers["default"].handle_mo_callback(request)
|
|
|
|
|
|
@router.post("/report")
|
|
async def aliyun_sms_report_callback(request: Request):
|
|
if not _sms_webhook_handlers:
|
|
return Response(content='{"code":"handler not available"}', media_type="application/json", status_code=503)
|
|
return await _sms_webhook_handlers["default"].handle_report_callback(request)
|
|
|
|
|
|
@router.post("/template-report")
|
|
async def aliyun_sms_template_report_callback(request: Request):
|
|
if not _sms_webhook_handlers:
|
|
return Response(content='{"code":"handler not available"}', media_type="application/json", status_code=503)
|
|
return await _sms_webhook_handlers["default"].handle_template_report_callback(request)
|