194 lines
4.8 KiB
Go
194 lines
4.8 KiB
Go
|
|
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)
|
||
|
|
}
|