naco-api/internal/activitypub/client.go
2026-07-18 22:42:44 +00:00

154 lines
3.9 KiB
Go

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
}