Files
backend/internal/pkg/tencent/client.go

194 lines
4.8 KiB
Go
Raw Normal View History

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)
}