package fa import ( "log/slog" "net/http" "time" ) // Cookies are FA's session cookies as they appear in a browser's storage. // Both A and B are required for authenticated requests; either may be empty // for unauthenticated browsing of public pages. type Cookies struct { A string B string } // CFCookies carries the Cloudflare clearance cookie obtained from a real // browser. Cloudflare binds Clearance to the exact User-Agent string that // produced it, so callers must also pass [WithUserAgent] with that UA. type CFCookies struct { Clearance string } // SFWMode controls whether FA serves the "safe for work" filtered view // that hides mature and adult submission thumbnails. The site exposes this // as a slider in the navbar; under the hood it writes a single `sfw` // cookie that the page-render code reads server-side. // // Note: SFW mode does not change which submissions appear in listings — // adult items still show up, but their thumbnails are replaced with a // "blocked content" placeholder and the file URL is hidden. To filter // listings by rating, use the browse/search rating filters (M4). type SFWMode int const ( // SFWAuto leaves the cookie alone. FA falls back to the account's // saved preference (or, for anonymous clients, defaults to NSFW // visible). Use this when you don't want the SDK to override what the // user clicked in the browser. SFWAuto SFWMode = iota // SFWOn sets `sfw=1` mature and adult content is blocked from // rendering on all pages this client fetches. SFWOn // SFWOff sets `sfw=0` mature and adult content renders fully. This // is FA's default for logged-in adult-verified accounts. SFWOff ) // Option configures a [Client] at construction time. type Option func(*config) // config is the internal, fully-resolved client configuration. Defaults are // applied in New before options run. type config struct { cookies Cookies cf CFCookies sfw SFWMode userAgent string rateInterval time.Duration rateBurst int logger *slog.Logger httpClient *http.Client maxRetries int jsonListings bool priorityRL bool } // defaultUserAgent identifies this SDK. Callers should override it with // WithUserAgent most importantly so it matches the UA that produced their // cf_clearance cookie. const defaultUserAgent = "go-fa-api/0.1 (+https://git.anthrove.art/public/go-fa-api)" // WithCookies sets the FA session cookies (a, b). Without these, requests to // authenticated endpoints will surface as [ErrUnauthorized]. func WithCookies(c Cookies) Option { return func(cfg *config) { cfg.cookies = c } } // WithCloudflare sets the cf_clearance cookie used to satisfy Cloudflare's // challenges. Must be paired with [WithUserAgent] using the same UA the // cookie was issued under. func WithCloudflare(c CFCookies) Option { return func(cfg *config) { cfg.cf = c } } // WithSFW overrides the account's saved SFW preference for this Client. // Pass [SFWOn] to force the SFW filter on, [SFWOff] to force it off, or // [SFWAuto] (the default) to leave it unset so the account or anonymous // default takes effect. func WithSFW(mode SFWMode) Option { return func(cfg *config) { cfg.sfw = mode } } // WithUserAgent overrides the default User-Agent header. When using a // cf_clearance cookie, this must match the browser UA the cookie came from. func WithUserAgent(ua string) Option { return func(cfg *config) { cfg.userAgent = ua } } // WithRateLimit sets the request interval and burst size for the token bucket // that guards every HTTP request the SDK makes. The default (1s, burst 1) is // the safest setting for FurAffinity; tightening it risks Cloudflare bans. func WithRateLimit(interval time.Duration, burst int) Option { return func(cfg *config) { cfg.rateInterval = interval cfg.rateBurst = burst } } // WithRequestsPerSecond is a shorthand for [WithRateLimit] when you think in // terms of throughput. A value of 0.5 means one request every two seconds. func WithRequestsPerSecond(rps float64) Option { return func(cfg *config) { if rps <= 0 { return } cfg.rateInterval = time.Duration(float64(time.Second) / rps) cfg.rateBurst = 1 } } // WithLogger attaches a structured logger. The SDK emits debug records for // retries and rate-limit waits; nothing is logged at info level. func WithLogger(l *slog.Logger) Option { return func(cfg *config) { if l != nil { cfg.logger = l } } } // WithHTTPClient lets callers supply a fully constructed *http.Client. Useful // for tests (httptest.Server) and for plugging in custom transports such as // uTLS. The SDK still wraps the client's Transport with its rate-limited // transport supply http.DefaultTransport for the default behaviour. func WithHTTPClient(hc *http.Client) Option { return func(cfg *config) { cfg.httpClient = hc } } // WithMaxRetries caps the number of automatic retry attempts on 429/5xx. // Defaults to 3. Set to 0 to disable retries entirely. func WithMaxRetries(n int) Option { return func(cfg *config) { if n >= 0 { cfg.maxRetries = n } } } // WithExperimentalJSONListings opts into the JSON-first merge strategy for // listing-page parsers (Gallery / Scraps / Favorites / Browse / Search / // SubmissionInbox). // // When enabled, the parser first reads the