新增腾讯短信(Tencent SMS)渠道扩展,支持在 Yuxi 平台中集成腾讯云短信渠道。 包含以下功能模块: - client: 腾讯云短信 API 客户端封装 - plugin: 渠道插件核心 - config: 渠道配置管理 - gateway: SSE/WebSocket 网关接入 - webhook: Webhook 事件处理 - outbound: 外发消息管理 - streaming: 流式消息处理 - security: 安全校验 - dedupe: 消息去重 - delivery: 送达状态回调 - compliance: 合规管理 - frequency: 频率控制 - templates: 短信模板 - agent_prompt: Agent 提示词 - types: 类型定义
496 lines
19 KiB
Python
496 lines
19 KiB
Python
from __future__ import annotations
|
|
|
|
import asyncio
|
|
import logging
|
|
from concurrent.futures import ThreadPoolExecutor
|
|
|
|
from .types import SendResult, TencentSmsAccount
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
_sms_executor = ThreadPoolExecutor(max_workers=10, thread_name_prefix="sms-")
|
|
|
|
MAX_BATCH_SIZE = 200
|
|
|
|
|
|
class TencentSmsClient:
|
|
def __init__(self, account: TencentSmsAccount):
|
|
self._account = account
|
|
self._client = None
|
|
|
|
def _build_client(self):
|
|
from tencentcloud.common import credential
|
|
from tencentcloud.common.profile.client_profile import ClientProfile
|
|
from tencentcloud.common.profile.http_profile import HttpProfile
|
|
from tencentcloud.sms.v20210111 import sms_client
|
|
|
|
cred = credential.Credential(self._account.secret_id, self._account.secret_key)
|
|
http_profile = HttpProfile()
|
|
http_profile.endpoint = self._account.endpoint
|
|
http_profile.reqTimeout = 30
|
|
client_profile = ClientProfile()
|
|
client_profile.httpProfile = http_profile
|
|
return sms_client.SmsClient(cred, self._account.region, client_profile)
|
|
|
|
def _get_client(self):
|
|
if self._client is None:
|
|
self._client = self._build_client()
|
|
return self._client
|
|
|
|
async def send_sms(
|
|
self,
|
|
phone_numbers: list[str],
|
|
template_id: str,
|
|
template_params: list[str],
|
|
sign_name: str = "",
|
|
session_context: str = "",
|
|
extend_code: str = "",
|
|
sender_id: str = "",
|
|
) -> list[SendResult]:
|
|
from tencentcloud.common.exception.tencent_cloud_sdk_exception import TencentCloudSDKException
|
|
from tencentcloud.sms.v20210111 import models
|
|
|
|
req = models.SendSmsRequest()
|
|
req.SmsSdkAppId = self._account.sms_sdk_app_id
|
|
req.SignName = sign_name or self._account.sign_name
|
|
req.TemplateId = template_id
|
|
req.TemplateParamSet = template_params
|
|
req.PhoneNumberSet = phone_numbers
|
|
req.SessionContext = session_context
|
|
|
|
ext = extend_code or self._account.extend_code
|
|
if ext:
|
|
req.ExtendCode = ext
|
|
sid = sender_id or self._account.sender_id
|
|
if sid:
|
|
req.SenderId = sid
|
|
|
|
loop = asyncio.get_running_loop()
|
|
try:
|
|
resp = await loop.run_in_executor(
|
|
_sms_executor,
|
|
self._get_client().SendSms,
|
|
req,
|
|
)
|
|
except TencentCloudSDKException as e:
|
|
logger.error("SendSms SDK \u5f02\u5e38: code=%s, message=%s", e.code, e.message)
|
|
raise
|
|
|
|
results = []
|
|
for status in resp.SendStatusSet:
|
|
results.append(
|
|
SendResult(
|
|
success=status.Code == "Ok",
|
|
serial_no=getattr(status, "SerialNo", ""),
|
|
phone_number=getattr(status, "PhoneNumber", ""),
|
|
code=status.Code or "",
|
|
message=getattr(status, "Message", ""),
|
|
fee=getattr(status, "Fee", 0),
|
|
iso_code=getattr(status, "IsoCode", ""),
|
|
session_context=getattr(status, "SessionContext", session_context),
|
|
)
|
|
)
|
|
return results
|
|
|
|
async def send_single(
|
|
self,
|
|
phone_number: str,
|
|
template_id: str,
|
|
template_params: list[str],
|
|
sign_name: str = "",
|
|
session_context: str = "",
|
|
extend_code: str = "",
|
|
sender_id: str = "",
|
|
) -> SendResult:
|
|
results = await self.send_sms(
|
|
[phone_number], template_id, template_params, sign_name, session_context, extend_code, sender_id
|
|
)
|
|
return results[0]
|
|
|
|
async def pull_send_status(self, limit: int = 100) -> list[dict]:
|
|
from tencentcloud.common.exception.tencent_cloud_sdk_exception import TencentCloudSDKException
|
|
from tencentcloud.sms.v20210111 import models
|
|
|
|
req = models.PullSmsSendStatusRequest()
|
|
req.SmsSdkAppId = self._account.sms_sdk_app_id
|
|
req.Limit = min(limit, 100)
|
|
|
|
loop = asyncio.get_running_loop()
|
|
try:
|
|
resp = await loop.run_in_executor(
|
|
_sms_executor,
|
|
self._get_client().PullSmsSendStatus,
|
|
req,
|
|
)
|
|
except TencentCloudSDKException as e:
|
|
logger.error("PullSmsSendStatus SDK \u5f02\u5e38: %s", e)
|
|
return []
|
|
|
|
result = []
|
|
for item in getattr(resp, "PullSmsSendStatusSet", []) or []:
|
|
result.append(
|
|
{
|
|
"serial_no": getattr(item, "SerialNo", ""),
|
|
"phone_number": getattr(item, "PhoneNumber", ""),
|
|
"report_status": getattr(item, "ReportStatus", ""),
|
|
"description": getattr(item, "Description", ""),
|
|
"user_receive_time": getattr(item, "UserReceiveTime", ""),
|
|
"session_context": getattr(item, "SessionContext", ""),
|
|
}
|
|
)
|
|
return result
|
|
|
|
async def pull_send_status_by_phone(
|
|
self, phone_number: str, begin_time: int = 0, end_time: int = 0, limit: int = 100
|
|
) -> list[dict]:
|
|
from tencentcloud.common.exception.tencent_cloud_sdk_exception import TencentCloudSDKException
|
|
from tencentcloud.sms.v20210111 import models
|
|
|
|
req = models.PullSmsSendStatusByPhoneNumberRequest()
|
|
req.SmsSdkAppId = self._account.sms_sdk_app_id
|
|
req.PhoneNumber = phone_number
|
|
req.SendDateTime = begin_time
|
|
req.EndDateTime = end_time
|
|
req.Limit = min(limit, 100)
|
|
|
|
loop = asyncio.get_running_loop()
|
|
try:
|
|
resp = await loop.run_in_executor(
|
|
_sms_executor,
|
|
self._get_client().PullSmsSendStatusByPhoneNumber,
|
|
req,
|
|
)
|
|
except TencentCloudSDKException as e:
|
|
logger.error("PullSmsSendStatusByPhoneNumber SDK \u5f02\u5e38: %s", e)
|
|
return []
|
|
|
|
result = []
|
|
for item in getattr(resp, "PullSmsSendStatusSet", []) or []:
|
|
result.append(
|
|
{
|
|
"serial_no": getattr(item, "SerialNo", ""),
|
|
"phone_number": getattr(item, "PhoneNumber", ""),
|
|
"report_status": getattr(item, "ReportStatus", ""),
|
|
"description": getattr(item, "Description", ""),
|
|
"user_receive_time": getattr(item, "UserReceiveTime", ""),
|
|
"session_context": getattr(item, "SessionContext", ""),
|
|
}
|
|
)
|
|
return result
|
|
|
|
async def pull_reply_status_by_phone(
|
|
self, phone_number: str, begin_time: int = 0, end_time: int = 0, limit: int = 100
|
|
) -> list[dict]:
|
|
from tencentcloud.common.exception.tencent_cloud_sdk_exception import TencentCloudSDKException
|
|
from tencentcloud.sms.v20210111 import models
|
|
|
|
req = models.PullSmsReplyStatusByPhoneNumberRequest()
|
|
req.SmsSdkAppId = self._account.sms_sdk_app_id
|
|
req.PhoneNumber = phone_number
|
|
req.SendDateTime = begin_time
|
|
req.EndDateTime = end_time
|
|
req.Limit = min(limit, 100)
|
|
|
|
loop = asyncio.get_running_loop()
|
|
try:
|
|
resp = await loop.run_in_executor(
|
|
_sms_executor,
|
|
self._get_client().PullSmsReplyStatusByPhoneNumber,
|
|
req,
|
|
)
|
|
except TencentCloudSDKException as e:
|
|
logger.error("PullSmsReplyStatusByPhoneNumber SDK \u5f02\u5e38: %s", e)
|
|
return []
|
|
|
|
result = []
|
|
for item in getattr(resp, "PullSmsReplyStatusSet", []) or []:
|
|
result.append(
|
|
{
|
|
"phone_number": getattr(item, "PhoneNumber", ""),
|
|
"reply_content": getattr(item, "ReplyContent", ""),
|
|
"reply_time": getattr(item, "ReplyTime", ""),
|
|
"reply_unix_time": getattr(item, "ReplyUnixTime", 0),
|
|
"extend_code": getattr(item, "ExtendCode", ""),
|
|
}
|
|
)
|
|
return result
|
|
|
|
async def pull_reply_status(self, limit: int = 100) -> list[dict]:
|
|
from tencentcloud.common.exception.tencent_cloud_sdk_exception import TencentCloudSDKException
|
|
from tencentcloud.sms.v20210111 import models
|
|
|
|
req = models.PullSmsReplyStatusRequest()
|
|
req.SmsSdkAppId = self._account.sms_sdk_app_id
|
|
req.Limit = min(limit, 100)
|
|
|
|
loop = asyncio.get_running_loop()
|
|
try:
|
|
resp = await loop.run_in_executor(
|
|
_sms_executor,
|
|
self._get_client().PullSmsReplyStatus,
|
|
req,
|
|
)
|
|
except TencentCloudSDKException as e:
|
|
logger.error("PullSmsReplyStatus SDK \u5f02\u5e38: %s", e)
|
|
return []
|
|
|
|
result = []
|
|
for item in getattr(resp, "PullSmsReplyStatusSet", []) or []:
|
|
result.append(
|
|
{
|
|
"phone_number": getattr(item, "PhoneNumber", ""),
|
|
"reply_content": getattr(item, "ReplyContent", ""),
|
|
"reply_time": getattr(item, "ReplyTime", ""),
|
|
"reply_unix_time": getattr(item, "ReplyUnixTime", 0),
|
|
"extend_code": getattr(item, "ExtendCode", ""),
|
|
}
|
|
)
|
|
return result
|
|
|
|
async def get_send_statistics(self, begin_time: str, end_time: str, page_size: int = 10, page_no: int = 1) -> dict:
|
|
from tencentcloud.common.exception.tencent_cloud_sdk_exception import TencentCloudSDKException
|
|
from tencentcloud.sms.v20210111 import models
|
|
|
|
req = models.SendStatusStatisticsRequest()
|
|
req.SmsSdkAppId = self._account.sms_sdk_app_id
|
|
req.BeginTime = begin_time
|
|
req.EndTime = end_time
|
|
req.PageSize = page_size
|
|
req.PageNumber = page_no
|
|
|
|
loop = asyncio.get_running_loop()
|
|
try:
|
|
resp = await loop.run_in_executor(
|
|
_sms_executor,
|
|
self._get_client().SendStatusStatistics,
|
|
req,
|
|
)
|
|
except TencentCloudSDKException as e:
|
|
logger.error("SendStatusStatistics SDK \u5f02\u5e38: %s", e)
|
|
return {}
|
|
|
|
data = getattr(resp, "Data", None)
|
|
if not data:
|
|
return {}
|
|
return {
|
|
"total_count": getattr(data, "TotalCount", 0),
|
|
"send_count": getattr(data, "SendCount", 0),
|
|
"success_count": getattr(data, "SuccessCount", 0),
|
|
"fail_count": getattr(data, "FailCount", 0),
|
|
"details": [
|
|
{
|
|
"date": getattr(d, "Date", ""),
|
|
"send_count": getattr(d, "SendCount", 0),
|
|
"success_count": getattr(d, "SuccessCount", 0),
|
|
"fail_count": getattr(d, "FailCount", 0),
|
|
}
|
|
for d in getattr(data, "Details", []) or []
|
|
],
|
|
}
|
|
|
|
async def get_callback_statistics(
|
|
self, begin_time: str, end_time: str, page_size: int = 10, page_no: int = 1
|
|
) -> dict:
|
|
from tencentcloud.common.exception.tencent_cloud_sdk_exception import TencentCloudSDKException
|
|
from tencentcloud.sms.v20210111 import models
|
|
|
|
req = models.CallbackStatusStatisticsRequest()
|
|
req.SmsSdkAppId = self._account.sms_sdk_app_id
|
|
req.BeginTime = begin_time
|
|
req.EndTime = end_time
|
|
req.PageSize = page_size
|
|
req.PageNumber = page_no
|
|
|
|
loop = asyncio.get_running_loop()
|
|
try:
|
|
resp = await loop.run_in_executor(
|
|
_sms_executor,
|
|
self._get_client().CallbackStatusStatistics,
|
|
req,
|
|
)
|
|
except TencentCloudSDKException as e:
|
|
logger.error("CallbackStatusStatistics SDK \u5f02\u5e38: %s", e)
|
|
return {}
|
|
|
|
data = getattr(resp, "Data", None)
|
|
if not data:
|
|
return {}
|
|
return {
|
|
"total_count": getattr(data, "TotalCount", 0),
|
|
"callback_count": getattr(data, "CallbackCount", 0),
|
|
"success_count": getattr(data, "SuccessCount", 0),
|
|
"fail_count": getattr(data, "FailCount", 0),
|
|
"details": [
|
|
{
|
|
"date": getattr(d, "Date", ""),
|
|
"callback_count": getattr(d, "CallbackCount", 0),
|
|
"success_count": getattr(d, "SuccessCount", 0),
|
|
"fail_count": getattr(d, "FailCount", 0),
|
|
}
|
|
for d in getattr(data, "Details", []) or []
|
|
],
|
|
}
|
|
|
|
async def describe_phone_number_info(self, phone_numbers: list[str]) -> list[dict]:
|
|
from tencentcloud.common.exception.tencent_cloud_sdk_exception import TencentCloudSDKException
|
|
from tencentcloud.sms.v20210111 import models
|
|
|
|
req = models.DescribePhoneNumberInfoRequest()
|
|
req.SmsSdkAppId = self._account.sms_sdk_app_id
|
|
req.PhoneNumberSet = phone_numbers
|
|
|
|
loop = asyncio.get_running_loop()
|
|
try:
|
|
resp = await loop.run_in_executor(
|
|
_sms_executor,
|
|
self._get_client().DescribePhoneNumberInfo,
|
|
req,
|
|
)
|
|
except TencentCloudSDKException as e:
|
|
logger.error("DescribePhoneNumberInfo SDK \u5f02\u5e38: %s", e)
|
|
return []
|
|
|
|
result = []
|
|
for item in getattr(resp, "PhoneNumberInfoSet", []) or []:
|
|
result.append(
|
|
{
|
|
"phone_number": getattr(item, "PhoneNumber", ""),
|
|
"nation_code": getattr(item, "NationCode", ""),
|
|
"subscriber_number": getattr(item, "SubscriberNumber", ""),
|
|
"iso_code": getattr(item, "IsoCode", ""),
|
|
"iso_name": getattr(item, "IsoName", ""),
|
|
}
|
|
)
|
|
return result
|
|
|
|
async def report_conversion(
|
|
self,
|
|
phone_number: str,
|
|
serial_no: str,
|
|
conversion_time: int = 0,
|
|
remark: str = "",
|
|
) -> bool:
|
|
import time
|
|
|
|
from tencentcloud.common.exception.tencent_cloud_sdk_exception import TencentCloudSDKException
|
|
from tencentcloud.sms.v20210111 import models
|
|
|
|
req = models.ReportConversionRequest()
|
|
req.SmsSdkAppId = self._account.sms_sdk_app_id
|
|
req.PhoneNumber = phone_number
|
|
req.SerialNo = serial_no
|
|
req.ConversionTime = conversion_time or int(time.time())
|
|
req.Remark = remark
|
|
|
|
loop = asyncio.get_running_loop()
|
|
try:
|
|
resp = await loop.run_in_executor(
|
|
_sms_executor,
|
|
self._get_client().ReportConversion,
|
|
req,
|
|
)
|
|
return getattr(resp, "Result", False)
|
|
except TencentCloudSDKException as e:
|
|
logger.error("ReportConversion SDK \u5f02\u5e38: %s", e)
|
|
return False
|
|
|
|
async def describe_sms_sign_list(self, sign_name_list: list[str] | None = None) -> list[dict]:
|
|
from tencentcloud.common.exception.tencent_cloud_sdk_exception import TencentCloudSDKException
|
|
from tencentcloud.sms.v20210111 import models
|
|
|
|
req = models.DescribeSmsSignListRequest()
|
|
req.SmsSdkAppId = self._account.sms_sdk_app_id
|
|
req.SignNameList = sign_name_list or [self._account.sign_name]
|
|
|
|
loop = asyncio.get_running_loop()
|
|
try:
|
|
resp = await loop.run_in_executor(
|
|
_sms_executor,
|
|
self._get_client().DescribeSmsSignList,
|
|
req,
|
|
)
|
|
except TencentCloudSDKException as e:
|
|
logger.error("DescribeSmsSignList SDK \u5f02\u5e38: %s", e)
|
|
return []
|
|
|
|
result = []
|
|
for item in getattr(resp, "DescribeSmsSignListStatusSet", []) or []:
|
|
result.append(
|
|
{
|
|
"sign_name": getattr(item, "SignName", ""),
|
|
"sign_type": getattr(item, "SignType", 0),
|
|
"document": getattr(item, "Document", ""),
|
|
"international": getattr(item, "International", 0),
|
|
"status_code": getattr(item, "StatusCode", 0),
|
|
"review_reply": getattr(item, "ReviewReply", ""),
|
|
"create_time": getattr(item, "CreateTime", ""),
|
|
}
|
|
)
|
|
return result
|
|
|
|
async def describe_sms_template_list(
|
|
self, template_id_set: list[str] | None = None, international: int = 0
|
|
) -> list[dict]:
|
|
from tencentcloud.common.exception.tencent_cloud_sdk_exception import TencentCloudSDKException
|
|
from tencentcloud.sms.v20210111 import models
|
|
|
|
req = models.DescribeSmsTemplateListRequest()
|
|
req.SmsSdkAppId = self._account.sms_sdk_app_id
|
|
req.International = international
|
|
if template_id_set:
|
|
req.TemplateIdSet = [int(tid) for tid in template_id_set if tid.isdigit()]
|
|
|
|
loop = asyncio.get_running_loop()
|
|
try:
|
|
resp = await loop.run_in_executor(
|
|
_sms_executor,
|
|
self._get_client().DescribeSmsTemplateList,
|
|
req,
|
|
)
|
|
except TencentCloudSDKException as e:
|
|
logger.error("DescribeSmsTemplateList SDK \u5f02\u5e38: %s", e)
|
|
return []
|
|
|
|
result = []
|
|
for item in getattr(resp, "DescribeSmsTemplateListStatusSet", []) or []:
|
|
result.append(
|
|
{
|
|
"template_id": str(getattr(item, "TemplateId", "")),
|
|
"template_name": getattr(item, "TemplateName", ""),
|
|
"template_content": getattr(item, "TemplateContent", ""),
|
|
"template_type": getattr(item, "TemplateType", 0),
|
|
"international": getattr(item, "International", 0),
|
|
"status_code": getattr(item, "StatusCode", 0),
|
|
"review_reply": getattr(item, "ReviewReply", ""),
|
|
"create_time": getattr(item, "CreateTime", ""),
|
|
}
|
|
)
|
|
return result
|
|
|
|
async def probe(self) -> bool:
|
|
from tencentcloud.common import credential
|
|
from tencentcloud.common.exception.tencent_cloud_sdk_exception import TencentCloudSDKException
|
|
from tencentcloud.sms.v20210111 import models, sms_client
|
|
|
|
try:
|
|
cred = credential.Credential(self._account.secret_id, self._account.secret_key)
|
|
loop = asyncio.get_running_loop()
|
|
|
|
def _check():
|
|
try:
|
|
client = sms_client.SmsClient(cred, self._account.region)
|
|
req = models.DescribeSmsSignListRequest()
|
|
req.SmsSdkAppId = self._account.sms_sdk_app_id
|
|
req.SignNameList = [self._account.sign_name]
|
|
client.DescribeSmsSignList(req)
|
|
return True
|
|
except TencentCloudSDKException as e:
|
|
if "AuthFailure" in (e.code or ""):
|
|
return False
|
|
return True
|
|
|
|
return await loop.run_in_executor(_sms_executor, _check)
|
|
except Exception as e:
|
|
logger.warning("probe exception: %s", e)
|
|
return False
|