Files
at-container-registry/pkg/atproto/profile.go
T

123 lines
4.2 KiB
Go

package atproto
import (
"context"
"encoding/json"
"errors"
"fmt"
"sync"
"time"
)
// Profile record key is always "self" per lexicon
const ProfileRKey = "self"
// Global map to track in-flight profile migrations (DID -> true)
// Used to prevent duplicate migration goroutines
var migrationLocks sync.Map
// EnsureProfile checks if a user's profile exists and creates it if needed
// This should be called during authentication (OAuth exchange or token service)
// If defaultHoldDID is provided, creates profile with that default (or empty if not provided)
// Expected format: "did:web:hold01.atcr.io"
// Normalizes URLs to DIDs for consistency (for backward compatibility)
func EnsureProfile(ctx context.Context, client *Client, defaultHoldDID string) error {
// Check if profile already exists
profile, err := client.GetRecord(ctx, SailorProfileCollection, ProfileRKey)
if err == nil && profile != nil {
// Profile exists, nothing to do
return nil
}
// Normalize to DID if it's a URL (or pass through if already a DID)
// This ensures we store DIDs consistently in new profiles
normalizedDID := ""
if defaultHoldDID != "" {
normalizedDID = ResolveHoldDIDFromURL(defaultHoldDID)
}
// Profile doesn't exist - create it
newProfile := NewSailorProfileRecord(normalizedDID)
_, err = client.PutRecord(ctx, SailorProfileCollection, ProfileRKey, newProfile)
if err != nil {
return fmt.Errorf("failed to create sailor profile: %w", err)
}
fmt.Printf("DEBUG [profile]: Created sailor profile with defaultHold=%s\n", normalizedDID)
return nil
}
// GetProfile retrieves the user's profile from their PDS
// Returns nil if profile doesn't exist
// Automatically migrates old URL-based defaultHold values to DIDs
func GetProfile(ctx context.Context, client *Client) (*SailorProfileRecord, error) {
record, err := client.GetRecord(ctx, SailorProfileCollection, ProfileRKey)
if err != nil {
// Check if it's a 404 (profile doesn't exist)
if errors.Is(err, ErrRecordNotFound) {
return nil, nil
}
return nil, fmt.Errorf("failed to get profile: %w", err)
}
// Parse the profile record
var profile SailorProfileRecord
if err := json.Unmarshal(record.Value, &profile); err != nil {
return nil, fmt.Errorf("failed to parse profile: %w", err)
}
// Migrate old URL-based defaultHold to DID format
// This ensures backward compatibility with profiles created before DID migration
if profile.DefaultHold != "" && !isDID(profile.DefaultHold) {
// Convert URL to DID transparently
migratedDID := ResolveHoldDIDFromURL(profile.DefaultHold)
profile.DefaultHold = migratedDID
// Persist the migration to PDS in a background goroutine
// Use a lock to ensure only one goroutine migrates this DID
did := client.did
if _, loaded := migrationLocks.LoadOrStore(did, true); !loaded {
// We got the lock - launch goroutine to persist the migration
go func() {
// Clean up lock when done (after a short delay to batch requests)
defer func() {
time.Sleep(1 * time.Second)
migrationLocks.Delete(did)
}()
// Create a new context with timeout for the background operation
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
// Update the profile on the PDS
profile.UpdatedAt = time.Now()
if err := UpdateProfile(ctx, client, &profile); err != nil {
fmt.Printf("WARNING [profile]: Failed to persist URL-to-DID migration for %s: %v\n", did, err)
} else {
fmt.Printf("DEBUG [profile]: Persisted defaultHold migration to DID: %s (for DID: %s)\n", migratedDID, did)
}
}()
}
}
return &profile, nil
}
// UpdateProfile updates the user's profile
// Normalizes defaultHold to DID format before saving
func UpdateProfile(ctx context.Context, client *Client, profile *SailorProfileRecord) error {
// Normalize defaultHold to DID if it's a URL
// This ensures we always store DIDs, even if user provides a URL
if profile.DefaultHold != "" && !isDID(profile.DefaultHold) {
profile.DefaultHold = ResolveHoldDIDFromURL(profile.DefaultHold)
fmt.Printf("DEBUG [profile]: Normalized defaultHold to DID: %s\n", profile.DefaultHold)
}
_, err := client.PutRecord(ctx, SailorProfileCollection, ProfileRKey, profile)
if err != nil {
return fmt.Errorf("failed to update profile: %w", err)
}
return nil
}