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