package onboarding import ( "context" "errors" "regexp" "strconv" "strings" "time" "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common" "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/profile" sms "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/sms/v20210111" ) // Tencent SMS accepts a request before the carrier delivers it. A failed or // ambiguous call is never retried here: the challenge remains subject to the // existing resend limits and cannot cause an accidental duplicate charge. type tencentSMSSender interface { SendSmsWithContext(context.Context, *sms.SendSmsRequest) (*sms.SendSmsResponse, error) } type TencentSMSProvider struct { parameters map[string][]string client tencentSMSSender appID, signName string templates map[string]string } var mainlandPhonePattern = regexp.MustCompile(`^\+86[1][3-9][0-9]{9}$`) var smsTemplateIDPattern = regexp.MustCompile(`^[0-9]+$`) const ( SMSPurposeLogin = "LOGIN" SMSPurposeRegister = "REGISTER" SMSPurposeChangePassword = "CHANGE_PASSWORD" SMSPurposeChangePhone = "CHANGE_PHONE" ) var smsTemplateKeys = map[string]string{ SMSPurposeLogin: "login", SMSPurposeRegister: "register", SMSPurposeChangePassword: "change_password", SMSPurposeChangePhone: "change_phone", } func NewTencentSMSProvider(secretID, secretKey, appID, signName, templateID string) (SMSProvider, error) { // Keep the original single-template environment configuration compatible. return NewTencentSMSProviderWithTemplates(secretID, secretKey, appID, signName, map[string]string{ "login": templateID, "register": templateID, "change_password": templateID, "change_phone": templateID, }) } func NewTencentSMSProviderWithTemplates(secretID, secretKey, appID, signName string, templates map[string]string, parameterOptions ...map[string][]string) (SMSProvider, error) { if strings.TrimSpace(secretID) == "" || strings.TrimSpace(secretKey) == "" || !smsTemplateIDPattern.MatchString(appID) || strings.TrimSpace(signName) == "" { return nil, errors.New("Tencent SMS requires SecretId, SecretKey, numeric SDK AppID and approved sign name") } copyTemplates := make(map[string]string, len(smsTemplateKeys)) parameters := make(map[string][]string, len(smsTemplateKeys)) for _, key := range smsTemplateKeys { if !smsTemplateIDPattern.MatchString(templates[key]) { return nil, errors.New("Tencent SMS requires numeric login, register, change_password and change_phone template IDs") } copyTemplates[key] = templates[key] parameters[key] = []string{"code"} } if len(parameterOptions) > 1 { return nil, errors.New("Tencent SMS accepts one template parameter configuration") } if len(parameterOptions) == 1 { for key, values := range parameterOptions[0] { if _, ok := copyTemplates[key]; !ok { return nil, errors.New("Tencent SMS template parameters have an unknown purpose") } seen := map[string]bool{} for _, value := range values { if (value != "code" && value != "expires_minutes") || seen[value] { return nil, errors.New("Tencent SMS template parameters must be code and optionally expires_minutes") } seen[value] = true } if !seen["code"] { return nil, errors.New("Tencent SMS template parameters must include code") } parameters[key] = append([]string(nil), values...) } } clientProfile := profile.NewClientProfile() clientProfile.HttpProfile.Endpoint = "sms.tencentcloudapi.com" clientProfile.HttpProfile.ReqTimeout = 10 clientProfile.NetworkFailureMaxRetries = 0 clientProfile.RateLimitExceededMaxRetries = 0 clientProfile.UnsafeRetryOnConnectionFailure = false client, err := sms.NewClient(common.NewCredential(secretID, secretKey), "ap-guangzhou", clientProfile) if err != nil { return nil, err } return &TencentSMSProvider{client: client, appID: appID, signName: signName, templates: copyTemplates, parameters: parameters}, nil } func (p *TencentSMSProvider) Send(ctx context.Context, phone, code, id string) error { return p.SendForPurpose(ctx, phone, code, id, SMSPurposeLogin) } func (p *TencentSMSProvider) SendForPurpose(ctx context.Context, phone, code, id, purpose string) error { templateID := p.templates[smsTemplateKeys[purpose]] if templateID == "" || !mainlandPhonePattern.MatchString(phone) || !smsCodePattern.MatchString(code) || !uuidV4(id) { return errors.New("SMS_GATEWAY_REJECTED") } req := sms.NewSendSmsRequest() req.PhoneNumberSet = common.StringPtrs([]string{phone}) req.SmsSdkAppId = common.StringPtr(p.appID) req.SignName = common.StringPtr(p.signName) req.TemplateId = common.StringPtr(templateID) values := make([]string, 0, len(p.parameters[smsTemplateKeys[purpose]])) for _, name := range p.parameters[smsTemplateKeys[purpose]] { if name == "code" { values = append(values, code) } else { values = append(values, strconv.Itoa(int(smsCodeLifetime/time.Minute))) } } req.TemplateParamSet = common.StringPtrs(values) req.SessionContext = common.StringPtr(id) result, err := p.client.SendSmsWithContext(ctx, req) if err != nil { return errors.New("SMS_GATEWAY_UNCERTAIN") } if result == nil || result.Response == nil || len(result.Response.SendStatusSet) != 1 || result.Response.SendStatusSet[0] == nil || result.Response.SendStatusSet[0].Code == nil || *result.Response.SendStatusSet[0].Code != "Ok" { return errors.New("SMS_GATEWAY_REJECTED") } return nil }