264 lines
7.3 KiB
Go
264 lines
7.3 KiB
Go
package usecase
|
||
|
||
import (
|
||
"context"
|
||
"encoding/json"
|
||
"fmt"
|
||
"strings"
|
||
|
||
app "haixun-backend/internal/library/errors"
|
||
"haixun-backend/internal/library/errors/code"
|
||
libthreads "haixun-backend/internal/library/threadsapi"
|
||
domusecase "haixun-backend/internal/model/threads_account/domain/usecase"
|
||
)
|
||
|
||
func (u *threadsAccountUseCase) RunAPIPlayground(
|
||
ctx context.Context,
|
||
tenantID, ownerUID, accountID string,
|
||
req domusecase.APIPlaygroundRequest,
|
||
) (*domusecase.APIPlaygroundResult, error) {
|
||
account, err := u.assertOwned(ctx, tenantID, ownerUID, accountID)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
token, err := u.ResolveAccountAPIAccessToken(ctx, tenantID, ownerUID, accountID)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
client := libthreads.NewClient(token)
|
||
if !client.Enabled() {
|
||
return nil, app.For(code.ThreadsAccount).InputMissingRequired("Threads API 未連線")
|
||
}
|
||
|
||
action := strings.TrimSpace(strings.ToLower(req.Action))
|
||
if action == "" {
|
||
return nil, app.For(code.ThreadsAccount).InputMissingRequired("action 為必填")
|
||
}
|
||
|
||
userID := strings.TrimSpace(account.ThreadsUserID)
|
||
if userID == "" {
|
||
if profile, perr := libthreads.GetMeProfile(ctx, token); perr == nil && profile != nil {
|
||
userID = profile.ID.String()
|
||
}
|
||
}
|
||
|
||
limit := req.Limit
|
||
if limit <= 0 {
|
||
limit = 10
|
||
}
|
||
if limit > 50 {
|
||
limit = 50
|
||
}
|
||
|
||
result := &domusecase.APIPlaygroundResult{Action: action}
|
||
var payload any
|
||
var runErr error
|
||
|
||
switch action {
|
||
case "me":
|
||
payload, runErr = libthreads.GetMeProfile(ctx, token)
|
||
case "user_threads":
|
||
if userID == "" {
|
||
runErr = fmt.Errorf("缺少 threads_user_id")
|
||
} else {
|
||
payload, runErr = client.UserThreads(ctx, userID, limit)
|
||
}
|
||
case "keyword_search":
|
||
q := strings.TrimSpace(req.Query)
|
||
if q == "" {
|
||
runErr = fmt.Errorf("query 為必填")
|
||
} else {
|
||
searchType := strings.TrimSpace(req.SearchType)
|
||
if searchType == "" {
|
||
searchType = "TOP"
|
||
}
|
||
payload, runErr = client.KeywordSearch(ctx, libthreads.KeywordSearchOptions{
|
||
Query: q, Limit: limit, SearchType: searchType,
|
||
})
|
||
}
|
||
case "profile_lookup":
|
||
u := strings.TrimPrefix(strings.TrimSpace(req.Username), "@")
|
||
if u == "" {
|
||
runErr = fmt.Errorf("username 為必填")
|
||
} else {
|
||
payload, runErr = client.ProfileLookup(ctx, u)
|
||
}
|
||
case "mentions":
|
||
if userID == "" {
|
||
runErr = fmt.Errorf("缺少 threads_user_id")
|
||
} else {
|
||
payload, runErr = client.UserMentions(ctx, userID, limit)
|
||
}
|
||
case "insights":
|
||
if userID == "" {
|
||
runErr = fmt.Errorf("缺少 threads_user_id")
|
||
} else {
|
||
payload, runErr = client.UserInsights(ctx, userID, nil)
|
||
}
|
||
case "location_search":
|
||
q := strings.TrimSpace(req.LocationQuery)
|
||
if q == "" {
|
||
q = strings.TrimSpace(req.Query)
|
||
}
|
||
if q == "" {
|
||
runErr = fmt.Errorf("location 查詢字串為必填")
|
||
} else {
|
||
payload, runErr = client.LocationSearch(ctx, q, limit)
|
||
}
|
||
case "media_replies", "media_conversation":
|
||
mid := strings.TrimSpace(req.MediaID)
|
||
if mid == "" {
|
||
runErr = fmt.Errorf("media_id 為必填")
|
||
} else {
|
||
if limit < 30 {
|
||
limit = 30
|
||
}
|
||
payload, runErr = client.MediaConversation(ctx, mid, limit)
|
||
}
|
||
case "media_stats":
|
||
mid := strings.TrimSpace(req.MediaID)
|
||
if mid == "" {
|
||
runErr = fmt.Errorf("media_id 為必填")
|
||
} else {
|
||
payload, runErr = client.GetMediaStats(ctx, mid)
|
||
}
|
||
case "media_detail":
|
||
mid := strings.TrimSpace(req.MediaID)
|
||
if mid == "" {
|
||
runErr = fmt.Errorf("media_id 為必填")
|
||
} else {
|
||
payload, runErr = client.GetMediaDetail(ctx, mid)
|
||
}
|
||
case "media_insights":
|
||
mid := strings.TrimSpace(req.MediaID)
|
||
if mid == "" {
|
||
runErr = fmt.Errorf("media_id 為必填")
|
||
} else {
|
||
payload, runErr = client.MediaInsights(ctx, mid, nil)
|
||
}
|
||
case "publish_text":
|
||
text := strings.TrimSpace(req.Text)
|
||
if userID == "" || text == "" {
|
||
runErr = fmt.Errorf("threads_user_id 與 text 為必填")
|
||
} else {
|
||
payload, runErr = libthreads.PublishText(ctx, libthreads.PublishTextInput{
|
||
ThreadsUserID: userID, AccessToken: token, Text: text,
|
||
})
|
||
}
|
||
case "prime_reply_target":
|
||
replyTo := strings.TrimSpace(req.ReplyToID)
|
||
permalink := strings.TrimSpace(req.Permalink)
|
||
if replyTo == "" || permalink == "" {
|
||
runErr = fmt.Errorf("reply_to_id 與 permalink 為必填")
|
||
} else {
|
||
hint := strings.TrimSpace(req.HintText)
|
||
if hint == "" {
|
||
hint = strings.TrimSpace(req.Query)
|
||
}
|
||
if primeErr := client.PrimeReplyTargetForPublish(ctx, libthreads.ResolveReplyMediaInput{
|
||
ExternalID: replyTo,
|
||
Permalink: permalink,
|
||
HintText: hint,
|
||
}, replyTo); primeErr != nil {
|
||
runErr = primeErr
|
||
} else {
|
||
payload = map[string]string{"primed": replyTo}
|
||
}
|
||
}
|
||
case "publish_reply":
|
||
text := strings.TrimSpace(req.Text)
|
||
replyTo := strings.TrimSpace(req.ReplyToID)
|
||
if userID == "" || text == "" || replyTo == "" {
|
||
runErr = fmt.Errorf("threads_user_id、reply_to_id、text 為必填")
|
||
} else {
|
||
permalink := strings.TrimSpace(req.Permalink)
|
||
if permalink != "" && !req.SkipPrime {
|
||
hint := strings.TrimSpace(req.HintText)
|
||
if hint == "" {
|
||
hint = strings.TrimSpace(req.Query)
|
||
}
|
||
if primeErr := client.PrimeReplyTargetForPublish(ctx, libthreads.ResolveReplyMediaInput{
|
||
ExternalID: replyTo,
|
||
Permalink: permalink,
|
||
HintText: hint,
|
||
}, replyTo); primeErr != nil {
|
||
runErr = fmt.Errorf("回覆他人貼文前需先透過 keyword_search 註冊目標:%w", primeErr)
|
||
}
|
||
}
|
||
if runErr == nil {
|
||
payload, runErr = libthreads.PublishReply(ctx, libthreads.PublishReplyInput{
|
||
ThreadsUserID: userID, AccessToken: token, ReplyToID: replyTo, Text: text,
|
||
})
|
||
}
|
||
}
|
||
case "delete_media":
|
||
mid := strings.TrimSpace(req.MediaID)
|
||
if mid == "" {
|
||
runErr = fmt.Errorf("media_id 為必填")
|
||
} else {
|
||
runErr = client.DeleteMedia(ctx, mid)
|
||
if runErr == nil {
|
||
payload = map[string]string{"deleted": mid}
|
||
}
|
||
}
|
||
case "hide_reply":
|
||
rid := strings.TrimSpace(req.ReplyID)
|
||
if rid == "" {
|
||
runErr = fmt.Errorf("reply_id 為必填")
|
||
} else {
|
||
runErr = client.HideReply(ctx, rid, req.Hide)
|
||
if runErr == nil {
|
||
payload = map[string]any{"reply_id": rid, "hide": req.Hide}
|
||
}
|
||
}
|
||
case "share_to_instagram":
|
||
mid := strings.TrimSpace(req.MediaID)
|
||
if mid == "" {
|
||
runErr = fmt.Errorf("media_id 為必填")
|
||
} else {
|
||
runErr = client.ShareToInstagram(ctx, mid)
|
||
if runErr == nil {
|
||
payload = map[string]string{"crossposted": mid}
|
||
}
|
||
}
|
||
case "refresh_token":
|
||
refreshed, rerr := libthreads.RefreshAccessToken(ctx, token)
|
||
if rerr != nil {
|
||
runErr = rerr
|
||
} else {
|
||
expiresAt := int64(0)
|
||
if refreshed.ExpiresIn >= 300 {
|
||
expiresAt = libthreads.NsFromNow(refreshed.ExpiresIn)
|
||
}
|
||
if _, serr := u.secretsRepo.SaveAPIAccessToken(ctx, account.ID, refreshed.AccessToken, expiresAt); serr != nil {
|
||
runErr = serr
|
||
} else {
|
||
payload = map[string]any{
|
||
"expires_in": refreshed.ExpiresIn,
|
||
"saved": true,
|
||
}
|
||
}
|
||
}
|
||
default:
|
||
return nil, app.For(code.ThreadsAccount).InputInvalidFormat("不支援的 action: " + action)
|
||
}
|
||
|
||
if runErr != nil {
|
||
result.Ok = false
|
||
result.Message = runErr.Error()
|
||
if libthreads.IsPermissionError(runErr) {
|
||
result.Message += "(請確認 Meta App 已授權此 scope,且帳號在測試人員名單)"
|
||
}
|
||
return result, nil
|
||
}
|
||
|
||
raw, err := json.Marshal(payload)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
result.Ok = true
|
||
result.Message = "ok"
|
||
result.Data = raw
|
||
return result, nil
|
||
}
|