Files
creator-hub/internal/platform/xiaohongshu/detail.go
T

93 lines
3.0 KiB
Go

package xiaohongshu
import (
"context"
"encoding/json"
"fmt"
"git.ipao.vip/rogee/creator-hub/internal/creator"
)
// GetNoteDetail reads one note through the platform feed endpoint and keeps
// the original link so xsec context survives a later comments resume.
func (c *Collector) GetNoteDetail(ctx context.Context, originalURL string) (creator.WorkInput, error) {
if c == nil || c.Browser == nil {
return creator.WorkInput{}, fmt.Errorf("%w: invalid xiaohongshu detail request", creator.ErrInvalid)
}
link, err := c.resolveSourceURL(ctx, originalURL)
if err != nil {
return creator.WorkInput{}, err
}
if link.Kind != LinkNote {
return creator.WorkInput{}, fmt.Errorf("%w: xiaohongshu note URL is required", creator.ErrInvalid)
}
access := accessContext{Token: link.Token, Source: link.Source}
body, err := json.Marshal(map[string]any{
"source_note_id": link.WorkKey,
"image_formats": []string{"jpg", "webp", "avif"},
"extra": map[string]string{"need_body_topic": "1"},
"xsec_source": access.Source,
"xsec_token": access.Token,
})
if err != nil {
return creator.WorkInput{}, fmt.Errorf("encode xiaohongshu detail request: %w", err)
}
response, err := c.Browser.Post(ctx, APIOrigin+FeedPath, body)
if err != nil {
return creator.WorkInput{}, err
}
if err := responseError(response, "detail"); err != nil {
return creator.WorkInput{}, err
}
item, parsedAccess, ok := parseDetail(response.Body, access)
if !ok {
return creator.WorkInput{}, fmt.Errorf("%w: invalid xiaohongshu detail response", creator.ErrInvalid)
}
if item.WorkKey != link.WorkKey {
return creator.WorkInput{}, fmt.Errorf("%w: xiaohongshu detail returned another note", creator.ErrConflict)
}
item.OriginalURL = link.CanonicalURL
item.SourceType = c.SourceType
item.SourceID = c.SourceID
if item.SourceType == "" {
item.SourceType = creator.SourceCompetitor
}
if item.SourceID == "" {
item.SourceID = c.AccountKey
}
c.contexts.Store(item.WorkKey, parsedAccess)
return item, nil
}
func parseDetail(body []byte, fallback accessContext) (creator.WorkInput, accessContext, bool) {
if len(body) == 0 || len(body) > 4<<20 {
return creator.WorkInput{}, accessContext{}, false
}
var envelope struct {
Success *bool `json:"success"`
Data json.RawMessage `json:"data"`
}
if json.Unmarshal(body, &envelope) != nil || envelope.Success == nil || !*envelope.Success || len(envelope.Data) == 0 {
return creator.WorkInput{}, accessContext{}, false
}
data, ok := objectValue(envelope.Data)
if !ok {
return creator.WorkInput{}, accessContext{}, false
}
if items, ok := arrayValue(data["items"]); ok && len(items) > 0 {
return parseWork(items[0], fallback)
}
if note := data["note"]; len(note) > 0 {
return parseWork(note, fallback)
}
return parseWork(envelope.Data, fallback)
}
func arrayValue(raw json.RawMessage) ([]json.RawMessage, bool) {
var values []json.RawMessage
if len(raw) == 0 || json.Unmarshal(raw, &values) != nil || values == nil {
return nil, false
}
return values, true
}