114 lines
3.0 KiB
Go
114 lines
3.0 KiB
Go
// 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 <submission-id>", 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
|
|
}
|