package tencent import ( "context" "crypto/hmac" "crypto/sha256" "encoding/base64" "encoding/hex" "encoding/json" "fmt" "io" "net/http" "strconv" "strings" "time" "go.uber.org/zap" ) const ( host = "tms.tencentcloudapi.com" service = "tms" version = "2020-12-29" action = "TextModeration" algorithm = "TC3-HMAC-SHA256" ) type Client interface { IsEnabled() bool TextModeration(ctx context.Context, content string) (*ModerationResponse, error) } type clientImpl struct { cfg Config httpClient *http.Client } func NewClient(cfg Config) Client { timeout := cfg.Timeout if timeout <= 0 { timeout = 30 } return &clientImpl{ cfg: cfg, httpClient: &http.Client{ Timeout: time.Duration(timeout) * time.Second, }, } } func (c *clientImpl) IsEnabled() bool { return c.cfg.Enabled && c.cfg.SecretID != "" && c.cfg.SecretKey != "" } func (c *clientImpl) TextModeration(ctx context.Context, content string) (*ModerationResponse, error) { if !c.IsEnabled() { return nil, fmt.Errorf("tencent tms client is not enabled") } if strings.TrimSpace(content) == "" { return &ModerationResponse{Suggestion: SuggestionPass}, nil } encodedContent := base64.StdEncoding.EncodeToString([]byte(content)) reqBody := textModerationRequest{ Content: encodedContent, BizType: c.cfg.BizType, } payload, err := json.Marshal(reqBody) if err != nil { return nil, fmt.Errorf("marshal request: %w", err) } timestamp := time.Now().Unix() authorization, err := c.sign(string(payload), timestamp) if err != nil { return nil, fmt.Errorf("sign request: %w", err) } req, err := http.NewRequestWithContext(ctx, http.MethodPost, "https://"+host, strings.NewReader(string(payload))) if err != nil { return nil, fmt.Errorf("create request: %w", err) } req.Header.Set("Authorization", authorization) req.Header.Set("Content-Type", "application/json; charset=utf-8") req.Header.Set("Host", host) req.Header.Set("X-TC-Action", action) req.Header.Set("X-TC-Timestamp", strconv.FormatInt(timestamp, 10)) req.Header.Set("X-TC-Version", version) req.Header.Set("X-TC-Region", c.cfg.Region) resp, err := c.httpClient.Do(req) if err != nil { return nil, fmt.Errorf("request tencent tms: %w", err) } defer resp.Body.Close() body, err := io.ReadAll(resp.Body) if err != nil { return nil, fmt.Errorf("read response: %w", err) } if resp.StatusCode != http.StatusOK { return nil, fmt.Errorf("tencent tms error status=%d body=%s", resp.StatusCode, string(body)) } var apiResp tencentAPIResponse if err := json.Unmarshal(body, &apiResp); err != nil { return nil, fmt.Errorf("unmarshal response: %w", err) } if apiResp.Response.Error != nil { return nil, fmt.Errorf("tencent tms api error: code=%s message=%s", apiResp.Response.Error.Code, apiResp.Response.Error.Message) } keywords := make([]string, 0, len(apiResp.Response.Keywords)) for _, kw := range apiResp.Response.Keywords { keywords = append(keywords, kw.Word) } result := &ModerationResponse{ Suggestion: ModerationSuggestion(apiResp.Response.Suggestion), Label: apiResp.Response.Label, RiskLevel: apiResp.Response.RiskLevel, Keywords: keywords, RequestID: apiResp.Response.RequestID, } zap.L().Debug("Tencent TMS moderation result", zap.String("suggestion", string(result.Suggestion)), zap.String("label", result.Label), zap.String("risk_level", result.RiskLevel), zap.Strings("keywords", result.Keywords), zap.String("request_id", result.RequestID), ) return result, nil } func (c *clientImpl) sign(payload string, timestamp int64) (string, error) { date := time.Unix(timestamp, 0).UTC().Format("2006-01-02") canonicalHeaders := "content-type:application/json; charset=utf-8\n" + "host:" + host + "\n" signedHeaders := "content-type;host" hashedPayload := sha256hex(payload) canonicalRequest := fmt.Sprintf("%s\n%s\n%s\n%s\n%s\n%s", "POST", "/", "", canonicalHeaders, signedHeaders, hashedPayload, ) credentialScope := fmt.Sprintf("%s/%s/tc3_request", date, service) hashedCanonicalRequest := sha256hex(canonicalRequest) string2sign := fmt.Sprintf("%s\n%d\n%s\n%s", algorithm, timestamp, credentialScope, hashedCanonicalRequest, ) secretDate := hmacsha256(date, []byte("TC3"+c.cfg.SecretKey)) secretService := hmacsha256(service, secretDate) secretSigning := hmacsha256("tc3_request", secretService) signature := hex.EncodeToString(hmacsha256(string2sign, secretSigning)) authorization := fmt.Sprintf("%s Credential=%s/%s, SignedHeaders=%s, Signature=%s", algorithm, c.cfg.SecretID, credentialScope, signedHeaders, signature, ) return authorization, nil } func sha256hex(s string) string { b := sha256.Sum256([]byte(s)) return hex.EncodeToString(b[:]) } func hmacsha256(s string, key []byte) []byte { h := hmac.New(sha256.New, key) h.Write([]byte(s)) return h.Sum(nil) }