inital commit
This commit is contained in:
113
examples/multiuser/main.go
Normal file
113
examples/multiuser/main.go
Normal file
@@ -0,0 +1,113 @@
|
||||
// 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
|
||||
}
|
||||
Reference in New Issue
Block a user