package activitypub import ( "context" "encoding/json" "fmt" "io" "net/http" "time" ) type APClient struct { client *http.Client host string bearerToken string } func NewAPClient(ctx context.Context, host, apikey string) (*APClient, error) { if host == "" || apikey == "" { return nil, fmt.Errorf("host and apikey must not be empty") } return &APClient{ client: http.DefaultClient, host: host, bearerToken: fmt.Sprintf("Bearer %s", apikey), }, nil } func (ap *APClient) GetPost(ctx context.Context, postID string) (*Post, error) { url := fmt.Sprintf("%s/api/v1/statuses/%s", ap.host, postID) req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) if err != nil { return nil, fmt.Errorf("failed to create request: %w", err) } req.Header.Set("Authorization", ap.bearerToken) resp, err := ap.client.Do(req) if err != nil { return nil, fmt.Errorf("failed to get post: %w", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return nil, fmt.Errorf("failed to get post: %s", resp.Status) } body, err := io.ReadAll(resp.Body) if err != nil { return nil, fmt.Errorf("failed to read response body: %w", err) } var internal = &internalPost{} if err = json.Unmarshal(body, internal); err != nil { return nil, fmt.Errorf("failed to unmarshal json response: %w", err) } return internal.toPost(), nil } func (ap *APClient) GetLatestNPosts(ctx context.Context, actor string, limit int) ([]*Post, error) { url := fmt.Sprintf("%s/api/v1/accounts/%s/statuses?limit=%d", ap.host, actor, limit) req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) if err != nil { return nil, fmt.Errorf("failed to create request: %w", err) } req.Header.Set("Authorization", ap.bearerToken) resp, err := ap.client.Do(req) if err != nil { return nil, fmt.Errorf("failed to get latest posts: %w", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return nil, fmt.Errorf("failed to get latest posts: %s", resp.Status) } body, err := io.ReadAll(resp.Body) if err != nil { return nil, fmt.Errorf("failed to read response body: %w", err) } var internal []*internalPost if err = json.Unmarshal(body, &internal); err != nil { return nil, fmt.Errorf("failed to unmarshal json response: %w", err) } posts := make([]*Post, len(internal)) for i, p := range internal { posts[i] = p.toPost() } return posts, nil } func (ap *APClient) GetPostsInTimeframe(ctx context.Context, actor string, start, end time.Time, limit int) ([]Post, error) { var posts []Post var maxID string for { if ctx.Err() != nil { return nil, ctx.Err() } if len(posts) >= limit { break } url := fmt.Sprintf("%s/api/v1/accounts/%s/statuses?limit=50", ap.host, actor) if maxID != "" { url += fmt.Sprintf("&max_id=%s", maxID) } req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) if err != nil { return nil, fmt.Errorf("failed to create request: %w", err) } req.Header.Set("Authorization", ap.bearerToken) resp, err := ap.client.Do(req) if err != nil { return nil, fmt.Errorf("failed to get posts: %w", err) } // !!IMPORTANT!! Manually close body instead of deferring, due to loop if resp.StatusCode != http.StatusOK { resp.Body.Close() return nil, fmt.Errorf("failed to get posts: %s", resp.Status) } body, err := io.ReadAll(resp.Body) resp.Body.Close() if err != nil { return nil, fmt.Errorf("failed to read response body: %w", err) } var internal []*internalPost if err := json.Unmarshal(body, &internal); err != nil { return nil, fmt.Errorf("failed to unmarshal response body: %w", err) } if len(internal) == 0 { break } for _, i := range internal { if i.CreatedAt.After(end) { continue } if i.CreatedAt.Before(start) { return posts, nil } posts = append(posts, *i.toPost()) if len(posts) >= limit { return posts, nil } } maxID = internal[len(internal)-1].ID } return posts, nil }