Something went wrong. Try again.
An ATProto Powered Reputation Network
Something went wrong. Try again.
4.0 kB · 116 lines
Go
at master
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117package microcosm
import ( "context" "encoding/json" "errors" "fmt" "io" "net/http" "net/url" "strings" "time"
"provenance/internal/atproto" "provenance/internal/domain" "provenance/internal/upstream")
const ( DefaultJetstreamURL = "wss://jetstream.fire.hose.cam/subscribe" DefaultSlingshotURL = "https://slingshot.microcosm.blue" ReplayOverlap = 5 * time.Second StreamReadTimeout = 2 * time.Minute MaxEventBytes = 1 << 20 UserAgent = upstream.UserAgent)
type Client struct { streamURL string slingshotURL string http *http.Client softMode domain.SoftMode}
func New(streamURL, slingshotURL string) (*Client, error) { return NewWithSoft(streamURL, slingshotURL, domain.SoftDisabled)}
func NewWithSoft(streamURL, slingshotURL string, mode domain.SoftMode) (*Client, error) { return NewWithQueue(streamURL, slingshotURL, mode, upstream.Default())}
func NewWithQueue(streamURL, slingshotURL string, mode domain.SoftMode, queue *upstream.Queue) (*Client, error) { if mode < domain.SoftDisabled || mode > domain.SoftAllAuthorLists { return nil, errors.New("invalid soft vouch mode") } if err := validateURL(streamURL, "wss", "ws"); err != nil { return nil, err } if err := validateURL(slingshotURL, "https", "http"); err != nil { return nil, err } u, _ := url.Parse(slingshotURL) if u.Path != "" && u.Path != "/" { return nil, errors.New("Slingshot URL must contain only an origin") } // Endpoint configuration is operator-controlled, so explicit private self-hosted instances are valid. transport := http.DefaultTransport.(*http.Transport).Clone() client := &http.Client{Transport: queue.Transport(transport), Timeout: atproto.RequestTimeout, CheckRedirect: func(*http.Request, []*http.Request) error { return http.ErrUseLastResponse }} return &Client{streamURL: streamURL, slingshotURL: strings.TrimRight(slingshotURL, "/"), http: client, softMode: mode}, nil}
func loopback(host string) bool { return host == "127.0.0.1" || host == "::1" || host == "localhost" }
func validateURL(raw, secure, development string) error { u, err := url.Parse(raw) if err != nil || u.Host == "" || u.User != nil || u.RawQuery != "" || u.Fragment != "" { return errors.New("invalid Microcosm endpoint URL") } if u.Scheme != secure && !(u.Scheme == development && loopback(u.Hostname())) { return errors.New("Microcosm endpoints require encrypted transport or explicit loopback development addresses") } return nil}
func (c *Client) record(ctx context.Context, did, key, cid string) (atproto.Record, error) { var record atproto.Record err := c.rawRecord(ctx, did, domain.Collection, key, cid, &record) return record, err}
func (c *Client) rawRecord(ctx context.Context, did, source, key, cid string, record any) error { params := url.Values{"repo": {did}, "collection": {source}, "rkey": {key}, "cid": {cid}} req, err := http.NewRequestWithContext(ctx, http.MethodGet, c.slingshotURL+"/xrpc/com.atproto.repo.getRecord?"+params.Encode(), nil) if err != nil { return err } req.Header.Set("User-Agent", UserAgent) resp, err := c.http.Do(req) if err != nil { return err } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return fmt.Errorf("Slingshot returned HTTP %d for event hydration", resp.StatusCode) } data, err := io.ReadAll(io.LimitReader(resp.Body, atproto.MaxResponseBytes+1)) if err != nil { return err } if len(data) > atproto.MaxResponseBytes { return errors.New("Slingshot response exceeds size limit") } var result struct { URI string `json:"uri"` CID string `json:"cid"` Value json.RawMessage `json:"value"` } if err := json.Unmarshal(data, &result); err != nil { return err } if result.URI != "at://"+did+"/"+source+"/"+key || result.CID != cid { return errors.New("Slingshot record does not match the event URI or CID") } return json.Unmarshal(result.Value, record)}