Files

129 lines
3.3 KiB
Go

package stocktwits
import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"time"
"github.com/tradarr/backend/internal/scraper"
)
const apiBase = "https://api.stocktwits.com/api/2"
type StockTwits struct {
client *http.Client
}
func New() *StockTwits {
return &StockTwits{
client: &http.Client{Timeout: 15 * time.Second},
}
}
func (s *StockTwits) Name() string { return "stocktwits" }
type apiResponse struct {
Response struct {
Status int `json:"status"`
Error string `json:"error,omitempty"`
} `json:"response"`
Messages []struct {
ID int `json:"id"`
Body string `json:"body"`
CreatedAt string `json:"created_at"`
User struct {
Username string `json:"username"`
} `json:"user"`
Entities struct {
Sentiment *struct {
Basic string `json:"basic"`
} `json:"sentiment"`
} `json:"entities"`
} `json:"messages"`
}
func (s *StockTwits) Scrape(ctx context.Context, symbols []string) ([]scraper.Article, error) {
var articles []scraper.Article
for i, symbol := range symbols {
// Délai entre les requêtes pour éviter le rate limiting
if i > 0 {
select {
case <-ctx.Done():
return articles, ctx.Err()
case <-time.After(500 * time.Millisecond):
}
}
msgs, err := s.fetchSymbol(ctx, symbol)
if err != nil {
fmt.Printf("stocktwits %s: %v\n", symbol, err)
continue
}
articles = append(articles, msgs...)
}
return articles, nil
}
func (s *StockTwits) fetchSymbol(ctx context.Context, symbol string) ([]scraper.Article, error) {
url := fmt.Sprintf("%s/streams/symbol/%s.json", apiBase, symbol)
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return nil, err
}
req.Header.Set("User-Agent", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 Chrome/120.0.0.0 Safari/537.36")
resp, err := s.client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
if resp.StatusCode == 429 {
return nil, fmt.Errorf("rate limited by StockTwits for %s", symbol)
}
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("StockTwits returned HTTP %d for %s: %s", resp.StatusCode, symbol, string(body))
}
var data apiResponse
if err := json.Unmarshal(body, &data); err != nil {
return nil, fmt.Errorf("parse response for %s: %w", symbol, err)
}
// L'API StockTwits retourne un status dans le body même en HTTP 200
if data.Response.Status != 0 && data.Response.Status != 200 {
return nil, fmt.Errorf("StockTwits API error %d for %s: %s", data.Response.Status, symbol, data.Response.Error)
}
var articles []scraper.Article
for _, msg := range data.Messages {
if msg.Body == "" {
continue
}
sentiment := ""
if msg.Entities.Sentiment != nil {
sentiment = " [" + msg.Entities.Sentiment.Basic + "]"
}
title := fmt.Sprintf("$%s — @%s%s", symbol, msg.User.Username, sentiment)
publishedAt, _ := time.Parse(time.RFC3339, msg.CreatedAt)
msgURL := fmt.Sprintf("https://stocktwits.com/%s/message/%d", msg.User.Username, msg.ID)
articles = append(articles, scraper.Article{
Title: title,
Content: msg.Body,
URL: msgURL,
PublishedAt: &publishedAt,
Symbols: []string{symbol},
})
}
fmt.Printf("stocktwits %s: %d messages fetched\n", symbol, len(articles))
return articles, nil
}