thread-master/old/backend/internal/library/threadsapi/publish_media.go

155 lines
4.2 KiB
Go

package threadsapi
import (
"context"
"fmt"
"strings"
"haixun-backend/internal/library/threadspost"
)
const maxCarouselItems = 20
type publishMediaInput struct {
ThreadsUserID string
AccessToken string
Text string
TopicTag string
ReplyToID string
ImageURLs []string
}
func normalizeImageURLs(urls []string) []string {
out := make([]string, 0, len(urls))
seen := make(map[string]struct{}, len(urls))
for _, raw := range urls {
url := strings.TrimSpace(raw)
if url == "" {
continue
}
if _, ok := seen[url]; ok {
continue
}
seen[url] = struct{}{}
out = append(out, url)
if len(out) >= maxCarouselItems {
break
}
}
return out
}
func publishMedia(ctx context.Context, in publishMediaInput) (*PublishResult, error) {
userID := strings.TrimSpace(in.ThreadsUserID)
token := strings.TrimSpace(in.AccessToken)
text := strings.TrimSpace(in.Text)
replyTo := strings.TrimSpace(in.ReplyToID)
imageURLs := normalizeImageURLs(in.ImageURLs)
if userID == "" || token == "" {
return nil, fmt.Errorf("threads api credentials incomplete")
}
if replyTo == "" && text == "" && len(imageURLs) == 0 {
return nil, fmt.Errorf("post text or image is required")
}
if replyTo != "" && text == "" && len(imageURLs) == 0 {
return nil, fmt.Errorf("reply text or image is required")
}
if replyTo != "" && len(imageURLs) > 1 {
return nil, fmt.Errorf("Threads API 目前不支援多圖回覆,請只附一張圖")
}
if replyTo != "" {
if err := verifyReplyTarget(ctx, replyTo, token); err != nil {
return nil, err
}
}
if replyTo == "" {
if err := threadspost.ValidatePublishContent(text, len(imageURLs) > 0); err != nil {
return nil, err
}
} else if err := threadspost.ValidateReplyContent(text, len(imageURLs) > 0); err != nil {
return nil, err
}
var containerID string
var err error
switch len(imageURLs) {
case 0:
if replyTo != "" {
containerID, err = createReplyContainer(ctx, userID, token, replyTo, text)
} else {
containerID, err = createTextContainer(ctx, userID, token, text, in.TopicTag)
}
case 1:
if replyTo == "" {
containerID, err = createImageContainer(ctx, userID, token, text, imageURLs[0], in.TopicTag)
} else {
containerID, err = createImageReplyContainer(ctx, userID, token, replyTo, text, imageURLs[0])
}
default:
containerID, err = createCarouselContainer(ctx, userID, token, text, in.TopicTag, replyTo, imageURLs)
}
if err != nil {
return nil, err
}
waitAttempts := containerWaitAttempts(len(imageURLs))
if err := waitForContainerReady(ctx, containerID, token, waitAttempts); err != nil {
return nil, err
}
mediaID, err := publishContainer(ctx, userID, token, containerID)
if err != nil {
return nil, err
}
permalink, _ := fetchPermalink(ctx, mediaID, token)
return &PublishResult{MediaID: mediaID, Permalink: permalink}, nil
}
func containerWaitAttempts(imageCount int) int {
if imageCount == 0 {
return 12
}
if imageCount == 1 {
return 30
}
return 30 + imageCount*2
}
func createCarouselContainer(ctx context.Context, userID, token, text, topicTag, replyTo string, imageURLs []string) (string, error) {
if len(imageURLs) < 2 {
return "", fmt.Errorf("carousel requires at least two images")
}
childIDs := make([]string, 0, len(imageURLs))
for _, imageURL := range imageURLs {
childID, err := createCarouselItemContainer(ctx, userID, token, imageURL)
if err != nil {
return "", err
}
if err := waitForContainerReady(ctx, childID, token, 30); err != nil {
return "", err
}
childIDs = append(childIDs, childID)
}
params := urlValues(token)
params.Set("media_type", "CAROUSEL")
params.Set("children", strings.Join(childIDs, ","))
if text != "" {
params.Set("text", text)
}
if tag := normalizeTopicTag(topicTag); tag != "" {
params.Set("topic_tag", tag)
}
if replyTo != "" {
params.Set("reply_to_id", replyTo)
}
return postThreadsContainer(ctx, userID, params)
}
func createCarouselItemContainer(ctx context.Context, userID, token, imageURL string) (string, error) {
params := urlValues(token)
params.Set("is_carousel_item", "true")
params.Set("media_type", "IMAGE")
params.Set("image_url", imageURL)
return postThreadsContainer(ctx, userID, params)
}