From 2df19af6ad2477589dc1402f62ca890e724f4339 Mon Sep 17 00:00:00 2001 From: mkelvers Date: Thu, 28 May 2026 12:18:03 +0200 Subject: [PATCH] refactor: centralize avatar URL generation and backfill existing users --- cmd/user/main.go | 5 +- .../fixes/20260528_backfill_avatar_url.go | 46 +++++++++++++++++++ internal/users/avatar.go | 11 +++++ 3 files changed, 60 insertions(+), 2 deletions(-) create mode 100644 internal/database/fixes/20260528_backfill_avatar_url.go create mode 100644 internal/users/avatar.go diff --git a/cmd/user/main.go b/cmd/user/main.go index 400dfd4..7b8d7ea 100644 --- a/cmd/user/main.go +++ b/cmd/user/main.go @@ -14,6 +14,7 @@ import ( "mal/internal/database" "mal/internal/db" "mal/internal/observability" + "mal/internal/users" ) func main() { @@ -91,7 +92,7 @@ func main() { } id := uuid.New().String() - avatarURL := fmt.Sprintf("https://api.dicebear.com/9.x/dylan/svg?seed=%s", username) + avatarURL := users.DefaultAvatarURL(username) _, err = dbConn.Exec("INSERT INTO user (id, username, password_hash, avatar_url) VALUES (?, ?, ?, ?)", id, username, string(hash), avatarURL) if err != nil { observability.Error("cli_user_create_failed", "cmd/user", "", map[string]any{"username": username}, err) @@ -117,7 +118,7 @@ func updateAvatars(dbConn *sql.DB) { os.Exit(1) } - avatarURL := fmt.Sprintf("https://api.dicebear.com/9.x/dylan/svg?seed=%s", username) + avatarURL := users.DefaultAvatarURL(username) _, err := dbConn.Exec("UPDATE user SET avatar_url = ? WHERE id = ?", avatarURL, id) if err != nil { observability.Error("cli_user_avatar_update_failed", "cmd/user", "", map[string]any{"username": username}, err) diff --git a/internal/database/fixes/20260528_backfill_avatar_url.go b/internal/database/fixes/20260528_backfill_avatar_url.go new file mode 100644 index 0000000..292a75f --- /dev/null +++ b/internal/database/fixes/20260528_backfill_avatar_url.go @@ -0,0 +1,46 @@ +package fixes + +import ( + "context" + "database/sql" + "fmt" + "mal/internal/users" +) + +func init() { + Register(Fix{ + ID: "20260528_backfill_avatar_url", + Apply: func(ctx context.Context, sqlDB *sql.DB) error { + rows, err := sqlDB.QueryContext(ctx, `SELECT id, username FROM user WHERE avatar_url = ''`) + if err != nil { + return err + } + defer func() { _ = rows.Close() }() + + type userRow struct { + id string + username string + } + toUpdate := make([]userRow, 0, 64) + for rows.Next() { + var r userRow + if err := rows.Scan(&r.id, &r.username); err != nil { + return err + } + toUpdate = append(toUpdate, r) + } + if err := rows.Err(); err != nil { + return err + } + + for _, u := range toUpdate { + avatarURL := users.DefaultAvatarURL(u.username) + if _, err := sqlDB.ExecContext(ctx, `UPDATE user SET avatar_url = ? WHERE id = ?`, avatarURL, u.id); err != nil { + return fmt.Errorf("update avatar_url for user %s: %w", u.id, err) + } + } + + return nil + }, + }) +} diff --git a/internal/users/avatar.go b/internal/users/avatar.go new file mode 100644 index 0000000..c02281f --- /dev/null +++ b/internal/users/avatar.go @@ -0,0 +1,11 @@ +package users + +import ( + "net/url" + "strings" +) + +func DefaultAvatarURL(username string) string { + seed := url.QueryEscape(strings.TrimSpace(username)) + return "https://api.dicebear.com/9.x/dylan/svg?seed=" + seed +}