thread-master/backend/internal/library/threadsapi/media.go

144 lines
4.1 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 threadsapi
import (
"context"
"encoding/json"
"fmt"
"net/http"
"net/url"
"strings"
"time"
)
type MediaDetail struct {
ID string `json:"id"`
Text string `json:"text"`
Username string `json:"username"`
Permalink string `json:"permalink"`
Timestamp string `json:"timestamp"`
MediaType string `json:"media_type"`
IsReply bool `json:"is_reply"`
}
func (c *Client) GetMediaDetail(ctx context.Context, mediaID string) (*MediaDetail, 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")
}
params := url.Values{}
params.Set("access_token", c.accessToken)
params.Set("fields", "id,text,username,permalink,timestamp,media_type,is_reply")
endpoint := graphBaseURL + "/" + url.PathEscape(mediaID) + "?" + params.Encode()
req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil)
if err != nil {
return nil, err
}
body, status, err := doRequest(req)
if err != nil {
return nil, err
}
if status != http.StatusOK {
return nil, parseAPIError(body, status)
}
var out MediaDetail
if err := json.Unmarshal(body, &out); err != nil {
return nil, fmt.Errorf("parse media detail: %w", err)
}
out.ID = strings.TrimSpace(out.ID)
if out.ID == "" {
return nil, fmt.Errorf("threads media detail missing id")
}
return &out, nil
}
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
}