Files
Codex Local bf1233d5df
ci / verify (push) Canceled after 0s
feat: expand companion platform, device controls, and memoir workflows
Add customer and operator login, BLE provisioning, device fleet controls, call memory, memoir generation, and refreshed family and maintenance portals. Include API contracts, migrations, firmware sources, tests, assets, and deployment documentation.
2026-10-08 10:06:44 +08:00

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
}