// Package sms implements cloud SMS delivery using saved integration settings. package sms import ( "context" "encoding/json" "errors" "regexp" "strings" "time" openapi "github.com/alibabacloud-go/darabonba-openapi/v2/client" aliyun "github.com/alibabacloud-go/dysmsapi-20170525/v4/client" util "github.com/alibabacloud-go/tea-utils/v2/service" "github.com/alibabacloud-go/tea/tea" kratoserrors "github.com/go-kratos/kratos/v3/errors" "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common" tcerrors "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/errors" "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/profile" tencent "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/sms/v20210111" biz "kra/internal/biz/integration" "kra/internal/config" "kra/internal/integration/runtimeconfig" ) type aliyunClient interface { SendSmsWithOptions(*aliyun.SendSmsRequest, *util.RuntimeOptions) (*aliyun.SendSmsResponse, error) } type tencentClient interface { SendSmsWithContext(context.Context, *tencent.SendSmsRequest) (*tencent.SendSmsResponse, error) } type Sender struct { store *runtimeconfig.Store newAliyun func(*openapi.Config) (aliyunClient, error) newTencent func(string, string, string) (tencentClient, error) } func NewSender(store *runtimeconfig.Store) biz.SMSSender { return &Sender{store: store, newAliyun: func(c *openapi.Config) (aliyunClient, error) { return aliyun.NewClient(c) }, newTencent: func(id, secret, region string) (tencentClient, error) { p := profile.NewClientProfile() p.HttpProfile.Endpoint = "sms.tencentcloudapi.com" p.HttpProfile.ReqTimeout = 10 p.DisableRegionBreaker = true p.NetworkFailureMaxRetries = 0 p.RateLimitExceededMaxRetries = 0 p.UnsafeRetryOnConnectionFailure = false return tencent.NewClient(common.NewCredential(id, secret), region, p) }, } } func (s *Sender) Send(ctx context.Context, m *biz.SMSMessage) (*biz.SMSResult, error) { if err := biz.ValidateSMSMessage(m); err != nil { return nil, err } if err := ctx.Err(); err != nil { return nil, err } c, ok := s.store.Get(biz.IntegrationKindSMS, m.Provider) if !ok || !c.Enabled { return nil, biz.ErrSMSDisabled } values := map[string]any{} if json.Unmarshal(c.Values, &values) != nil { return nil, biz.ErrSMSInvalid } if err := biz.ValidateIntegrationConfig(biz.IntegrationKindSMS, m.Provider, values); err != nil { return nil, err } for key, value := range values { if text, ok := value.(string); ok && biz.IsIntegrationSecretKey(key) && config.IsMaskedSecret(text) { return nil, biz.ErrSMSDisabled } } get := func(key string) string { v, _ := values[key].(string); return v } if m.Provider == "aliyun" { return s.sendAliyun(ctx, m, get) } return s.sendTencent(ctx, m, get) } func (s *Sender) sendAliyun(ctx context.Context, m *biz.SMSMessage, get func(string) string) (*biz.SMSResult, error) { // This SDK has no context-aware SendSms; bound both connection and read time. timeout := 10000 if deadline, ok := ctx.Deadline(); ok { remaining := int(time.Until(deadline).Milliseconds()) if remaining <= 0 { return nil, context.DeadlineExceeded } if remaining < timeout { timeout = remaining } } client, err := s.newAliyun(&openapi.Config{ AccessKeyId: tea.String(get("access_key_id")), AccessKeySecret: tea.String(get("access_key_secret")), Endpoint: tea.String("dysmsapi.aliyuncs.com"), Protocol: tea.String("https"), ConnectTimeout: tea.Int(timeout), ReadTimeout: tea.Int(timeout), }) if err != nil { return nil, sdkError(err) } params := m.NamedParams if params == nil { params = map[string]string{} } raw, _ := json.Marshal(params) response, err := client.SendSmsWithOptions(&aliyun.SendSmsRequest{ PhoneNumbers: tea.String(strings.TrimPrefix(m.Phone, "+86")), SignName: tea.String(get("sign_name")), TemplateCode: tea.String(get("template_id")), TemplateParam: tea.String(string(raw)), }, &util.RuntimeOptions{Autoretry: tea.Bool(false), MaxAttempts: tea.Int(1), ConnectTimeout: tea.Int(timeout), ReadTimeout: tea.Int(timeout)}) if err != nil { return nil, sdkError(err) } if response == nil || response.Body == nil { return nil, biz.ErrSMSFailed } if tea.StringValue(response.Body.Code) != "OK" { return nil, providerError(tea.StringValue(response.Body.Code)) } return &biz.SMSResult{RequestID: tea.StringValue(response.Body.RequestId), MessageID: tea.StringValue(response.Body.BizId)}, nil } func (s *Sender) sendTencent(ctx context.Context, m *biz.SMSMessage, get func(string) string) (*biz.SMSResult, error) { client, err := s.newTencent(get("secret_id"), get("secret_key"), get("region")) if err != nil { return nil, sdkError(err) } request := tencent.NewSendSmsRequest() request.PhoneNumberSet = common.StringPtrs([]string{m.Phone}) request.SmsSdkAppId = common.StringPtr(get("sdk_app_id")) request.SignName = common.StringPtr(get("sign_name")) request.TemplateId = common.StringPtr(get("template_id")) request.TemplateParamSet = common.StringPtrs(m.Params) response, err := client.SendSmsWithContext(ctx, request) if err != nil { return nil, sdkError(err) } if response == nil || response.Response == nil || len(response.Response.SendStatusSet) != 1 || response.Response.SendStatusSet[0] == nil { return nil, biz.ErrSMSFailed } status := response.Response.SendStatusSet[0] if tea.StringValue(status.Code) != "Ok" { return nil, providerError(tea.StringValue(status.Code)) } return &biz.SMSResult{RequestID: tea.StringValue(response.Response.RequestId), MessageID: tea.StringValue(status.SerialNo)}, nil } var providerCodePattern = regexp.MustCompile(`^[A-Za-z][A-Za-z0-9._-]{0,127}$`) func providerError(code string) error { if !providerCodePattern.MatchString(code) { return biz.ErrSMSFailed } return kratoserrors.BadRequest("SMS_SEND_FAILED", "短信提交失败,服务商错误码:"+code) } // Return only provider codes: raw SDK errors can contain signed requests and PII. func sdkError(err error) error { var tc *tcerrors.TencentCloudSDKError if errors.As(err, &tc) { return providerError(tc.Code) } var ali *tea.SDKError if errors.As(err, &ali) { return providerError(tea.StringValue(ali.Code)) } return biz.ErrSMSFailed }