// multiuser demonstrates the per-request credentials pattern: a single // fa.Client (one IP, one shared rate limiter) servicing many end users by // passing their cookies as per-call Options that override the client's // defaults. // // Run with one or more FA accounts encoded as comma-separated A:B:CF // tuples, e.g. // // FA_USERS="aCookieA:bCookieA:cfClearanceA,aCookieB:bCookieB:" \ // FA_UA="Mozilla/5.0 ..." \ // go run ./examples/multiuser 12345678 // // The CF clearance is optional per user (empty third field is fine). FA_UA // must match the UA the cf_clearance cookies were issued under. package main import ( "context" "fmt" "log" "os" "strconv" "strings" "time" fa "git.anthrove.art/public/go-fa-api" ) // UserCreds is whatever your storage layer hands back per end user. type UserCreds struct { Label string A, B string CFClearance string } func main() { if len(os.Args) < 2 { log.Fatalf("usage: %s ", os.Args[0]) } id, err := strconv.ParseInt(os.Args[1], 10, 64) if err != nil { log.Fatalf("invalid submission id: %v", err) } raw := os.Getenv("FA_USERS") if raw == "" { log.Fatal("FA_USERS must be set (comma-separated A:B:CF tuples)") } users := parseUsers(raw) if len(users) == 0 { log.Fatal("FA_USERS parsed to zero users") } ua := envOr("FA_UA", "go-fa-api-example/0.1") // One client. Built once at startup. The single rate limiter inside is // what protects the shared egress IP from Cloudflare bans no matter // how many users we add, the combined request rate stays at WithRPS. client := fa.New( fa.WithUserAgent(ua), fa.WithRequestsPerSecond(1), ) ctx := context.Background() for _, u := range users { start := time.Now() sub, err := client.GetSubmission(ctx, fa.SubmissionID(id), // Per-call overrides win over the client's defaults. The shared // limiter still gates this request, so user B cannot starve user // A and the combined rate never exceeds WithRequestsPerSecond. fa.WithCookies(fa.Cookies{A: u.A, B: u.B}), fa.WithCloudflare(fa.CFCookies{Clearance: u.CFClearance}), ) if err != nil { log.Printf("[%s] GetSubmission: %v", u.Label, err) continue } fmt.Printf("[%s] %s by %s (%s) fetched in %v\n", u.Label, sub.Title, sub.Author.DisplayName, sub.Rating, time.Since(start).Round(time.Millisecond)) } } func parseUsers(raw string) []UserCreds { var out []UserCreds for i, part := range strings.Split(raw, ",") { part = strings.TrimSpace(part) if part == "" { continue } fields := strings.SplitN(part, ":", 3) if len(fields) < 2 { log.Printf("FA_USERS[%d]: skipping malformed entry %q", i, part) continue } u := UserCreds{ Label: fmt.Sprintf("user%d", i+1), A: strings.TrimSpace(fields[0]), B: strings.TrimSpace(fields[1]), } if len(fields) == 3 { u.CFClearance = strings.TrimSpace(fields[2]) } out = append(out, u) } return out } func envOr(key, fallback string) string { if v := os.Getenv(key); v != "" { return v } return fallback }