thread-master/backend/internal/model/threads_account/usecase/api_playground.go

218 lines
5.9 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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":
mid := strings.TrimSpace(req.MediaID)
if mid == "" {
runErr = fmt.Errorf("media_id 為必填")
} else {
payload, runErr = client.MediaReplies(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_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 "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 {
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
}