135 lines
5.3 KiB
Go
135 lines
5.3 KiB
Go
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
|
||
|
|
}
|