155 lines
4.2 KiB
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)
|
|
} |