Files
go-fa-api/options.go
2026-05-25 22:27:18 +02:00

194 lines
7.1 KiB
Go

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 <script id="js-submissionData">
// blob FA embeds on every listing page and uses it as the primary source
// for title, author display name, URL-safe login, and avatar URL. HTML
// scraping fills in fields the JSON doesn't carry (ID, rating, thumbnail).
// If the script is absent or malformed on a given page, the parser
// transparently falls back to pure HTML scraping caller sees no error.
//
// This is gated as "experimental" because: (1) the JSON's description
// field carries BBCode rather than HTML and is occasionally truncated, so
// we intentionally don't use it; (2) FA could remove the script tag at
// any time, in which case the fallback path is what actually runs.
//
// Defaults to false. Most callers should leave it off until selector
// drift becomes a real problem.
func WithExperimentalJSONListings(enabled bool) Option {
return func(cfg *config) { cfg.jsonListings = enabled }
}
// WithPrioritizedRateLimiting enables multi-level priority on the client's
// rate limiter. It is an opt-in feature; by default the limiter serves every
// request in plain FIFO order.
//
// When enabled, each request is scheduled according to the [Priority] marker
// on its context (see [WithPriority] and [WithBackgroundPriority]): the
// limiter serves a higher-priority request before a lower-priority one. A
// request with no marker is [PriorityNormal]. The overall pace is unchanged —
// there is still a single global token bucket, since FA rate-limits per
// account only the order in which competing requests are served.
//
// Enable this when the application mixes user-driven requests with
// best-effort background work (preloading, crawling) and wants the former to
// never wait behind the latter. With the feature disabled, [Priority] markers
// are inert.
//
// Defaults to false.
func WithPrioritizedRateLimiting(enabled bool) Option {
return func(cfg *config) { cfg.priorityRL = enabled }
}