93 lines
3.0 KiB
Go
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
|
|
}
|