Files
story/internal/onboarding/tencent_sms.go
T

135 lines
5.3 KiB
Go
Raw Normal View History

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
}