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

125 lines
2.8 KiB
Go

package threadsapi
import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"strings"
"time"
)
const graphBaseURL = "https://graph.threads.net/v1.0"
type Client struct {
accessToken string
http *http.Client
}
func NewClient(accessToken string) *Client {
return &Client{
accessToken: strings.TrimSpace(accessToken),
http: &http.Client{
Timeout: 25 * time.Second,
},
}
}
func (c *Client) Enabled() bool {
return c != nil && c.accessToken != ""
}
type SearchItem struct {
ID string `json:"id"`
Text string `json:"text"`
Permalink string `json:"permalink"`
Username string `json:"username"`
Timestamp string `json:"timestamp"`
LikeCount int `json:"like_count"`
ReplyCount int `json:"reply_count"`
}
type KeywordSearchOptions struct {
Query string
Limit int
SearchType string // TOP or RECENT
}
func (c *Client) KeywordSearch(ctx context.Context, opts KeywordSearchOptions) ([]SearchItem, error) {
if !c.Enabled() {
return nil, fmt.Errorf("threads api access token is required")
}
query := strings.TrimSpace(opts.Query)
if query == "" {
return nil, fmt.Errorf("threads keyword search query is required")
}
searchType := strings.TrimSpace(opts.SearchType)
if searchType == "" {
searchType = "TOP"
}
limit := opts.Limit
if limit <= 0 {
limit = 12
}
if limit > 50 {
limit = 50
}
params := url.Values{}
params.Set("access_token", c.accessToken)
params.Set("q", query)
params.Set("search_type", searchType)
params.Set("fields", "id,text,permalink,username,timestamp,like_count,reply_count,repost_count,quote_count")
params.Set("limit", fmt.Sprintf("%d", limit))
endpoint := graphBaseURL + "/keyword_search?" + params.Encode()
req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil)
if err != nil {
return nil, err
}
res, err := c.http.Do(req)
if err != nil {
return nil, err
}
defer res.Body.Close()
body, err := io.ReadAll(io.LimitReader(res.Body, 1<<20))
if err != nil {
return nil, err
}
if res.StatusCode != http.StatusOK {
return nil, parseAPIError(body, res.StatusCode)
}
var payload struct {
Data []SearchItem `json:"data"`
}
if err := json.Unmarshal(body, &payload); err != nil {
return nil, fmt.Errorf("parse threads keyword_search: %w", err)
}
return payload.Data, nil
}
func parseAPIError(body []byte, status int) error {
var payload struct {
Error struct {
Message string `json:"message"`
} `json:"error"`
ErrorMessage string `json:"error_message"`
}
_ = json.Unmarshal(body, &payload)
msg := strings.TrimSpace(payload.Error.Message)
if msg == "" {
msg = strings.TrimSpace(payload.ErrorMessage)
}
if msg == "" {
msg = string(body)
}
if len(msg) > 240 {
msg = msg[:240]
}
return fmt.Errorf("threads api %d: %s", status, msg)
}