Auto-generated user avatars: dicebear-go (constellation style, electric preset, seed = email) generated in background on login #376
Labels
No labels
actions
bug
cli
duplicate
enhancement
fork
forum
git storage
help wanted
insights
invalid
issues
moderation
oidc
ownership transfer
packages
pr/merge protection rules
projects
pull requests
question
releases
sponsorships
tags
webhooks
wiki
wontfix
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
crueber/walhub#376
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
What's requested
Auto-generate an avatar for every user who logs in without one:
dicebear-go— with the "constellation" style and the "electric" preset.Current state (code evidence)
avatarininternal/Go code orweb/src(the #371 navbar identity control is specified to take an optional avatar URL with username fallback, so the display side is already planned for it).users/<principal>/profile.json(CAS'd user profile,internal/identity/identity.go:10,148) with a GET/PUT API (/api/v1/users/{principal},internal/identity/http.go:29,236). The avatar's natural home is a field on this profile or a sibling object (users/<principal>/avatar.svg) — implementer's call, note which.(repo, kind)task single-flight (internal/wal/tasks.go); the maintainer's follow loop (internal/maintain/follow.go) shows the separate-cadence goroutine pattern. Avatar generation is a small, one-shot, per-login job — the task table may be overkill (it's repo-keyed); a simple goroutine with a dedup set (one in-flight generation per principal) is likely the right shape. Decide and state it.github.com/dicebear/dicebear-go/v10(+github.com/dicebear/styles/v10for the style definitions) — pure-Go, deterministic SVG generation from style + seed, no network calls, requires Go 1.23+ (repo is on Go 1.27, fine). The "constellation" style and "electric" preset are DiceBear catalog entries; verify both names exist in thestyles/v10package (constellation is a v9+ style; the preset maps to the style's options — check the exact option key, likely"preset": "electric").Proposed design
AGENTS.md§1 and the relevant doc's "Decisions & deviations" section (user-authorized 2026-09-12):github.com/dicebear/dicebear-go/v10+github.com/dicebear/styles/v10, purpose: deterministic user-avatar generation. Two modules, both zero-sub-dependencies per the library README — verify withgo mod graphbefore claiming it.internal/server/auth.go—MintSession/principalFromEmailcallers), after authentication succeeds, check whether the user has an avatar; if not, enqueue generation with seed = the user's email (exactly as specified — including when #370's usernames land, the seed stays the email), styleconstellation, presetelectric. Generate the SVG in-process (the library is local and fast — a goroutine, not a task-table job), write it to the user's avatar object, and set the profile's avatar reference. If a concurrent login for the same user is already generating, skip (single-flight by principal).GET /api/v1/users/{principal}/avataror a static-ish route — pick per the existing users routes inidentity/http.go), content-typeimage/svg+xml, with a long-lived cache class (generated output is deterministic per seed — effectively immutable; use the immutable cache class with the profile-version in the ETag or anavatar_versioncounter so regeneration busts caches).users/{principal}view, owner profile #234) use it too.renderBody/markdown does NOT inline avatars — display surfaces only.avatar_disabledflag on the profile, or treat deletion as opting out until they request one — pick and document); SVG safety (DiceBear output is generated locally from the style definition, not user input beyond the seed — the seed is the email, which is text substituted into the SVG; confirm the library escapes the seed and sanitize/escape it anyway before writing, per the repo's sanitize-first posture).Acceptance criteria
AGENTS.md§1 + the doc's deviations section, naming both modules;go.modshows exactly those additions (no transitive deps).Fix ready for review: #379 (branch fix/issue-376, no conflicts, NOT merged). Notes for the reviewer: (1) two issue-text assumptions did not survive verification — constellation has no options so there is no 'electric' preset key (defaults used), and the library pulls two build-required transitives (schema + jsonschema/v6, go mod graph proof in the PR); both are documented in AGENTS.md §1 + 01 Decisions. (2) Opt-out policy: DELETE records avatar_disabled and login never regenerates until POST regenerates. (3) Not merged per instructions.
REVIEW: PR #379 (fix/issue-376) — adversarial pass, new third-party dep. Verified in scratch worktree /tmp/walhub-376 (
944e2f5). No browser drive (per task rules; tests + reasoning). Main worktree untouched (tracked tree clean before/after).LAW-1: PASS. AGENTS.md §1 amendment names both modules (dicebear-go/v10 + styles/v10), purpose, user-authorized 2026-09-12, Forgejo #376, Decisions pointer, and both build-required transitives with go-mod-graph rationale. Deviations mirrored in docs/features/01 Decisions + docs/go/06 D1/§8.6. go.mod adds EXACTLY 2 direct (v10.7.0/v10.6.0) + 2 indirect (schema v1.5.1, jsonschema/v6 v6.0.2); x/text pre-existing. go mod graph + go mod why confirm both transitives come from dicebear-go internal/validate+render. Pinned. No net imports in dicebear-go or schema (grep clean). dlclark/regexp2 appears in the module graph but is imported ONLY by jsonschema's own example_regexp_test.go — go list -deps ./internal/identity proves it is NOT linked into the binary. Exception text covers build-required transitives; accurate.
PRESET DEVIATION: VERIFIED TRUE. styles/v10 constellation.json has no options key and no 'electric' string; 'electric' exists only in notionists.json. Generation with defaults IS the constellation style requested. avatar.go:69-78 + 01 Decisions document this honestly. Claim (b) likewise true (graph proof above).
GENERATION: PASS. AvatarHook seam (server.Options/avatarHook, fired post-mint in authCallback, wired in cmd/walhub/serve.go:avatarHookOf, nil in setup-only) only enqueues; EnsureAvatarAsync (avatar.go:288) spawns a goroutine behind a per-principal mutex dedup set, mutex never held across store calls. Fired ONLY on OIDC session-mint, never per-request. Failures are best-effort drops (TestEnsureAvatarAsyncFailureRetries); next login retries. Single-flight covered by TestEnsureAvatarSingleFlight; whole identity suite green under -race.
SEED/ESCAPING: PASS (stronger than asked). Seed=emailOf(p) (verified email, auth.go:312) at login; regen resolves via EmailForUsername with legacy-email fallback, 404s without mappable email (never invents a seed). Library deliberately excludes the raw seed from output (avatar.go upstream comment); TestGenerateSeedAbsent covers hostile seeds. I independently rendered hostile seeds ('">
Fixed by PR #379 (review clean — law-1 amendment + transitives verified, preset deviation confirmed honest, seed-escaping live-checked, opt-out honored), merged. Closing.