Files
creator-hub/cmd/control-plane/xiaohongshu.go
T

193 lines
7.1 KiB
Go

package main
import (
"context"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"net/http"
"net/url"
"strings"
"time"
"git.ipao.vip/rogee/creator-hub/internal/creator"
"git.ipao.vip/rogee/creator-hub/internal/douyin"
"git.ipao.vip/rogee/creator-hub/internal/hub"
"git.ipao.vip/rogee/creator-hub/internal/xiaohongshu"
)
type xiaohongshuGatewayBrowser struct {
gateway hub.Gateway
environment hub.EnvironmentContext
}
func (browser xiaohongshuGatewayBrowser) generation() (map[string]any, error) {
request, err := (douyinGatewayBrowser{gateway: browser.gateway, environment: browser.environment}).request()
if err != nil {
return nil, err
}
return map[string]any{
"binding_version": request.BindingVersion,
"runtime_id": request.RuntimeID,
"network_id": request.NetworkID,
"network_exit_id": request.NetworkExitID,
}, nil
}
func (browser xiaohongshuGatewayBrowser) Get(ctx context.Context, target string) (xiaohongshu.Response, error) {
request, err := browser.generation()
if err != nil {
return xiaohongshu.Response{}, err
}
request["url"] = target
status, body, err := gatewayCall(ctx, browser.gateway, http.MethodPost,
"/v1/browsers/"+url.PathEscape(browser.environment.Alias)+"/xiaohongshu/get", request, 30*time.Second)
if err != nil || status != http.StatusOK {
return xiaohongshu.Response{}, errors.New("restricted Xiaohongshu browser operation failed")
}
return decodeXiaohongshuResponse(body)
}
func (browser xiaohongshuGatewayBrowser) Post(ctx context.Context, target string, payload []byte) (xiaohongshu.Response, error) {
if len(payload) == 0 || len(payload) > 4<<20 {
return xiaohongshu.Response{}, errors.New("invalid Xiaohongshu browser body")
}
var bodyValue any
if err := json.Unmarshal(payload, &bodyValue); err != nil {
return xiaohongshu.Response{}, fmt.Errorf("invalid Xiaohongshu browser body: %w", err)
}
if _, ok := bodyValue.(map[string]any); !ok {
return xiaohongshu.Response{}, errors.New("Xiaohongshu browser body must be an object")
}
request, err := browser.generation()
if err != nil {
return xiaohongshu.Response{}, err
}
request["url"] = target
request["body"] = bodyValue
status, responseBody, err := gatewayCall(ctx, browser.gateway, http.MethodPost,
"/v1/browsers/"+url.PathEscape(browser.environment.Alias)+"/xiaohongshu/post", request, 30*time.Second)
if err != nil || status != http.StatusOK {
return xiaohongshu.Response{}, errors.New("restricted Xiaohongshu browser POST failed")
}
return decodeXiaohongshuResponse(responseBody)
}
func (browser xiaohongshuGatewayBrowser) Identity(ctx context.Context, expectedKey string) (string, error) {
request, err := browser.generation()
if err != nil {
return "", err
}
request["expected_account_key"] = expectedKey
status, body, err := gatewayCall(ctx, browser.gateway, http.MethodPost,
"/v1/browsers/"+url.PathEscape(browser.environment.Alias)+"/xiaohongshu/identity", request, 30*time.Second)
if err != nil || status != http.StatusOK {
return "", errors.New("Xiaohongshu identity verification failed")
}
var identity struct {
UID string `json:"uid"`
}
if err := json.Unmarshal(body, &identity); err != nil || strings.TrimSpace(identity.UID) == "" {
return "", errors.New("Xiaohongshu identity response omitted uid")
}
return identity.UID, nil
}
func (browser xiaohongshuGatewayBrowser) Media(ctx context.Context, target string) ([]byte, string, error) {
request, err := browser.generation()
if err != nil {
return nil, "", err
}
request["url"] = target
status, body, err := gatewayCall(ctx, browser.gateway, http.MethodPost,
"/v1/browsers/"+url.PathEscape(browser.environment.Alias)+"/xiaohongshu/media", request, 90*time.Second)
if err != nil || status != http.StatusOK {
return nil, "", errors.New("restricted Xiaohongshu media request failed")
}
var response struct {
Status int `json:"status"`
ContentType string `json:"content_type"`
BodyBase64 string `json:"body_base64"`
}
if err := json.Unmarshal(body, &response); err != nil || response.Status < 200 || response.Status >= 300 || response.BodyBase64 == "" {
return nil, "", errors.New("Xiaohongshu media response is invalid")
}
data, err := decodeBase64(response.BodyBase64)
if err != nil {
return nil, "", err
}
return data, response.ContentType, nil
}
func decodeXiaohongshuResponse(body []byte) (xiaohongshu.Response, error) {
var response struct {
Status int `json:"status"`
Body string `json:"body"`
Challenge string `json:"challenge"`
}
if err := json.Unmarshal(body, &response); err != nil || response.Status < 100 || response.Status > 599 {
return xiaohongshu.Response{}, errors.New("restricted Xiaohongshu browser returned an invalid response")
}
return xiaohongshu.Response{Status: response.Status, Body: []byte(response.Body), Challenge: response.Challenge}, nil
}
func newCreatorCollector(ctx context.Context, platform string, gateway hub.Gateway, environment hub.EnvironmentContext, accountKey, sourceType, sourceID string) (creator.PlatformCollector, string, error) {
switch platform {
case creator.PlatformDouyin:
browser := creatorGatewayBrowser{gateway: gateway, environment: environment}
uid, err := browser.Identity(ctx, accountKey)
if err != nil {
return nil, "", err
}
collector := douyinCollector(browser, accountKey, sourceType, sourceID)
canonical, err := collector.CanonicalSecUID(ctx, uid)
if err != nil {
return nil, "", err
}
collector.AccountKey = canonical
return &collector, canonical, nil
case creator.PlatformXiaohongshu:
browser := xiaohongshuGatewayBrowser{gateway: gateway, environment: environment}
uid, err := browser.Identity(ctx, accountKey)
if err != nil {
return nil, "", err
}
return &xiaohongshu.Collector{Browser: browser, AccountKey: uid, SourceType: sourceType, SourceID: sourceID}, uid, nil
default:
return nil, "", fmt.Errorf("%w: unsupported creator platform %s", creator.ErrUnavailable, platform)
}
}
func douyinCollector(browser creatorGatewayBrowser, accountKey, sourceType, sourceID string) douyin.CreatorCollector {
return douyin.CreatorCollector{Browser: browser, AccountKey: accountKey, SourceType: sourceType, SourceID: sourceID}
}
func verifyCreatorPlatformIdentity(ctx context.Context, platform string, gateway hub.Gateway, environment hub.EnvironmentContext, expectedKey string) (string, error) {
switch platform {
case creator.PlatformDouyin:
return (creatorGatewayBrowser{gateway: gateway, environment: environment}).Identity(ctx, expectedKey)
case creator.PlatformXiaohongshu:
return (xiaohongshuGatewayBrowser{gateway: gateway, environment: environment}).Identity(ctx, expectedKey)
default:
return "", fmt.Errorf("%w: unsupported creator platform %s", creator.ErrUnavailable, platform)
}
}
func decodeBase64(value string) ([]byte, error) {
const maxEncoded = 96 << 20
if len(value) > maxEncoded {
return nil, errors.New("media response is too large")
}
data, err := base64.StdEncoding.DecodeString(value)
if err != nil {
return nil, fmt.Errorf("decode media response: %w", err)
}
if len(data) > maxCreatorMediaBytes {
return nil, errors.New("media response is too large")
}
return data, nil
}
var _ xiaohongshu.Browser = xiaohongshuGatewayBrowser{}