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) }