inital commit

This commit is contained in:
2026-05-26 20:21:55 +02:00
parent 965f9d6ad4
commit 0ae20aa68d
21 changed files with 651 additions and 111 deletions

113
examples/multiuser/main.go Normal file
View 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
}

View File

@@ -25,7 +25,7 @@ func main() {
fa.WithUserAgent(envOr("FA_UA", "go-fa-api-example/0.1")),
)
n, err := client.Notifications(context.Background())
n, err := client.Notifications(context.Background(), fa.NotificationsOptions{})
if err != nil {
log.Fatalf("Notifications: %v", err)
}