package threadsapi import ( "context" "encoding/json" "fmt" "net/http" "net/url" "strings" "time" ) type MediaStats struct { LikeCount int `json:"like_count"` ReplyCount int `json:"reply_count"` RepostCount int `json:"repost_count"` QuoteCount int `json:"quote_count"` Views int `json:"views,omitempty"` Shares int `json:"shares,omitempty"` } // GetMediaStats loads engagement counts via GET /{media-id}/insights. // Meta does not expose like_count on the media object itself (requesting those fields returns API errors). func GetMediaStats(ctx context.Context, accessToken, mediaID string) (*MediaStats, error) { return NewClient(accessToken).GetMediaStats(ctx, mediaID) } func (c *Client) GetMediaStats(ctx context.Context, mediaID string) (*MediaStats, error) { if !c.Enabled() { return nil, fmt.Errorf("threads api access token is required") } mediaID = strings.TrimSpace(mediaID) if mediaID == "" { return nil, fmt.Errorf("threads media id is required") } metrics, err := c.MediaInsights(ctx, mediaID, []string{"likes", "replies", "reposts", "quotes", "views", "shares"}) if err != nil { if IsPermissionError(err) { return nil, fmt.Errorf("%w(貼文互動數需 threads_manage_insights,請確認 OAuth 已授權)", err) } return nil, err } return mapInsightMetrics(metrics), nil } func mapInsightMetrics(metrics map[string]int) *MediaStats { if metrics == nil { metrics = map[string]int{} } return &MediaStats{ LikeCount: metrics["likes"], ReplyCount: metrics["replies"], RepostCount: metrics["reposts"], QuoteCount: metrics["quotes"], Views: metrics["views"], Shares: metrics["shares"], } } type TokenRefreshResult struct { AccessToken string `json:"access_token"` TokenType string `json:"token_type"` ExpiresIn int `json:"expires_in"` } func RefreshAccessToken(ctx context.Context, currentToken string) (*TokenRefreshResult, error) { u := fmt.Sprintf("%s/refresh_access_token", graphBaseURL) req, err := http.NewRequestWithContext(ctx, http.MethodGet, u, nil) if err != nil { return nil, err } q := url.Values{} q.Set("grant_type", "th_refresh_token") q.Set("access_token", currentToken) req.URL.RawQuery = q.Encode() resp, err := http.DefaultClient.Do(req) if err != nil { return nil, err } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return nil, fmt.Errorf("threads api token refresh error: %s", resp.Status) } var result TokenRefreshResult if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { return nil, err } if result.AccessToken == "" { return nil, fmt.Errorf("threads api token refresh returned empty token") } return &result, nil } func NsFromNow(seconds int) int64 { return time.Now().UnixNano() + int64(seconds)*1e9 }