134 lines
3.2 KiB
Go
134 lines
3.2 KiB
Go
|
|
package placement
|
||
|
|
|
||
|
|
import (
|
||
|
|
"context"
|
||
|
|
"strings"
|
||
|
|
|
||
|
|
libthreads "haixun-backend/internal/library/threadsapi"
|
||
|
|
)
|
||
|
|
|
||
|
|
const replyFetchTopPosts = 10
|
||
|
|
|
||
|
|
// ReplyCandidate is a normalized reply attached to a scan post.
|
||
|
|
type ReplyCandidate struct {
|
||
|
|
ExternalID string
|
||
|
|
Author string
|
||
|
|
Text string
|
||
|
|
Permalink string
|
||
|
|
LikeCount int
|
||
|
|
PostedAt string
|
||
|
|
}
|
||
|
|
|
||
|
|
type ScrapeRepliesInput struct {
|
||
|
|
Posts []ScanCandidate
|
||
|
|
Member MemberContext
|
||
|
|
RepliesPerPost int
|
||
|
|
MaxPosts int
|
||
|
|
}
|
||
|
|
|
||
|
|
// AttachReplies fetches replies for top-priority posts when scrape is enabled.
|
||
|
|
func AttachReplies(ctx context.Context, input ScrapeRepliesInput) []ScanCandidate {
|
||
|
|
if len(input.Posts) == 0 {
|
||
|
|
return input.Posts
|
||
|
|
}
|
||
|
|
perPost := input.RepliesPerPost
|
||
|
|
if perPost <= 0 {
|
||
|
|
perPost = 10
|
||
|
|
}
|
||
|
|
if perPost > 25 {
|
||
|
|
perPost = 25
|
||
|
|
}
|
||
|
|
maxPosts := input.MaxPosts
|
||
|
|
if maxPosts <= 0 {
|
||
|
|
maxPosts = replyFetchTopPosts
|
||
|
|
}
|
||
|
|
|
||
|
|
targets := pickReplyTargets(input.Posts, maxPosts)
|
||
|
|
if len(targets) == 0 {
|
||
|
|
return input.Posts
|
||
|
|
}
|
||
|
|
|
||
|
|
client := libthreads.NewClient(input.Member.ThreadsAPIAccessToken)
|
||
|
|
byKey := make(map[string][]ReplyCandidate, len(targets))
|
||
|
|
for _, target := range targets {
|
||
|
|
externalID := strings.TrimSpace(target.ExternalID)
|
||
|
|
if externalID == "" {
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
var replies []ReplyCandidate
|
||
|
|
if input.Member.ApiConnected && input.Member.ThreadsAPIAccessToken != "" {
|
||
|
|
items, err := client.MediaReplies(ctx, externalID, perPost)
|
||
|
|
if err == nil {
|
||
|
|
for _, item := range items {
|
||
|
|
text := strings.TrimSpace(item.Text)
|
||
|
|
if text == "" {
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
replies = append(replies, ReplyCandidate{
|
||
|
|
ExternalID: strings.TrimSpace(item.ID),
|
||
|
|
Author: strings.TrimSpace(item.Username),
|
||
|
|
Text: text,
|
||
|
|
Permalink: strings.TrimSpace(item.Permalink),
|
||
|
|
LikeCount: item.LikeCount,
|
||
|
|
PostedAt: strings.TrimSpace(item.Timestamp),
|
||
|
|
})
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
key := candidateKey(target)
|
||
|
|
byKey[key] = replies
|
||
|
|
}
|
||
|
|
|
||
|
|
out := make([]ScanCandidate, 0, len(input.Posts))
|
||
|
|
for _, post := range input.Posts {
|
||
|
|
key := candidateKey(post)
|
||
|
|
if replies, ok := byKey[key]; ok && len(replies) > 0 {
|
||
|
|
post.Replies = replies
|
||
|
|
}
|
||
|
|
out = append(out, post)
|
||
|
|
}
|
||
|
|
return out
|
||
|
|
}
|
||
|
|
|
||
|
|
func pickReplyTargets(posts []ScanCandidate, maxPosts int) []ScanCandidate {
|
||
|
|
ranked := append([]ScanCandidate(nil), posts...)
|
||
|
|
for i := 0; i < len(ranked); i++ {
|
||
|
|
for j := i + 1; j < len(ranked); j++ {
|
||
|
|
if replyTargetRank(ranked[j]) > replyTargetRank(ranked[i]) {
|
||
|
|
ranked[i], ranked[j] = ranked[j], ranked[i]
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
out := make([]ScanCandidate, 0, maxPosts)
|
||
|
|
for _, post := range ranked {
|
||
|
|
if strings.TrimSpace(post.ExternalID) == "" && strings.TrimSpace(post.Permalink) == "" {
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
out = append(out, post)
|
||
|
|
if len(out) >= maxPosts {
|
||
|
|
break
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return out
|
||
|
|
}
|
||
|
|
|
||
|
|
func replyTargetRank(post ScanCandidate) int {
|
||
|
|
switch post.Priority {
|
||
|
|
case "gold":
|
||
|
|
return 300 + post.PlacementScore
|
||
|
|
case "recent":
|
||
|
|
return 200 + post.PlacementScore
|
||
|
|
case "relevant":
|
||
|
|
return 100 + post.PlacementScore
|
||
|
|
default:
|
||
|
|
return post.PlacementScore
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func candidateKey(post ScanCandidate) string {
|
||
|
|
if id := strings.TrimSpace(post.ExternalID); id != "" {
|
||
|
|
return "id:" + id
|
||
|
|
}
|
||
|
|
return "url:" + strings.TrimSpace(post.Permalink)
|
||
|
|
}
|